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 }
}
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
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.
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?
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.
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.
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.
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.
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.
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.
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).