Lens is an abstraction over function that allow to read and update parts of immutable data.
The abstraction name comes from the analogy of focusing on a specific part of the data structure.
Another analogy could be with pointers, but in this case data is treated as immutable which means that instead of modifying it returns a new copy.
In this quick tour you can find some basic examples of operating with Lenses.
To allow lensing over your record types, lens (as functions) have to be written by hand for each field.
As a convention, all lens identifiers will start with an underscore _
.
Here's an example usage of lenses with business objects:
open System
open FSharpPlus
// In order to use the Lens module of F#+ we import the following:
open FSharpPlus.Lens
// From Mauricio Scheffer: https://gist.github.com/mausch/4260932
type Person =
{ Name: string
DateOfBirth: DateTime }
module Person =
let inline _name f p =
f p.Name <&> fun x -> { p with Name = x }
type Page =
{ Contents: string }
module Page =
let inline _contents f p =
f p.Contents <&> fun x -> {p with Contents = x}
type Book =
{ Title: string
Author: Person
Pages: Page list }
module Book =
let inline _author f b =
f b.Author <&> fun a -> { b with Author = a }
let inline _authorName b = _author << Person._name <| b
let inline _pages f b =
f b.Pages <&> fun p -> { b with Pages = p }
let inline _pageNumber i b =
_pages << List._item i << _Some <| b
let rayuela =
{ Book.Title = "Rayuela"
Author = { Person.Name = "Julio Cortázar"
DateOfBirth = DateTime(1914, 8, 26) }
Pages = [
{ Contents = "Once upon a time" }
{ Contents = "The End"} ] }
// read book author name:
let authorName1 = view Book._authorName rayuela
// you can also write the read operation as:
let authorName2 = rayuela ^. Book._authorName
// write value through a lens
let book1 = setl Book._authorName "William Shakespear" rayuela
// update value
let book2 = over Book._authorName String.toUpper rayuela
Note:
The operator <&>
is not available in F#+ v1.0 but since it's a flipped map, you can use </flip map/>
instead.
However it's recommended to upgrade F#+ since you'll get better compile times with <&>
.
Also called a Partial Lens, they focus in parts of the data that could be there or not.
See the following example using the built-in _Some
prism.
type Team = { Name: string; Victories: int }
let inline _name f t = f t.Name <&> fun n -> { t with Name = n }
let inline _victories f t = f t.Victories <&> fun v -> { t with Victories = v }
type Player = { Team: Team; Score: int }
let inline _team f p = f p.Team <&> fun t -> { p with Team = t }
let inline _score f p = f p.Score <&> fun s -> { p with Score = s }
type Result = { Winner: Player option; Started: bool}
let inline _winner f r = f r.Winner <&> fun w -> { r with Winner = w }
let inline _started f r = f r.Started <&> fun s -> { r with Started = s }
type Match<'t> = { Players: 't; Finished: bool }
// For polymorphic updates to be possible, we can't use `with` expression on generic field lens.
let inline _players f m = f m.Players <&> fun p -> { Finished = m.Finished; Players = p }
let inline _finished f m = f m.Finished <&> fun f -> { m with Finished = f }
// Lens composed with Prism -> Prism
let inline _winnerTeam x = (_players << _winner << _Some << _team) x
// initial state
let match0 =
{ Players =
{ Team = { Name = "The A Team"; Victories = 0 }; Score = 0 },
{ Team = { Name = "The B Team"; Victories = 0 }; Score = 0 }
Finished = false }
// Team 1 scores
let match1 = over (_players << _1 << _score) ((+) 1) match0
// Team 2 scores
let match2 = over (_players << _2 << _score) ((+) 1) match1
// Produce Match<Result> from Match<Player * Player>
// This is possible with these Lenses since they support polymorphic updates.
let matchResult0 = setl _players { Winner = None; Started = true } match2
// See if there is a winner by using a prism
let _noWinner = preview _winnerTeam matchResult0
// Team 1 scores
let match3 = over (_players << _1 << _score) ((+) 1) match2
// End of the match
let match4 = setl _finished true match3
let match5 = over (_players << _1 << _team << _victories) ((+) 1) match4
let matchResult1 = over _players (fun (x, _) -> { Winner = Some x; Started = true }) match5
// And the winner is ...
let winner = preview _winnerTeam matchResult1
let t1 = [|"Something"; ""; "Something Else"; ""|] |> setl (_all "") ("Nothing")
// val t1 : string [] = [|"Something"; "Nothing"; "Something Else"; "Nothing"|]
// we can preview it
let t2 = [|"Something"; "Nothing"; "Something Else"; "Nothing"|] |> preview (_all "Something")
// val t2 : string option = Some "Something"
// view all elements in a list
let t3 = [|"Something"; "Nothing"; "Something Else"; "Nothing"|] |> toListOf (_all "Something")
// val t3 : string list = ["Something"]
// also view it, since string is a monoid
let t4 = [|"Something"; "Nothing"; "Something Else"; "Nothing"|] |> view (_all "Something")
// val t4 : string = "Something"
// Lens composed with a Traversal -> Traversal
let t5 = [((), "Something"); ((),""); ((), "Something Else"); ((),"")] |> preview (_all ((),"Something") << _2)
// val t5 : Option<string> = Some "Something"
open FSharpPlus.Lens
open FSharpPlus // This module contain other functions relevant for the examples (length, traverse)
open FSharpPlus.Data // Mult
let f1 = over both length ("hello","world")
// val f1 : int * int = (5, 5)
let f2 = ("hello","world")^.both
// val f2 : string = "helloworld"
let f3 = anyOf both ((=)'x') ('x','y')
// val f3 : bool = true
let f4 = (1,2)^..both
// val f4 : int list = [1; 2]
let f5 = over items length ["hello";"world"]
// val f5 : int list = [5; 5]
let f6 = ["hello";"world"]^.items
// val f6 : string = "helloworld"
let f7 = anyOf items ((=)'x') ['x';'y']
// val f7 : bool = true
let f8 = [1;2]^..items
// val f8 : int list = [1; 2]
let f9 = foldMapOf (traverse << both << _Some) Mult [(Some 21, Some 21)]
// val f9 : Mult<int> = Mult 441
let f10 = foldOf (traverse << both << _Some) [(Some 21, Some 21)]
// val f10 : int = 42
let f11 = allOf both (fun x-> x >= 3) (4,5)
// val f11 : bool = true
let toOption (isSome, v) = if isSome then Some v else None
let fromOption = function Some (x:'t) -> (true, x) | None -> (false, Unchecked.defaultof<'t>)
let inline isoTupleOption x = x |> iso toOption fromOption
let i1 = view isoTupleOption (System.Int32.TryParse "42")
// val i1 : int option = Some 42
let i2 = view (from' isoTupleOption) (Some 42)
// val i2 : bool * int = (true, 42)
// Iso composed with a Lens -> Lens
let i3 = view (_1 << isoTupleOption) (System.Int32.TryParse "42", ())
// val i3 : int option = Some 42
let fv3 = maximumOf (traverse << both << _Some) [(Some 1, Some 2);(Some 3,Some 4)]
// val fv3 : int option = Some 4
let fv4 = minimumOf (traverse << both << _Some) [(Some 1, Some 2);(Some 3,Some 4)]
// val fv4 : int option = Some 1
-
Highly recommended Matt Thornton's blog Grokking Lenses.
It contains examples using F#+ and an explanation from scratch.
namespace System
namespace FSharpPlus
module Lens
from FSharpPlus
<summary>
Lens functions and operators
</summary>
type Person =
{
Name: string
DateOfBirth: DateTime
}
Person.Name: string
Multiple items
val string: value: 'T -> string
--------------------
type string = String
Person.DateOfBirth: DateTime
Multiple items
[<Struct>]
type DateTime =
new: year: int * month: int * day: int -> unit + 10 overloads
member Add: value: TimeSpan -> DateTime
member AddDays: value: float -> DateTime
member AddHours: value: float -> DateTime
member AddMilliseconds: value: float -> DateTime
member AddMinutes: value: float -> DateTime
member AddMonths: months: int -> DateTime
member AddSeconds: value: float -> DateTime
member AddTicks: value: int64 -> DateTime
member AddYears: value: int -> DateTime
...
<summary>Represents an instant in time, typically expressed as a date and time of day.</summary>
--------------------
DateTime ()
(+0 other overloads)
DateTime(ticks: int64) : DateTime
(+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : DateTime
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: DateTimeKind) : DateTime
(+0 other overloads)
val _name: f: (string -> 'a) -> p: Person -> 'b (requires member Map)
val f: (string -> 'a) (requires member Map)
val p: Person
val x: string
type Page =
{ Contents: string }
Page.Contents: string
val _contents: f: (string -> 'a) -> p: Page -> 'b (requires member Map)
val p: Page
type Book =
{
Title: string
Author: Person
Pages: Page list
}
Book.Title: string
Book.Author: Person
Multiple items
module Person
from Lens
--------------------
type Person =
{
Name: string
DateOfBirth: DateTime
}
Book.Pages: Page list
Multiple items
module Page
from Lens
--------------------
type Page =
{ Contents: string }
type 'T list = List<'T>
val _author: f: (Person -> 'a) -> b: Book -> 'b (requires member Map)
val f: (Person -> 'a) (requires member Map)
val b: Book
val a: Person
val _authorName: b: (string -> 'a) -> (Book -> 'c) (requires member Map and member Map)
val b: (string -> 'a) (requires member Map and member Map)
val _pages: f: (Page list -> 'a) -> b: Book -> 'b (requires member Map)
val f: (Page list -> 'a) (requires member Map)
val p: Page list
val _pageNumber: i: int -> b: (Page -> 'a) -> (Book -> 'e) (requires member Map and member Return and member Map and member Map)
val i: int
val b: (Page -> 'a) (requires member Map and member Return and member Map and member Map)
Multiple items
module List
from FSharpPlus.Lens
--------------------
module List
from FSharpPlus
<summary>
Additional operations on List
</summary>
--------------------
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| op_Nil
| op_ColonColon of Head: 'T * Tail: 'T list
interface IReadOnlyList<'T>
interface IReadOnlyCollection<'T>
interface IEnumerable
interface IEnumerable<'T>
member GetReverseIndex: rank: int * offset: int -> int
member GetSlice: startIndex: int option * endIndex: int option -> 'T list
static member Cons: head: 'T * tail: 'T list -> 'T list
member Head: 'T
member IsEmpty: bool
member Item: index: int -> 'T with get
...
val _item: i: int -> f: ('f option -> 'g) -> t: 'f list -> 'h (requires member Map)
<summary>
Given a specific key, produces a Lens from a List<value> to an Option<value>. When setting,
a Some(value) will insert or replace the value into the list at the given index. Setting a value of
None will delete the value at the specified index. Works well together with non.
</summary>
val _Some: x: ('f -> 'g) -> ('f option -> 'i) (requires member Map and member Return)
<summary>
Prism providing a Traversal for targeting the 'Some' part of an Option<'T>
</summary>
val rayuela: Book
Multiple items
module Book
from Lens
--------------------
type Book =
{
Title: string
Author: Person
Pages: Page list
}
val authorName1: string
val view: lens: (('a -> Data.Const<'a,'b>) -> 'c -> Data.Const<'d,'e>) -> ('c -> 'd)
<summary>Read from a lens.</summary>
<param name="lens">The lens.</param>
<param name="source">The object.</param>
<returns>The part the lens is targeting.</returns>
val authorName2: string
val book1: Book
val setl: lens: (('a -> Data.Identity<'b>) -> 'c -> Data.Identity<'d>) -> v: 'b -> ('c -> 'd)
<summary>Write to a lens.</summary>
<param name="lens">The lens.</param>
<param name="v">The value we want to write in the part targeted by the lens.</param>
<param name="source">The original object.</param>
<returns>The new object with the value modified.</returns>
val book2: Book
val over: lens: (('a -> Data.Identity<'b>) -> 'c -> Data.Identity<'d>) -> f: ('a -> 'b) -> ('c -> 'd)
<summary>Update a value in a lens.</summary>
<param name="lens">The lens.</param>
<param name="f">A function that converts the value we want to write in the part targeted by the lens.</param>
<param name="source">The original object.</param>
<returns>The new object with the value modified.</returns>
Multiple items
type String =
interface IEnumerable<char>
interface IEnumerable
interface ICloneable
interface IComparable
interface IComparable<string>
interface IConvertible
interface IEquatable<string>
new: value: nativeptr<char> -> unit + 8 overloads
member Clone: unit -> obj
member CompareTo: value: obj -> int + 1 overload
...
<summary>Represents text as a sequence of UTF-16 code units.</summary>
--------------------
String(value: nativeptr<char>) : String
String(value: char array) : String
String(value: ReadOnlySpan<char>) : String
String(value: nativeptr<sbyte>) : String
String(c: char, count: int) : String
String(value: nativeptr<char>, startIndex: int, length: int) : String
String(value: char array, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int) : String
String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : String
val toUpper: source: string -> string
<summary>
Converts to uppercase -- nullsafe function wrapper for String.ToUpperInvariant method.
</summary>
type Team =
{
Name: string
Victories: int
}
Team.Name: string
Team.Victories: int
Multiple items
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> =
int
val _name: f: (string -> 'a) -> t: Team -> 'b (requires member Map)
val t: Team
val n: string
val _victories: f: (int -> 'a) -> t: Team -> 'b (requires member Map)
val f: (int -> 'a) (requires member Map)
val v: int
type Player =
{
Team: Team
Score: int
}
Multiple items
Player.Team: Team
--------------------
type Team =
{
Name: string
Victories: int
}
Player.Score: int
val _team: f: (Team -> 'a) -> p: Player -> 'b (requires member Map)
val f: (Team -> 'a) (requires member Map)
val p: Player
Player.Team: Team
val _score: f: (int -> 'a) -> p: Player -> 'b (requires member Map)
val s: int
Multiple items
module Result
from FSharpPlus
<summary>
Additional operations on Result<'T,'Error>
</summary>
--------------------
module Result
from Microsoft.FSharp.Core
--------------------
type Result =
{
Winner: Player option
Started: bool
}
--------------------
[<Struct>]
type Result<'T,'TError> =
| Ok of ResultValue: 'T
| Error of ErrorValue: 'TError
Result.Winner: Player option
Multiple items
val option: f: ('g -> 'h) -> n: 'h -> _arg1: 'g option -> 'h
<summary>
Takes a function, a default value and a option value. If the option value is None, the function returns the default value.
Otherwise, it applies the function to the value inside Some and returns the result.
</summary>
<category index="0">Common Combinators</category>
--------------------
type 'T option = Option<'T>
Result.Started: bool
type bool = Boolean
val _winner: f: (Player option -> 'a) -> r: Result -> 'b (requires member Map)
val f: (Player option -> 'a) (requires member Map)
val r: Result
val w: Player option
val _started: f: (bool -> 'a) -> r: Result -> 'b (requires member Map)
val f: (bool -> 'a) (requires member Map)
val s: bool
type Match<'t> =
{
Players: 't
Finished: bool
}
't
Match.Players: 't
Match.Finished: bool
val _players: f: ('a -> 'b) -> m: Match<'a> -> 'd (requires member Map)
val f: ('a -> 'b) (requires member Map)
val m: Match<'a>
Match.Players: 'a
val p: 'c
val _finished: f: (bool -> 'a) -> m: Match<'b> -> 'c (requires member Map)
val m: Match<'b>
val f: bool
val _winnerTeam: x: (Team -> 'a) -> (Match<Result> -> 'g) (requires member Map and member Map and member Return and member Map and member Map)
val x: (Team -> 'a) (requires member Map and member Map and member Return and member Map and member Map)
val match0: Match<Player * Player>
val match1: Match<Player * Player>
val _1: f: ('a -> 'b) -> t: 'f -> 'e (requires member Map and member MapItem1 and member Item1)
<summary>
Lens for the first element of a tuple
</summary>
val match2: Match<Player * Player>
val _2: f: ('a -> 'b) -> t: 'f -> 'e (requires member Map and member MapItem2 and member Item2)
<summary>
Lens for the second element of a tuple
</summary>
val matchResult0: Match<Result>
union case Option.None: Option<'T>
val _noWinner: Team option
val preview: prism: (('a -> Data.Const<Data.First<'a>,'b>) -> 'c -> Data.Const<Data.First<'d>,'e>) -> ('c -> 'd option)
<summary>Retrieve the first value targeted by a Prism, Fold or Traversal (or Some result from a Getter or Lens). See also (^?).</summary>
<param name="prism">The prism.</param>
<param name="source">The object.</param>
<returns>The value (if any) the prism is targeting.</returns>
val match3: Match<Player * Player>
val match4: Match<Player * Player>
val match5: Match<Player * Player>
val matchResult1: Match<Result>
val x: Player
union case Option.Some: Value: 'T -> Option<'T>
val winner: Team option
val t1: string array
val _all: ref: 'a -> f: ('a -> 'b) -> s: 'c -> 'd (requires equality and member Return and member Traverse)
val t2: string option
val t3: string list
val toListOf: l: (('a -> Data.Const<Data.Endo<'a list>,'b>) -> 'c -> Data.Const<Data.Endo<'d list>,'e>) -> ('c -> 'd list)
<summary>
Extract a list of the targets of a Fold. See also (^..).
</summary>
val t4: string
val t5: string option
namespace FSharpPlus.Data
val f1: int * int
val over: lens: (('a -> Identity<'b>) -> 'c -> Identity<'d>) -> f: ('a -> 'b) -> ('c -> 'd)
<summary>Update a value in a lens.</summary>
<param name="lens">The lens.</param>
<param name="f">A function that converts the value we want to write in the part targeted by the lens.</param>
<param name="source">The original object.</param>
<returns>The new object with the value modified.</returns>
val both: f: ('a -> 'b) -> a: 'a * b: 'a -> 'f (requires member Map and member (<*>))
val length: source: 'Foldable<'T> -> int (requires member Length)
<summary>Gets the number of elements in the foldable.</summary>
<category index="11">Foldable</category>
<param name="source">The input foldable.</param>
<returns>The length of the foldable.</returns>
val f2: string
val f3: bool
val anyOf: l: (('a -> Const<Any,'b>) -> 'c -> Const<Any,'d>) -> f: ('a -> bool) -> ('c -> bool)
val f4: int list
val f5: int list
val items: x: ('a -> 'b) -> ('c -> 'd) (requires member Traverse)
val f6: string
val f7: bool
val f8: int list
val f9: Mult<int>
val foldMapOf: l: (('a -> Const<'b,'c>) -> 'd -> Const<'e,'f>) -> f: ('a -> 'b) -> ('d -> 'e)
val traverse: f: ('T -> 'Functor<'U>) -> t: 'Traversable<'T> -> 'Functor<'Traversable<'U>> (requires member Traverse)
<summary>
Map each element of a structure to an action, evaluate these actions from left to right, and collect the results.
</summary>
<category index="13">Traversable</category>
Multiple items
union case Mult.Mult: 'a -> Mult<'a>
--------------------
[<Struct>]
type Mult<'a> =
| Mult of 'a
static member (+) : Mult<'n> * Mult<'n> -> Mult<'a2> (requires member ( * ))
static member Zero: unit -> Mult<'a1> (requires member One)
<summary>
Numeric wrapper for multiplication monoid (*, 1)
</summary>
val f10: int
val foldOf: l: (('a -> Const<'a,'b>) -> 'c -> Const<'d,'e>) -> ('c -> 'd)
val f11: bool
val allOf: l: (('a -> Const<All,'b>) -> 'c -> Const<All,'d>) -> f: ('a -> bool) -> ('c -> bool)
val x: int
val toOption: isSome: bool * v: 'a -> 'a option
val isSome: bool
val v: 'a
val fromOption: _arg1: 't option -> bool * 't
val x: 't
module Unchecked
from Microsoft.FSharp.Core.Operators
val defaultof<'T> : 'T
val isoTupleOption: x: 'a -> 'b (requires member Dimap and member Map)
val x: 'a (requires member Dimap and member Map)
val iso: func: ('s -> 'a) -> inv: ('b -> 't) -> ('i -> 'j) (requires member Dimap and member Map)
<summary>Build an 'Iso' from a pair of inverse functions.</summary>
<param name="func">The transform function.</param>
<param name="inv">The inverse of the transform function.</param>
<returns>The iso.</returns>
val i1: int option
val view: lens: (('a -> Const<'a,'b>) -> 'c -> Const<'d,'e>) -> ('c -> 'd)
<summary>Read from a lens.</summary>
<param name="lens">The lens.</param>
<param name="source">The object.</param>
<returns>The part the lens is targeting.</returns>
[<Struct>]
type Int32 =
member CompareTo: value: int -> int + 1 overload
member Equals: obj: int -> bool + 1 overload
member GetHashCode: unit -> int
member GetTypeCode: unit -> TypeCode
member ToString: unit -> string + 3 overloads
member TryFormat: destination: Span<char> * charsWritten: byref<int> * ?format: ReadOnlySpan<char> * ?provider: IFormatProvider -> bool
static member Parse: s: ReadOnlySpan<char> * ?style: NumberStyles * ?provider: IFormatProvider -> int + 4 overloads
static member TryParse: s: ReadOnlySpan<char> * style: NumberStyles * provider: IFormatProvider * result: byref<int> -> bool + 3 overloads
static val MaxValue: int
static val MinValue: int
<summary>Represents a 32-bit signed integer.</summary>
Int32.TryParse(s: string, result: byref<int>) : bool
Int32.TryParse(s: ReadOnlySpan<char>, result: byref<int>) : bool
Int32.TryParse(s: string, style: Globalization.NumberStyles, provider: IFormatProvider, result: byref<int>) : bool
Int32.TryParse(s: ReadOnlySpan<char>, style: Globalization.NumberStyles, provider: IFormatProvider, result: byref<int>) : bool
val i2: bool * int
val from': l: (Internals.Exchange<('a -> 'a),('b -> Identity<'b>)> -> Internals.Exchange<('c -> 'd),('e -> Identity<'f>)>) -> ('g -> 'h) (requires member Dimap and member Map)
val i3: int option
val fv3: int option
val maximumOf: l: (('a -> Const<Dual<Endo<'a option>>,'b>) -> 'c -> Const<Dual<Endo<'d option>>,'e>) -> ('c -> 'd option) (requires comparison)
<summary>
Get the largest target of a Fold.
</summary>
val fv4: int option
val minimumOf: l: (('a -> Const<Dual<Endo<'a option>>,'b>) -> 'c -> Const<Dual<Endo<'d option>>,'e>) -> ('c -> 'd option) (requires comparison)
<summary>
Get the smallest target of a Fold.
</summary>