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:

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
namespace System
namespace FSharpPlus
module Lens

from FSharpPlus
type Person =
  { Name: string
    DateOfBirth: DateTime }
Person.Name: string
Multiple items
val string : value:'T -> string

--------------------
type string = String
Person.DateOfBirth: DateTime
Multiple items
type DateTime =
  struct
    new : ticks:int64 -> DateTime + 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
    ...
  end

--------------------
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

--------------------
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
    interface IReadOnlyList<'T>
    interface IReadOnlyCollection<'T>
    interface IEnumerable
    interface IEnumerable<'T>
    member GetSlice : startIndex:int option * endIndex:int option -> 'T list
    member Head : 'T
    member IsEmpty : bool
    member Item : index:int -> 'T with get
    member Length : int
    member Tail : 'T list
    ...
val _item : i:int -> f:('f option -> 'g) -> t:'f list -> 'h (requires member Map)
val _Some : x:('f -> 'g) -> ('f option -> 'i) (requires member Map and member Return)
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
val authorName2 : string
val book1 : Book
val setl : optic:(('a -> Data.Identity<'b>) -> 's -> Data.Identity<'t>) -> value:'b -> source:'s -> 't
val book2 : Book
val over : optic:(('a -> Data.Identity<'b>) -> 's -> Data.Identity<'t>) -> updater:('a -> 'b) -> source:'s -> 't
Multiple items
type String =
  new : value:char[] -> string + 8 overloads
  member Chars : int -> char
  member Clone : unit -> obj
  member CompareTo : value:obj -> int + 1 overload
  member Contains : value:string -> bool + 3 overloads
  member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit
  member EndsWith : value:string -> bool + 3 overloads
  member EnumerateRunes : unit -> StringRuneEnumerator
  member Equals : obj:obj -> bool + 2 overloads
  member GetEnumerator : unit -> CharEnumerator
  ...

--------------------
String(value: char []) : String
String(value: nativeptr<char>) : String
String(value: nativeptr<sbyte>) : String
String(value: ReadOnlySpan<char>) : String
String(c: char, count: int) : String
String(value: char [], startIndex: int, length: int) : String
String(value: nativeptr<char>, 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
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
val _score : f:(int -> 'a) -> p:Player -> 'b (requires member Map)
val s : int
Multiple items
module Result

from FSharpPlus

--------------------
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

--------------------
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 }
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>
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 get_Item1)
val match2 : Match<Player * Player>
val _2 : f:('a -> 'b) -> t:'f -> 'e (requires member Map and member MapItem2 and member get_Item2)
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
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 []
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)
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
val both : f:('a -> 'b) -> a:'a * b:'a -> 'f (requires member Map and member ( <*> ))
val length : source:'Foldable<'T> -> int (requires member Length)
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)
Multiple items
union case Mult.Mult: 'a -> Mult<'a>

--------------------
[<Struct>]
type Mult<'a> =
  | Mult of 'a
    static member get_Zero : unit -> Mult<'a1> (requires member One)
    static member ( + ) : Mult<'n> * Mult<'n> -> Mult<'a2> (requires member ( * ))
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)
val i1 : int option
val view : optic:(('a -> Const<'a,'b>) -> 's -> Const<'a,'t>) -> source:'s -> 'a
type Int32 =
  struct
    member CompareTo : value:obj -> int + 1 overload
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member GetTypeCode : unit -> TypeCode
    member ToString : unit -> string + 3 overloads
    member TryFormat : destination:Span<char> * charsWritten:int * ?format:ReadOnlySpan<char> * ?provider:IFormatProvider -> bool
    static val MaxValue : int
    static val MinValue : int
    static member Parse : s:string -> int + 4 overloads
    static member TryParse : s:string * result:int -> bool + 3 overloads
  end
Int32.TryParse(s: ReadOnlySpan<char>, result: byref<int>) : bool
Int32.TryParse(s: string, result: byref<int>) : bool
Int32.TryParse(s: ReadOnlySpan<char>, style: Globalization.NumberStyles, provider: IFormatProvider, result: byref<int>) : bool
Int32.TryParse(s: string, style: Globalization.NumberStyles, provider: IFormatProvider, result: byref<int>) : bool
val i2 : bool * int
val from' : l:(Internals.Exchange<'a,'b,'a,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)
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)