Alan Wayne
Alan Wayne

Reputation: 5394

In F#, how to change a property of a property within a record (Syntax?)

Lets assume I have a record like:

 type Page = { footersize: Size }

what is the correct syntax to change only the Height of the footersize:

 { page with footersize = ??? }

TIA

Edit#1: "Size" is the size of a FrameworkElement, i.e., Size is in the .Net world as Size(width,height) and is a structure, not record. As tried below, this does NOT work:

{p with footersize = {p.footersize with Height = 96 * 0.5}}   

Error: This expression was expected to have type 'Size' but here as type Page.

Upvotes: 1

Views: 531

Answers (1)

Asti
Asti

Reputation: 12687

The most direct way is to copy-update the whole object:

let page' =
    { page with
        FooterSize = Size(page.FooterSize.Width, 100.)
    }

If you have some deeper nesting, it gets a bit harder.

type Page = { FooterSize: Size }
type Document = { Title: string; Page: Page }

let document = { Title = "Fun and Profit"; Page = { FooterSize= Size(10., 10.) } }

To update height, now you have to do:

let document' = { document  with Page = { document.Page with FooterSize= Size(document.Page.FooterSize.Width, 100.) }}

And that goes off screen! There's a language suggestion for nested record assignment, which would allow you to do { document with Page.FooterSize.Height = 100. } , but that's still a little ways off.

Lenses

type Lens<'a,'b> = ('a -> 'b) * ('a -> 'b -> 'a)

Don't worry if this seems confusing! It will become much clearer in time.

Lenses are the functional equivalent to C#'s property set technology™ (basically page.FooterSize.Height = 100. in C#).

They're quite simple, just a pair of getter, setter functions.

let getFooterSize (p: Page) = p.FooterSize
let setFooterSize (p: Page) size = { p with FooterSize = size }

let getPage (d: Document) = d.Page
let setPage (d: Document) page = { d with Page = page }

let getWH (s: Size) = s.Width, s.Height
let setWH (s: Size) (w, h) = Size(w, h)

Of course, we can get values by simply using the compose right operator:

let (w, h) = (getPage >> getFooterSize >> getWH) document

But setting doesn't compose. But with just three simple operators, we can have something very readable:

let get (getter, _) = getter
let set (_, setter) = setter

let (>=>) (get_ab, set_ab) (get_bc, set_bc) =
  get_ab >> get_bc,
  fun a c -> set_ab a (set_bc (get_ab a) c)

Since our lenses are just pair of getters and setters:

let pageL = getPage, setPage
let footerL = getFooterSize, setFooterSize
let sizeL = getWH, setWH

That's it. Those are our lenses. We can now compose these lenses with the fish operator (>=>) we had defined.

let (footer_w, footer_h) = get (pageL >=> footerL >=> sizeL) document
let document' = set (pageL >=> footerL >=> sizeL) document (footer_w, 100.)

Of course, you can write a lens in a much shorter form:

let pageL : Lens<Document, Page> = 
    (fun d -> d.Page), (fun d p -> { d with Page = p })

Upvotes: 4

Related Questions