intermediate
The Bracket
Type class abstracts the ability to safely acquire, use, and then release a resource.
Essentially, it could be considered the functional programming equivalent to the well known imperative
try/catch/finally
structure.
Bracket
extends MonadError
.
It ensures that the acquired resource is released at the end after using it, even if the using action throws an error. That ensures that no errors are swallowed.
Another key point of Bracket
would be the ability to abstract over whether the resource is going to be used
synchronously or asynchronously.
Let’s say we want to work with a file. Let’s use a mock File
API to avoid messing with the real one here.
These methods would allow to open a File, close it and also read it’s content as a string.
import arrow.fx.IO
/**
* Mock File API.
*/
class File(url: String) {
fun open(): File = this
fun close(): Unit {}
override fun toString(): String = "This file contains some interesting content!"
}
fun openFile(uri: String): IO<File> = IO { File(uri).open() }
fun closeFile(file: File): IO<Unit> = IO { file.close() }
fun fileToString(file: File): IO<String> = IO { file.toString() }
Note that we wrapped them into IO
. IO
is able to wrap any side effecting computation to make it pure. In a real world system, these operations would contain
side effects since they’d end up accessing the file system.
Read the IO
docs for more context on this.
Now, let’s say we want to open a file, do some work with it, and then close it. With Bracket
, we could make that
process look like this:
import arrow.fx.IO
class File(url: String) {
fun open(): File = this
fun close(): Unit {}
override fun toString(): String = "This file contains some interesting content!"
}
fun openFile(uri: String): IO<File> = IO { File(uri).open() }
fun closeFile(file: File): IO<Unit> = IO { file.close() }
fun fileToString(file: File): IO<String> = IO { file.toString() }
fun main(args: Array<String>) {
//sampleStart
val safeComputation = openFile("data.json").bracket(
release = { file -> closeFile(file) },
use = { file -> fileToString(file) })
//sampleEnd
println(safeComputation)
}
This would ensure the file gets closed right after completing the use
operation, which would be fileToString(file)
here. If that operation throws an error, the file would also be closed.
Note that the result is still an IO.Async
operation, which means it’s still deferred (not executed yet).
We’ve mentioned that Bracket
is agnostic of whether the use
lambda is computed synchronously or asynchronously.
That’s because it’s able to run over any data type F
that can support synchronous and asynchronous
computations, like IO
or Observable
.
It basically targets what in Functional Programming is known as a “Higher Kind”.
There’s a complete section about this pattern in the docs.
Let’s learn more about how Bracket
can support this pattern as a Type class with a basic example showcasing this high level of abstraction technique.
import arrow.Kind
import arrow.fx.typeclasses.Bracket
class File(url: String) {
fun open(): File = this
fun close(): Unit {}
override fun toString(): String = "This file contains some interesting content!"
}
class Program<F>(BF: Bracket<F, Throwable>) : Bracket<F, Throwable> by BF {
fun openFile(uri: String): Kind<F, File> = just(File(uri).open())
fun closeFile(file: File): Kind<F, Unit> = just(file.close())
fun fileToString(file: File): Kind<F, String> = just(file.toString())
}
This is basically the same program from previous examples, but defined over any F
data type that there is an instance
of Bracket
for. In other words, this program is constrained by the capabilities that Bracket
can provide.
We are also fixing the error type from Bracket<F, E>
to be Throwable
.
Let’s run the program for the three mentioned data types as an example of polymorphism now.
We can run the program for IO
:
import arrow.fx.IO
import arrow.fx.extensions.io.bracket.bracket
import arrow.Kind
import arrow.fx.typeclasses.Bracket
class File(url: String) {
fun open(): File = this
fun close(): Unit {}
override fun toString(): String = "This file contains some interesting content!"
}
class Program<F>(BF: Bracket<F, Throwable>) : Bracket<F, Throwable> by BF {
fun openFile(uri: String): Kind<F, File> = just(File(uri).open())
fun closeFile(file: File): Kind<F, Unit> = just(file.close())
fun fileToString(file: File): Kind<F, String> = just(file.toString())
}
fun main(args: Array<String>) {
//sampleStart
val ioProgram = Program(IO.bracket())
val safeComputation = with (ioProgram) {
openFile("data.json").bracket(
release = { file -> closeFile(file) },
use = { file -> fileToString(file) })
}
//sampleEnd
println(safeComputation)
}
Let’s now run the same exact program also for ObservableK
:
import arrow.fx.rx2.ObservableK
import arrow.fx.rx2.extensions.observablek.bracket.bracket
import arrow.Kind
import arrow.fx.typeclasses.Bracket
class File(url: String) {
fun open(): File = this
fun close(): Unit {}
override fun toString(): String = "This file contains some interesting content!"
}
class Program<F>(BF: Bracket<F, Throwable>) : Bracket<F, Throwable> by BF {
fun openFile(uri: String): Kind<F, File> = just(File(uri).open())
fun closeFile(file: File): Kind<F, Unit> = just(file.close())
fun fileToString(file: File): Kind<F, String> = just(file.toString())
}
fun main(args: Array<String>) {
//sampleStart
val observableProgram = Program(ObservableK.bracket())
val safeComputation = with (observableProgram) {
openFile("data.json").bracket(
release = { file -> closeFile(file) },
use = { file -> fileToString(file) })
}
//sampleEnd
println(safeComputation)
}
Note that we’re running the exact same program passing in two different data types. All of them can provide an
instance of Bracket
, which means that they can support asynchronous and synchronous computations.
This is the style you’d usually use in a Functional Program.
Requires passing release
and use
lambdas. It ensures acquiring, using and releasing the resource at the end.
fun <A, B> Kind<F, A>.bracket(release: (A) -> Kind<F, Unit>, use: (A) -> Kind<F, B>): Kind<F, B>
import arrow.fx.IO
class File(url: String) {
fun open(): File = this
fun close(): Unit {}
override fun toString(): String = "This file contains some interesting content!"
}
fun openFile(uri: String): IO<File> = IO { File(uri).open() }
fun closeFile(file: File): IO<Unit> = IO { file.close() }
fun fileToString(file: File): IO<String> = IO { file.toString() }
fun main(args: Array<String>) {
//sampleStart
val safeComputation = openFile("data.json").bracket(
release = { file -> closeFile(file) },
use = { file -> fileToString(file) })
//sampleEnd
println(safeComputation)
}
It’s a generalized version of bracket()
which uses ExitCase
to distinguish between different exit cases when
releasing the acquired resource. ExitCase
can take the values Completed
, Canceled
, or Error(e)
. So depending
how the use
execution finalizes, the corresponding ExitCase
value will be passed to the release
lambda.
Requires passing release
and use
lambdas. It ensures acquiring, using and releasing the resource at the end.
fun <A, B> Kind<F, A>.bracketCase(release: (A, ExitCase<Throwable>) -> Kind<F, Unit>, use: (A) -> Kind<F, B>): Kind<F, B>
import arrow.fx.IO
import arrow.fx.typeclasses.ExitCase
class File(url: String) {
fun open(): File = this
fun close(): Unit {}
override fun toString(): String = "This file contains some interesting content!"
}
fun openFile(uri: String): IO<File> = IO { File(uri).open() }
fun closeFile(file: File): IO<Unit> = IO { file.close() }
fun fileToString(file: File): IO<String> = IO { file.toString() }
fun main(args: Array<String>) {
//sampleStart
val safeComputation = openFile("data.json").bracketCase(
release = { file, exitCase ->
when (exitCase) {
is ExitCase.Completed -> { /* do something */ }
is ExitCase.Canceled -> { /* do something */ }
is ExitCase.Error -> { /* do something */ }
}
closeFile(file)
},
use = { file ->
fileToString(file)
})
//sampleEnd
println(safeComputation)
}
For a full list of other useful combinators available in Bracket
see the Source
Arrow provides BracketLaws
in the form of test cases for internal
verification of lawful instances and third party apps creating their own Bracket instances.
Bracket
instancesArrow already provides Bracket instances for most common datatypes both in Arrow and the Kotlin stdlib. Oftentimes you may find the need to provide your own for unsupported datatypes.
You may create or automatically derive instances of Bracket
for your own datatypes which you will be able to use in
the context of abstract polymorphic code.
See Deriving and creating custom typeclass
Module | Data types |
arrow.fx | IO |
arrow.fx.reactor | FluxK, MonoK |
arrow.fx.rx2 | FlowableK, MaybeK, ObservableK, SingleK |