FSharpPlus


Lens

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:

#r @"nuget: FSharpPlus"
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 <&>.

Prism

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

Traversal

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"

Fold

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

Iso

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

Maximum and minimum

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

Recommended reading

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 + 14 overloads member Add: value: TimeSpan -> DateTime member AddDays: value: float -> DateTime member AddHours: value: float -> DateTime member AddMicroseconds: 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 ...
<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&lt;value&gt; to an Option&lt;value&gt;. 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&lt;'T&gt; </summary>
val rayuela: Book
Multiple items
module Book from Lens

--------------------
type Book = { Title: string Author: Person Pages: Page list }
val authorName1: string
val view: optic: (('a -> Data.Const<'a,'b>) -> 's -> Data.Const<'a,'t>) -> source: 's -> 'a
<summary>Read from a lens.</summary>
<param name="optic">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: optic: (('a -> Data.Identity<'b>) -> 's -> Data.Identity<'t>) -> value: 'b -> source: 's -> 't
<summary>Write to a lens.</summary>
<param name="optic">The lens.</param>
<param name="value">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: optic: (('a -> Data.Identity<'b>) -> 's -> Data.Identity<'t>) -> updater: ('a -> 'b) -> source: 's -> 't
<summary>Update a value in a lens.</summary>
<param name="optic">The lens.</param>
<param name="updater">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&lt;'T,'Error&gt; </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: optic: (('a -> Data.Const<Data.First<'a>,'b>) -> 's -> Data.Const<Data.First<'a>,'t>) -> source: 's -> 'a 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="optic">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: optic: (('a -> Identity<'b>) -> 's -> Identity<'t>) -> updater: ('a -> 'b) -> source: 's -> 't
<summary>Update a value in a lens.</summary>
<param name="optic">The lens.</param>
<param name="updater">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: optic: (('a -> Const<'a,'b>) -> 's -> Const<'a,'t>) -> source: 's -> 'a
<summary>Read from a lens.</summary>
<param name="optic">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 Abs: value: int -> int static member Clamp: value: int * min: int * max: int -> int static member CopySign: value: int * sign: int -> int static member CreateChecked<'TOther (requires 'TOther :> INumberBase<'TOther>)> : value: 'TOther -> 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, provider: IFormatProvider, result: byref<int>) : bool
Int32.TryParse(s: ReadOnlySpan<char>, provider: IFormatProvider, 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 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>