Resource
Motivation
It's quite common to encounter a case where a developer will need to acquire a limited resource (e.g. file, socket), use that resource in some way and then properly release that resource. Failure to do so is an easy way to leak finite resources.
Resource
makes this common pattern easy to encode, without having to rely
on unwieldy and error prone try
/catch
/finally
blocks.
Use Case
Here's a basic example of using Resource
in the wild in relation to reading
a File
:
final Resource<RandomAccessFile> fileResource = Resource.make(
IO.fromFutureF(() => File('/path/to/file.bin').open()),
(raf) => IO.exec(() => raf.close()),
);
We now have a Resource
that will automatically handle opening and closing the
underlying resource (i.e. the RandomAccessFile
) regardless of whether the
operation we use the resource with succeeds, fails or is canceled. This
naturally begs the question of how we are supposed to use the resource. For
this example, let's say we need to read the first 100 bytes of data from the
file:
// Use the resource by passing an IO op to the 'use' function
final IO<Uint8List> program =
fileResource.use((raf) => IO.fromFutureF(() => raf.read(100)));
(await program.unsafeRunFutureOutcome()).fold(
() => print('Program canceled.'),
(err) => print('Error: ${err.message}. But the file was still closed!'),
(bytes) => print('Read ${bytes.length} bytes from file.'),
);
Combinators
Resource
comes with many of the same combinators at IO
like map
,
flatMap
, etc.
Because Resource
comes with these combinators, composing and managing
multiple resources at one time become easy! When using try
/catch
/finally
,
things can get messy at best, and incorrect at worst. But using Resource
it's possible to create readible, expressive code:
Resource<RandomAccessFile> openFile(String path) => Resource.make(
IO.fromFutureF(() => File(path).open()),
(raf) => IO.exec(() => raf.close()),
);
IO<Uint8List> readBytes(RandomAccessFile raf, int n) =>
IO.fromFutureF(() => raf.read(n));
IO<Unit> writeBytes(RandomAccessFile raf, Uint8List bytes) =>
IO.fromFutureF(() => raf.writeFrom(bytes)).voided();
Uint8List concatBytes(Uint8List a, Uint8List b) =>
Uint8List.fromList([a, b].expand((element) => element).toList());
/// Copy the first [n] bytes from [fromPathA] and [fromPathB], then write
/// bytes to [toPath]
IO<Unit> copyN(String fromPathA, String fromPathB, String toPath, int n) => (
openFile(fromPathA),
openFile(fromPathB),
openFile(toPath)
).tupled().useN(
(fromA, fromB, to) {
return (readBytes(fromA, n), readBytes(fromB, n))
.parMapN(concatBytes)
.flatMap((a) => writeBytes(to, a));
},
);
copyN(
'/from/this/file',
'/and/this/file',
'/to/that/file',
100,
).unsafeRunAndForget();