Skip to content

rojepp/reflenses

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages