kolektiv

Andrew Cherry

Aether — Total & Partial Lenses for F#

NOTE: This article has been superceded by later changes to the Aether library in late 2015. See the Aether site for up to date information.

NOTE: This article was updated in June 2015 to indicate the use of the final syntax in Aether.

I spent a little time recently looking deeper at lenses in F# (I use them quite commonly). There’s an implementation available as part of FSharpx, but I decided to see whether it might be nicer to have a single purpose lens library, with a few extra features as a goal.

Firstly, the ability for other libraries to provide over their own types for external consumers, without requiring a dependency on the lens library (the FSharpx implementation requires lenses to be a specific record type, meaning a dependency on FSharpx). Secondly, a clear distinction between total and partial lenses (lenses which definitely return a value from the getter, and lenses which may return a value from the getter — in this case an F# option).

I also took a chance to look around at other lens libs, and settled on a naming pattern and convention taken from the data-lens library in Haskell.

I’ll be doing a full introduction shortly, but for now, the library is called Aether and it’s over on my Xyncro GitHub org here, along with installation instructions. It’s MIT licensed.

In the meantime, here’s an illustrative snippet…

type A =
    { One: B 
      Two: B option }

    (* A total lens to One *)
    static member oneLens =
        (fun x -> x.One), (fun o x -> { x with A.One = o })

    (* A partial lens to Two *)
    static member twoPLens =
        (fun x -> x.Two), (fun t x -> { x with A.Two = Some t })

and B =
    { One: string
      Two: string option }

    static member oneLens =
        (fun x -> x.One), (fun o x -> { x with B.One = o })

    static member twoPLens =
        (fun x -> x.Two), (fun t x -> { x with B.Two = Some t })

(* A total lens to the value of "One" in the nested B *)
let testLens = A.oneLens >--> B.oneLens

(* A partial lens to the value of "One" in the nested B option *)
let testPLens = A.twoPLens >?-> B.oneLens

(* A function which will return the value of One in the nested B from a,
   an instance of A (a string)
   (could also be written "testLens ^. a") *)
let getOne a = Lens.get testLens a

(* A function which will return the value of One in the nested B option from a,
   an instance of A (a string option)
   (could also be written "testLens ^?. a") *)
let getOneMaybe a = Lens.getPartial testPLens a

(* A function will set the value of One within an instance of A to x (a string)
   (could also be written "(testLens ^?= x) a") *)
let setOne x a = Lens.setPartial testPLens x a