Skip to content

Latest commit

 

History

History
83 lines (66 loc) · 3.03 KB

readme.md

File metadata and controls

83 lines (66 loc) · 3.03 KB

Reflenses

Experimental lenses using reflection to avoid boilerplate.

The syntax for making a copy of F# immutable records is a bit verbose when you have nested records. This code is an attempt to change that. Inspired by Mauricio Scheffers post.

type Pet = { Animal: string; Name: string }
type Person = { FirstName: string; LastName: string; FavoriteColor: string; Pet: Pet option }
let original = { FirstName = "Robert" 
                 LastName = "Jeppesen" 
                 FavoriteColor = "Blue"
                 Pet = Some { Animal = "Dog"; Name = "Raggedy" } }
// Flat hierarchy, not bad at all.
let newversion = { original with FavoriteColor = "Yellow" }
// Nested copy, more cumbersome, and how does it 
// really work with option types?
// You have to replace all of it. 
// Better pray that newversion.Pet was a 'Some'!
let newversion2 = { newversion with Pet = Some { newversion.Pet with Name = "ABC" } }

With Reflenses, you can create a new record by doing this:

// The simple case is not a win, more complex than 
// the built-in method. 
let newversion = set original <@ (fun p -> p.FavoriteColor) @> "Yellow"
// Replace all of the pet
let newversion2 = set newversion <@ (fun p -> p.Pet) @> (Some { Animal = "Monkey"; Name = "Spot" })
// Just replace the pet name
let renamedpet = set newversion <@ (fun p -> p.Pet.Value.Name) @> "Spot (still a dog)"

All of these are type-checked, thanks to the power of F# quotations. I'm not quite happy with the syntax for setting partial options, at the moment. Share your ideas!

You can also set several options at once. This is done as a tuple in the quotation and input value:

let newversion = set original <@ (fun p -> p.FavoriteColor, p.Pet.Value.Animal) @> "Black","Elephant"

This is also fully type checked.

Performance

This is way slower than the built-in way of creating new records. Before optimizing, a test run of 10.000 records would take ~12 seconds on my 4 year old laptop. After optmizing the most obvious bits, it's down to ~3 seconds, where the native F# version is ~20 ms. If you hoist the expression out of the loop, you get a massive speedup. This quotation literal doesn't seem to get cached by F#, even when it does not capture any locals.

let expr = <@ (fun f -> f.Car.Make.Make) @>
  let inline time f iterations = 
    let sw = System.Diagnostics.Stopwatch()
    sw.Start()
    for i in 1 .. iterations do
       f () |> ignore
    sw.Stop()
    printfn "%A" sw.ElapsedMilliseconds
 
time (fun () -> set robert expr "Volvo")  10000 // Expression out of loop, fast
time (fun () -> set robert <@ (fun f -> f.Car.Make.Make) @> "Volvo" ) 10000// Slow
time (fun () -> { robert with Car = { robert.Car with Make = { robert.Car.Make with Make = "Volvo" } } } ) 10000 // Fastest
     
104 ms
2314 ms
20 ms

Code is in F# with no other dependencies for runtime. Tested by XUnit.