Skip to main content

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

tip

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();