An Optional
is an optic that allows seeing into a structure and getting, setting, or modifying an optional focus.
It combines the properties of a Lens
(getting, setting, and modifying) with the properties of a Prism
(an optional focus).
Optional
combines their weakest functions: set
and getOrModify
.
set: (S, A) -> S
, meaning we can look into S
, set a value for an optional focus A
, and obtain the modified source.getOrModify: (S) -> Either<S, A>
, meaning we can get the focus OR return the original value.For a structure List<Int>
, we can create an Optional
to focus an optional head Int
.
import arrow.core.*
import arrow.optics.*
val optionalHead: Optional<ListK<Int>, Int> = Optional(
getOption = { list -> list.firstOrNull().toOption() },
set = { list, int -> list.mapIndexed { index, value -> if (index == 0) int else value }.k() }
)
Our optionalHead
allows us to operate on the head of List<Int>
without having to worry if it is available. You can find optionalHead
in the optics library: ListK.head<Int>()
.
import arrow.optics.extensions.*
ListK.head<Int>().set(listOf(1, 3, 6).k(), 5)
// [5, 3, 6]
ListK.head<Int>().modify(listOf(1, 3, 6).k()) { head -> head * 5 }
// [5, 3, 6]
We can also lift such functions.
val lifted = ListK.head<Int>().lift { head -> head * 5 }
lifted(emptyList<Int>().k())
// []
Or modify or lift functions using Applicative
.
import arrow.fx.IO
import arrow.core.extensions.option.applicative.*
ListK.head<Int>().modifyF(Option.applicative(), listOf(1, 3, 6).k()) { head ->
Option.just(head/2)
}
// Option.Some([0, 3, 6])
import arrow.fx.extensions.io.applicative.*
import arrow.fx.fix
val liftedFO = ListK.head<Int>().liftF(IO.applicative()) { head ->
IO.effect { head / 0 }
}
liftedFO(listOf(1, 3, 6).k()).fix().attempt().unsafeRunSync()
// Either.Left(java.lang.ArithmeticException: / by zero)
An Optional
instance can be manually constructed from any default or custom Iso
, Lens
, or Prism
instance by calling their asOptional()
or by creating a custom Optional
instance as shown above.
We can compose Optional
s to build telescopes with an optional focus. Imagine we try to retrieve a User
’s email from a backend. The result of our call is Option<User>
. So, we first want to look into Option
, which optionally could be a Some
. And then we want to look into User
, which optionally filled in his email.
data class Participant(val name: String, val email: String?)
val participantEmail: Optional<Participant, String> = Optional(
getOrModify = { participant -> participant.email?.right() ?: participant.left() },
set = { participant, email -> participant.copy(email = email) }
)
val optEmail: Optional<Option<Participant>, String> = PPrism.some<Participant>() compose participantEmail
optEmail.getOption(Some(Participant("test", "email")))
// Option.Some(email)
optEmail.getOption(None)
// Option.None
optEmail.getOption(Some(Participant("test", null)))
// Option.None
Optional
can be composed with all optics, resulting in the following optics:
Iso | Lens | Prism | Optional | Getter | Setter | Fold | Traversal | |
---|---|---|---|---|---|---|---|---|
Optional | Optional | Optional | Optional | Optional | Fold | Setter | Fold | Traversal |
To avoid boilerplate, optionals can be generated for A?
, and Option<A>
fields for a data class
.
The Optionals
will be generated as extension properties on the companion object val T.Companion.paramName
.
@optics data class Person(val age: Int?, val address: Option<Address>) {
companion object
}
val optionalAge: Optional<Person, Int> = Person.age
val optionalAddress: Optional<Person, Address> = Person.address
A POptional
is very similar to PLens and PPrism. So let’s see if we can combine both examples shown in their documentation.
Given a PPrism
with a focus into Some
of Option<Tuple2<Int, String>>
that can polymorphically change its content to Tuple2<String, String>
and a PLens
with a focus into the Tuple2<Int, String>
that can morph the first parameter from Int
to String
, we can compose them together building an Optional
that can look into Option
and morph the first type of the Tuple2
within.
val pprism = PPrism.pSome<Tuple2<Int, String>, Tuple2<String, String>>()
val plens = Tuple2.pFirst<Int, String, String>()
val someTuple2: POptional<Option<Tuple2<Int, String>>, Option<Tuple2<String, String>>, Int, String> =
pprism compose plens
val lifted: (Option<Tuple2<Int, String>>) -> Option<Tuple2<String, String>> = someTuple2.lift { _ -> "Hello, " }
lifted(None)
// Option.None
Arrow provides OptionalLaws
in the form of test cases for internal verification of lawful instances and third party apps creating their own optionals.
Do you like Arrow?
✖