Proposal: record setters

Currently Elm has a feature where .name is equivalent to writing (\r -> r.name) This is useful for getting fields, but there is no similar feature for setting them. I think it should be done by defining .name.get and .name.set

So instead of .name having this type:

{ b | name : a } -> a

It would instead have this type:

{ get : { b | name : a } -> a
, set : a -> { b | name : a } -> { b | name : a }
}

I proposed something similar almost a year ago, but it doesn’t appear there is any enthusiasm.
https://discourse.elm-lang.org/t/a-record-update-function-operator/4083/2

It raised the point that the type of the set is unclear what would be optimal.

In the Zipper library there is this example:

updateCtoBonus : Float -> Company -> Company
updateCtoBonus f c =
    c
        |> zip
        |> into .cto (\cto company -> { company | cto = cto })
        |> intoMaybe .pay (\pay cto -> { cto | pay = pay })
        |> andThenInto .bonus (\bonus pay -> { pay | bonus = bonus })
        |> mapMaybe (\bonus -> bonus * f)
        |> unzip

If my idea was implemented it could be simplified into this:

updateCtoBonus : Float -> Company -> Company
updateCtoBonus f c =
    c
        |> zip
        |> into .cto
        |> intoMaybe .pay
        |> andThenInto .bonus
        |> mapMaybe (\bonus -> bonus * f)
        |> unzip

Your example in the zipper library is a really good example for your proposal. However I’m not sure it’s all that common that an API asks for a getter and a setter. Of course if your proposal was implemented perhaps more APIs would likely accept getter/setter combinations. Still I think you’re generally doing one or the other, rather than both.

But I think you’re proposal now wreaks the common case for just using .name to mean the getter. Eg. List.map .age people. In your proposal you would have to introduce yet more syntax to allow for List.map .age.get people which wouldn’t currently be possible, without that additional syntax you would be forced to write List.map (\r -> r.age) people. Similarly for setting some field to a constant on a list of records.

The proposal of using #name or !name for a setter would change your example to be:

updateCtoBonus : Float -> Company -> Company
updateCtoBonus f c =
    c
        |> zip
        |> into .cto !cto
        |> intoMaybe .pay !pay
        |> andThenInto .bonus !bonus
        |> mapMaybe (\bonus -> bonus * f)
        |> unzip

Not quite as nice as yours but not far from it, and it wouldn’t be a breaking change. It would also be more appropriate for what (I think) is the common case of using either the getter or the setter but not both: List.map .age people compared to List.map (\r -> r.age) people

Meybe there should be .field for getting, !field for setting, and #field for a record with get and set

1 Like

The oldest discussion I could find of this idea is here from 2013.

I still have the same reservations about syntactic simplicity and performance. Adding more syntax means more to learn and get stuck on, whereas the current design encourages the use of named top-level functions instead.

I personally think all the lens stuff in Haskell was a net negative for clarity in the language overall, and I wouldn’t want to use that as a model for additions to Elm. So while we could save some characters with new features here, I think the alternate designs introduce downsides that are not worth the trade.

13 Likes

I’ve got the feeling that I use getters AS MUCH as setters. I never understood why the one has a pipeline friendly syntax while the other has not.

I’m wondering, if adding pipeline setters isn’t an option (because more syntax means more to learn), why do we still have pipeline getters? Can’t the same arguments be applied here as well?

2 Likes

Dot notation to access properties of an object is already something many programs are comfortable with:

person.age

The majority of programmers will immediately know what that means and I’d hazard a guess and say even those new to programming might be able to work it out.

.age person

This then, is an extension of a syntax that is deeply familiar to a large number of people. It ends up being a “oh thats handy” thing.

There is no common “setter” syntax for object properties across languages so any operator we add is one that comes at a greater cognitive cost.

2 Likes

That’s a great answer to the question of why the one has pipeline friendly syntax whilst the other does not.

Still I feel that record update, and in particular nested record update, is a touch awkward. Sometimes you have to introduce a name (and possibly a whole let-in block) just to update a field in a record that is either within an existing record, or the result of an expression such as a function call. Eg.

setDarkMode : Model -> Model
setDarkMode model =
        let
              settings =
                     model.settings
        in
        { model | settings = { settings | darkMode = True } }

This is because I cannot do { model.settings | darkMode = True }, but would be solved by a pipeline friendly operator for record update.
Of course in this simplistic setting I could get around it by doing setDarkModel ({settings} as model) = however that doesn’t work if you first manipulate model in someway.

As pointed out, most of this is solved by writing top-level definitions. However, these feel very noisy and are prone to end up being auto-generated by some IDE, meaning we have more code to maintain. I feel routinely auto-generated stuff is a language-smell. Though that argument falls a bit flat given that I don’t know of any Elm IDE that does this auto-generation.

Anyway I feel it’s a trade-off between syntactic simplicity and noise. It’s not about saving characters, but increasing the signal-to-noise ratio. A bunch of top-level definitions to me feels like a bunch of code that doesn’t achieve anything and feels like fighting the language.

Note this possibly feels as if I’m arguing for some pipeline friendly operator for record update, I’m not. In my previous post the conclusion specifically stated that I wasn’t proposing this I just thought it an interesting discussion. In particular I think the hard part would be figuring out whether such a pipeline operator would have the type:
a -> { b | field : a } -> { b | field : a }
or
{b | field : a } -> a -> { b | field : a}
ie. do you take the new value or the existing record first? In that previous post I pointed out that there are arguments for either.

I share the feeling that “all the lens stuff in Haskell was a net negative for clarity”, in particular I find some Haskell programmers go a bit bonkers over “pointless-style”, where eliminating variables rather than readability seems to be the goal. It’s easy to get stuck in ‘simple heuristics’ such as “eliminate variables = good”. That’s a nice easy goal to go towards and know when you’re done, rather than “the most readable definition” which is much harder to go towards and to recognise once you’ve achieved it. But ultimately of course a much better goal. (Also if I’m honest pointless style is often a fun exercise to produce, even if it doesn’t always end up with a readable result).

I agree about operators like %= or ^., but I disagree with the general assessment.

I want to point out that the lens library gives huge benefits when working with ADTs apart from basic getters and setters. I made heavy use of that in elm-reduce while exlusively using the (.) operator, which is just the good old function composition. The other operators can be replaced by functions with namens like “view” and “over” or just normal function application.

However, the benefits can’t be used in elm, as that would need higher kinded types to implement (and goes beyond this proposal), so I guess this is kind of off topic.

1 Like

Why not? Nothing prevents you from giving an updated model to the function or updating more than one model field in the model update.

Another pattern that I’ve seen around is this one:

updateSettings : (Settings -> Settings) -> { a | settings : Settings } -> { a | settings : Settings }
updateSettings updater record =
    { record | settings = updater record.settings }


setDarkMode : Model -> Model
setDarkMode model =
    updateSettings (\s -> { s | darkMode = True }) model

So the main complaint about nested record update falls kind of flat when you take a step back and consider the architectural decisions that led you needing to do a nested record update.

If settings is “important” enough (read; a stand-alone domain we can model) to be a record itself then we shouldn’t be updating it in the model’s update function.

{ model.settings | darkMode = True }

We can’t do this but we can just as well do:

{ model | settings = Settings.toggleDarkMode model.settings }

Which moves the responsibility into a module that is concerned about the correct data type.

6 Likes

Why not? Nothing prevents you from giving an updated model to the function or updating more than one model field in the model update.

Do you mean I could do:

setDarkMode : Model -> Model
setDarkMode model =
        let
              ({ settings} as newModel) =
                     someModelUpdateFun model
        in
        { newModel | settings = { settings | darkMode = True } }

Does that work in let settings? I don’t know I confess I hadn’t even thought of it.

setDarkMode : Model -> Model
setDarkMode ({ settings } as model) =
    { model | settings = { settings | darkMode = True } }

and use someModelUpdateFun model |> setDarkMode

4 Likes

That’s also a good answer.
When I first started in Elm I had a lot of nested records and at some point, probably after watching Richard Feldman’s talk which relates extensible records to function range as opposed to inheritance, I started having much flatter records and using extensible record types in function signatures. I’ve found there are a couple of trade-offs, but nevertheless nested records is often some kind of unnecessary structure.

Anyway I understand you didn’t propose using extensible records (though I think you were hinting that that would be the other possibility if ‘settings’ wasn’t important enough …).

Still, what you’re proposing is basically a more structured way of writing the top level definitions. Because you still need to write the Settings.toggleDarkMode. So I think still you end up with a lot of noisy code that doesn’t achieve anything and it still feels like you’re fighting the language a bit.

If the proposition that you shouldn’t use nested records without having an “important enough” stand alone domain, then shouldn’t we just go ahead and ban nested records? If you need to have a separate module, then perhaps you should be forced to put that nested record behind a constructor for that separate domain. Otherwise any consumer wouldn’t be forced to go through your module interface for updating/accessing the record.

I don’t know if others can articulate better on nested records, but I personally feel that nested records have their place, though I cannot quite articulate why.

1 Like

Ah I see, thanks.
I remember a long time ago I was surprised by the restriction on record update that the record that you’re updating can be a single variable name and that’s it. I submitted a PR to the compiler that allowed for an expression in that place, so you could have { <expr> | blah = blah}. I cannot remember the exact reasons but the PR was rejected with the rough reason of “additional syntax complexity with no clearly demonstrated benefit”. I though it would be pretty easy to find a bunch of code that would be obviously improved with this addition, so I spent an hour or two going through the elm libraries trying to find code that would be obviously improved, and I found that task near impossible.

That experience was humbling, I remember submitting the PR thinking it was a no-brainer, being surprised it was rejected and then being further surprised that I could even articulate a good argument for its inclusion let alone a convincing one.

5 Likes

Elm has an optics implementation in Monocle. The Lens is similar to this proposal but even more generic (e.g. set : b -> a -> a). Beyond just records, with optics you get have “getters/setters” for all sort of structures and they compose nicely. Others have been pointing this out as well. I’ve used it all over my code base despite it being boilerplate-y to write the optics since they’re not derived; however, using them is a much more straightforward API for setting values – and I’d argue it’s easier to follow the code as the heavily-nested stuff isn’t readable and easy to make mistakes in pulling apart and rebuilding records (especially when nested).

3 Likes

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