This is a F# library for simple optics which is highly inspired by Aether. I also had a look at FSharpPlus (but I didn't understand it tbh).
I created this library to learn what optics are and how they work.
Supported optics
- Lenses
- Prisms
Optics are a way to work conveniently with deeply nested data structures in an immutable way. For that you define elemental optics and then compose them to get optics for more complex structures.
To further explain this, I chose a phone book as an example.
type Address = {
street: string
city: string
}
type User = {
adress: Address
phoneNumber: string
}If you need to change the city of a user in plain F# you have to do something like this:
// user: User
let user' = { user with address = { user.address with city = "Berlin" }}And this doesn't get better for more nested structures. But you can use Optics to do that more conveniently.
At first you define an optic called lens for the address field in User. We'll call it UserOptic.address. Then you can create a lens for city in Address, we'll call it AddressOptic.city. Then you can define a UserOptic for city by composing both lenses. If you have that boilerplate code you can simply set the city with the defined lens. With this library this looks like this:
open SimpleOptics
// user: User
let user' = Optic.set UserOptic.city user "Berlin"And this does always looks the same for more nested structures. What changes is only the optics boiler plate.
Then there are Prisms. Those are for data which could not be there like an entry in a map. You can combine Lenses and Prisms and will always get a prism back.
For all examples you have to open the SimpleOptics namespace:
open SimpleOpticsLike in Aether you define your optics as a pair of getter and setter. e.g.
// Lens<Address,City>
Lens((fun address -> address.city), (fun address city' -> { address with city = city'}))
// Prism<User,FirstName>
Prism((fun user -> user.firstName), (fun user firstName' -> { user with firstName = Some firstName' }))Since F# 8 you can use the short form for the getters. e.g.
// Lens<Address,City>
Lens(_.city, (fun address city' -> { address with city = city'}))
// Prism<User,FirstName>
Prism(_.firstName, (fun user firstName' -> { user with firstName = Some firstName' }))If your properties aren't unique enough for the type inference to grasp the correct types, you can specify them partially. e.g.
// Lens<Address,City>
let lens: Lens<Address, _> = Lens(_.city, (fun address city' -> { address with city = city'}))
// Prism<User,FirstName>
let prism: Prism<User, _> = Prism(_.firstName, (fun user firstName' -> { user with firstName = Some firstName' }))To combine optics you can either use the compose function from the Optic module or use the >-> operator:
module UserOptic =
let city = Optic.compose UserOptic.address AddressOptic.city
let street = UserOptic.address >-> AddressOptic.cityTo use optics you can either use the functions from the Optic module or the equivalent operator:
// Get
let street = Optic.get UserOptic.street user
let city = user ^. UserOptic.city
// Set
let user1 = Optic.set UserOptic.street "Main Street" user
let user2 = (UserOptic.city ^= "Berlin") user
// Map
let user3 = Optic.map UserOptic.street (fun street -> street.ToUpper()) user
let user4 = UserOptic.city ^% (fun city -> city.ToUpper()) userThere are a few generic presets for common structures you can use. To use them you have
to open the SimpleOptics.Presets namespace:
open SimpleOptics.PresetsYou can then use the defined presets. At the moment these are:
- ListOptic
- ArrayOptic