Skip to content

Instantly share code, notes, and snippets.

@marick
Last active December 2, 2016 19:35
Show Gist options
  • Select an option

  • Save marick/db960fd4b6ff38b7747006522d7eaec5 to your computer and use it in GitHub Desktop.

Select an option

Save marick/db960fd4b6ff38b7747006522d7eaec5 to your computer and use it in GitHub Desktop.

Background

I am trying to extend http://package.elm-lang.org/packages/arturopala/elm-monocle/latest. It provides "methods" that can be used to get and set fields within records. Like this:

  lensFromWholeToPart.get {part = 9}
  => 9
  lensFromWholeToPart.set 333 {part = 9}
  => {part = 333}

In addition to the Lens type, Monocle also has Optional, which handles Maybe. The differences don't matter for this example, except that Optional's version of get is named getOption.

You can compose lenses to get new lenses that get and set deep within a data structure. You can also compose Optionals with Lenses, yielding new Optionals.

For concreteness, suppose I have a animal_editableCopy Optional that goes from an Animal to a field. I also have an editableCopy_editableName Lens that goes from that field to one of its fields. Composing them with Monocle looks like this:

   animal_editedName = Optional.composeLens animal_editableCopy editableCopy_editableName

More than just lenses

However, lenses don't have update or transform functions like this:

  lensFromWholeToPart.update sqrt {part = 9}
  => {part = 3}

That gets awkward when there are Maybe entries in the record. There's probably no generally Right Thing to do when you update along a path that has a Nothing in it. For my application, though, there is a right thing to do: any Nothing anywhere means that update doesn't change the whole. So I've defined my own UpdatingLens and UpdatingOptional:

type alias UpdatingLens whole part =
  { get : whole -> part
  , set : part -> whole -> whole
  , update : (part -> part) -> whole -> whole
  }

It's easy and automatic to convert the get and set functions to an UpdatingLens:

lens getPart setPart =
  { get = getPart
  , set = setPart
  , update = lensUpdate getPart setPart
  }

lensUpdate getPart setPart partTransformer whole =
  setPart (whole |> getPart |> partTransformer) whole

The same is true for converting Monocle's Optional to my UpdatingOptional. The function that does that is called opt, and I'll use it below.

The problem at hand, then, is to write a composeLens that works with my types:

composeLens : UpdatingOptional whole part -> UpdatingLens part subpart -> UpdatingOptional whole subpart

What doesn't work

It would be cool if my composeLens could be written like this:

composeLens left right = 
  let
    composed = Optional.composeLens left right
  in
    opt composed.getOption composed.set

animal_editedName = composeLens animal_editableCopy editableCopy_editableName

That doesn't work, though:

The 2nd argument to function `composeLens` is causing a mismatch.

46|                     composeLens animal_editableCopy editableCopy_editableName
                                                        ^^^^^^^^^^^^^^^^^^^^^^^^^
Function `composeLens` is expecting the 2nd argument to be:

    { ... }

But it is:

    { ..., update : ... }

What does work

Instead, it seems I have to explicitly "downcast" my types to Monocle's, compose those, and then add on to the result:

composeLens left right = 
  let
    left_ = extractOptional left
    right_ = extractLens right
    composed = Optional.composeLens left_ right_
  in
    opt composed.getOption composed.set


extractLens : UpdatingLens whole part -> Lens whole part
extractLens u =
  { get = u.get
  , set = u.set
  }

But... structural typing?

I believe the issue is that Monocle defines its functions like this:

composeLens : Optional a b -> Lens b c -> Optional a c

For the type checking to work, it'd have to define them as something like

composeLens : {left | getOption : ..., set : ...} -> { right | ... } -> ...

I think. Not sure. But it looks as if, to use OO lingo, Optional is closed for both modification and extension.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment