Fractal `update` functions thanks to lenses

Hi guys,

Quick disclaimer

I am very new to Elm and while reading An introduction to Elm something bugged me. The fact that only views are shown as composable (cf. https://guide.elm-lang.org/reuse).

I tried to find an elegant solution to let sub components/modules/functional slices of the app/… update their own model without having to know about the top level app model.

This is a very humble proposal and it’s probably missing a lot of cases (for example Command are not handled in my current proposal). I am looking for feedback on this idea. There might be a way that I missed to do that already, so I am up for any feedback :slight_smile:

It’s highly inspired of the Cycle.js way to do that (with cycle-onionify and lenses https://github.com/staltz/cycle-onionify).

In the rest of the issue I’ll use the term component to talk about a functional slice of an app (something that knows how to render, that possesses the business logic to update its own state and that returns some effects (the former is not part of this proposal)).

My problem: Components cannot define their own update function

So far, what I see is that there can only be a single signature for any update function of the app update : AppMessage -> AppModel -> AppModel.
Whether you are building an update function for the root component or for a nested leaf component, the function will always take a full AppModel.
The problem is that leaf component then need to know about the structure of AppModel and cannot be reused in another context (only their view can be reused).

Concrete example: the checkbox example

Let’s work on a concrete example to illustrate my proposal. The checkbox example is a good one I think http://elm-lang.org/examples/checkboxes

The model looks like this

type alias Model =
  { notifications : Bool
  , autoplay : Bool
  , location : Bool
  }

The views are factorized with the checkbox function

The update method is defined by the parent that handles all the messages from the checkbox views.

Lenses to the rescue

The idea is to be able to select a specific slice of the main model and let the component work on that slice independently. This way the component can work without knowing anything about the app structure.

In order to achieve that, I came up with an isolate function that will feed the component the right slice of the model and when an update occurs, update the parent model with the updated model of the component.

type alias Getter parentModel childModel =
    parentModel -> childModel

type alias Setter parentModel childModel =
    parentModel -> childModel -> parentModel

type alias Update message model =
    message -> model -> model

isolate : Getter parentModel childModel -> Setter parentModel childModel -> Update message childModel -> Update message parentModel
isolate getter setter update message model =
    setter model (update message (getter model))

Final result

Here is the current update function of the given example

update : Msg -> Model -> Model
update msg model =
  case msg of
    ToggleNotifications ->
      { model | notifications = not model.notifications }

    ToggleAutoplay ->
      { model | autoplay = not model.autoplay }

    ToggleLocation ->
      { model | location = not model.location }

Here is the isolated version

-- checkbox.elm
checkboxUpdate: Msg -> Bool -> Bool
checkboxUpdate msg model = not model

-- main.elm
notificationUpdate = isolate (model -> model.notification) (model -> notification -> {model | notification = notification}) checkbodUpdate (\_ -> model -> model)
-- same for autoplay and location

update: Msg -> Model -> Model
update msg model = notificationUpdate msg >> autoplayUpdate msg >> locationUpdate msg

This enable easy reuse of components at different level of the application without having to write a reducer every single time.

Limitations

This proposal is not yet a viable option for, as far as I know, two reasons:

  • Commands are not handles (I might have a idea for that)
  • messages are not isolated, meaning that all the checkbox will react to a click on a single one of them (I have no idea yet how to fix that).
  • … probably some other limitations that I cannot see due to my limited knowledge of Elm.

What I am looking for?

Pretty much anything from “this is a super bad idea because of this, this and that” or “this is not the elm philosophy at all” to “Good idea, I might have some idea to fix the limitations” or “Well, is already possible with this solution”.

Working on this was super interesting anyway and I really had a great time working on this, so even if this is a bad idea, my time was not lost at all :wink:

BTW: really :heart: the language!!

4 Likes

Haskell is the language that committed to this idea most strongly, instigating a bunch of discussion in other communities. ClojureScript explored it under the name “cursors” a bit afterwards. Since then, both of these communities cooled off on the idea after seeing it in practice. I know folks in Haskell who do not allow lenses as a transitive dependency even.

At the peak excitement around this idea in 2014, I wrote https://github.com/evancz/focus to explore the idea for myself. I did not think it was going to be good in the end, and I think that’s what played out in the communities that explored it at the ecosystem level. The README explains the major problems I was worried about, primarily about revealing structure that should be hidden by nicer use of modules.

My opinion is that the code you define as a problem is actually really simple. We could make things more complex to save characters. Making simple code more complex is fine, but making complex code more complex is a pretty big downside. More generally, I do not think all code should be at the absolute limit of comprehension. Some code can just be easy. I don’t think that is a bad thing.

I suggest trying Elm out more and watching some talks to get a feeling for things a bit more.

5 Likes

Hi Evan, Thanks for this clear reply. This is a good point indeed and I might have rushed into a solution too quickly. As I said, this was super interesting to work on this, so if it ends up nowhere, that no problem :slight_smile:

Anyway, the frustration I had, I believe, came from the fact that the guide only introduce how to split views into reusable pieces of code.
At that point in reading the guide I felt like I was missing something. I felt the guide told me to write a huge update function in the main.elm that handles all the messages of all the view components.

To that day, I still don’t really know how to define smaller update functions that work on chunks of the app and that I can compose together.

I would be super nice, in my opinion, to have a chapter on this part in the guide? What do you think?

1 Like

I always thought Richard’s 2017 Elm Europe talk was illuminating on the subject of breaking down your update function, its a good one:

1 Like

Oh and my point was not to save character but more to group together view and behavior so that a component of an app can be read in a single file and reused at many different places

Thanks @rupert added to my “to watch list” :slight_smile:

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.