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<List<Int>, Int> = Optional(
getOption = { list -> list.firstOrNull().toOption() },
set = { list, int -> list.mapIndexed { index, value -> if (index == 0) int else value } }
)
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.*
POptional.listHead<Int>().set(listOf(1, 3, 6), 5)
POptional.listHead<Int>().modify(listOf(1, 3, 6)) { head -> head * 5 }
We can also lift such functions.
val lifted = POptional.listHead<Int>().lift { head -> head * 5 }
lifted(emptyList<Int>())
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.getOrNull(Some(Participant("test", "email")))
optEmail.getOrNull(None)
optEmail.getOrNull(Some(Participant("test", null)))
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<Pair<Int, String>>
that can polymorphically change its content to Pair<String, String>
and a PLens
with a focus into the Pair<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 Pair
within.
val pprism = PPrism.pSome<Pair<Int, String>, Pair<String, String>>()
val plens = PLens.pairPFirst<Int, String, String>()
val somePair: POptional<Option<Pair<Int, String>>, Option<Pair<String, String>>, Int, String> =
pprism compose plens
val lifted: (Option<Pair<Int, String>>) -> Option<Pair<String, String>> = somePair.lift { _ -> "Hello, " }
lifted(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?
✖