Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upProposal: Field update/map syntax #984
Comments
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Apanatshka
Jul 9, 2015
Contributor
I think that in general we may want to consider dropping the heavy-weight record add/remove/update syntax. Just use .get, !set (this proposal), +add, -remove:
I don't think I'd support an ad-hoc addition of syntax like this without a discussion about why we're not at least eliminating the other syntax for doing this at the same time. But I prefer a larger discussion about overhauling the syntax.
|
I think that in general we may want to consider dropping the heavy-weight record add/remove/update syntax. Just use I don't think I'd support an ad-hoc addition of syntax like this without a discussion about why we're not at least eliminating the other syntax for doing this at the same time. But I prefer a larger discussion about overhauling the syntax. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Apanatshka
Jul 9, 2015
Contributor
Another thing: I'd like to view this more as mapping than a setting syntax, so the types would be more general:
!x : (a -> b) -> { r | x : a } -> { r | x : b }|
Another thing: I'd like to view this more as mapping than a setting syntax, so the types would be more general: !x : (a -> b) -> { r | x : a } -> { r | x : b } |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
TheSeamau5
Jul 9, 2015
Great idea. That was a big oversight on my part.
I think there's no reason not having the function be more general. I've updated the proposal to reflect this.
TheSeamau5
commented
Jul 9, 2015
|
Great idea. That was a big oversight on my part. I think there's no reason not having the function be more general. I've updated the proposal to reflect this. |
TheSeamau5
changed the title from
Proposal: Field setter syntax
to
Proposal: Field update/map syntax
Jul 9, 2015
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
karldray
Jul 9, 2015
I think it makes more sense to have "set" rather than "update" as the primitive. You can derive "update" from "get" and "set", which seems more natural than deriving "set" from "update" and always.
karldray
commented
Jul 9, 2015
|
I think it makes more sense to have "set" rather than "update" as the primitive. You can derive "update" from "get" and "set", which seems more natural than deriving "set" from "update" and |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
TheSeamau5
Jul 9, 2015
@karldray my rationale, which could be erroneous, is that record updates happen mostly during the update phase of your program. This is mostly where I've needed syntax like this. And, more often than not, I don't really want to set a new raw value. I'd like to pass some update function, like in the Elm Architecture.
For example, you have a state
type alias State =
{ button : Button.State
, ...
}and in your update function you'd like to say
update action state =
case action of
ButtonAction buttonAction ->
!button (Button.update buttonAction) state
TheSeamau5
commented
Jul 9, 2015
|
@karldray my rationale, which could be erroneous, is that record updates happen mostly during the update phase of your program. This is mostly where I've needed syntax like this. And, more often than not, I don't really want to set a new raw value. I'd like to pass some update function, like in the Elm Architecture. For example, you have a state type alias State =
{ button : Button.State
, ...
}and in your update function you'd like to say update action state =
case action of
ButtonAction buttonAction ->
!button (Button.update buttonAction) state
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
You could add syntax for both |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 9, 2015
Contributor
I like where this is going. Here's a code snippet from a project of mine:
{model | r <- undo now model.r
, theta <- undo now model.theta
, state <- Shrinking}Which I think would look something more like
model |> !r (undo now) |> !theta (undo now) |> !state (always Shrinking)The only thing that bugs me a little is the always, but that's because record updates are not the same as record field setting. For example, {foo = 5, foo = 4} is perfectly valid. I know there's a semantics reason for this, and I haven't read the paper, but if there's no technical reason not to I'd like +field to be "add the field is it's not there, or update it if it is, ignoring the prior value".
|
I like where this is going. Here's a code snippet from a project of mine: {model | r <- undo now model.r
, theta <- undo now model.theta
, state <- Shrinking}Which I think would look something more like model |> !r (undo now) |> !theta (undo now) |> !state (always Shrinking)The only thing that bugs me a little is the |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rtfeldman
Jul 9, 2015
Member
Reading this over now, but @TheSeamau5 I think there's a typo in the second code snippet...should be this instead, yeah? (.x instead of .p in the second case.)
-- 1) direct dot syntax on the variable
p.x
-- 2) as a getter function
.x p|
Reading this over now, but @TheSeamau5 I think there's a typo in the second code snippet...should be this instead, yeah? ( -- 1) direct dot syntax on the variable
p.x
-- 2) as a getter function
.x p |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Apanatshka
Jul 9, 2015
Contributor
My suggestion would be to go all the way with this and remove the old syntax:
-- read
.x : { r | x : a } -> a
-- write
!x : b -> { r | x : a } -> { r | x : b } -- NOTE: different from this proposal
-- add
+x : a -> r -> { r | a }
-- remove
-x : { r | x : a } -> r
-- update (readwrite)
.!x : (a -> b) -> { r | x : a } -> { r | x : b }
r.!x f = r!x (f r.x)
-- Focus
:x : Focus { r | x:a } a
:x = Focus.create .x .!x|
My suggestion would be to go all the way with this and remove the old syntax: -- read
.x : { r | x : a } -> a
-- write
!x : b -> { r | x : a } -> { r | x : b } -- NOTE: different from this proposal
-- add
+x : a -> r -> { r | a }
-- remove
-x : { r | x : a } -> r
-- update (readwrite)
.!x : (a -> b) -> { r | x : a } -> { r | x : b }
r.!x f = r!x (f r.x)
-- Focus
:x : Focus { r | x:a } a
:x = Focus.create .x .!x |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
texastoland
Jul 9, 2015
- Do you have a real world example you can share where this would vastly improve readability?
- I'm against reconstituting math operators. That's not going to help anyone learn the language. It might even confuse me.
texastoland
commented
Jul 9, 2015
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rtfeldman
Jul 9, 2015
Member
In general, the idea of replacing record update syntax with something more akin to how getters currently work sounds really nice. It's more composable and I suspect it would be easier to teach newbies as well.
I looked through my code and found that in contrast to @TheSeamau5 I was always setting a raw value, so sticking with the current "update a raw value" approach would be preferable for my use cases.
One thing to note is that this would encourage using |> for multiple updates, which I am totally in favor of, because |> is awesome and I support its being used for more things. That said, person |> !name "Sam" has a lot of symbols right in a row...I think this operator would work better on the right side, e.g. person |> name! "Sam"
I also agree with @dnalot about not reusing math operators. In JS, !x and +x and -x already have meanings, and their meanings have nothing to do with what they would do in this proposal. The potential for confusion seems very high.
I played around with a bunch of alternatives and the one I really liked was : as a suffix. Consider how this code from Dreamwriter could be rewritten: https://github.com/rtfeldman/dreamwriter/blob/master/Component/LeftSidebar.elm#L51-L59
Before:
case update of
NoOp ->
model
SetViewMode mode ->
{ model | viewMode <- mode }
OpenDocId id ->
{ model |
currentDocId <- Just id,
viewMode <- CurrentDocMode
}After:
case update of
NoOp ->
model
SetViewMode mode ->
model |> viewMode: mode
OpenDocId id ->
model
|> currentDocId: (Just id)
|> viewMode: CurrentDocModeThis has the advantages of looking nice and having (I suspect) a very low learning curve, as what it does closely resembles what a suffixed : does in JavaScript (namely "I want this field to have the following value").
The disadvantage is that this is valid syntax for an object literal in CoffeeScript or a hash literal in Ruby, so there's still some potential for confusion.
One other thing to note is that this would presumably require a compiler optimization in order to achieve performance parity with current record updates. By default, I would assume the generated code for updating two fields is more efficient than the code for updating the one and then doing a separate update for the other, but I could be wrong.
I don't have any thoughts on the "add a field" and "remove a field" cases, as I have only ever had occasion to use "update a field."
|
In general, the idea of replacing record update syntax with something more akin to how getters currently work sounds really nice. It's more composable and I suspect it would be easier to teach newbies as well. I looked through my code and found that in contrast to @TheSeamau5 I was always setting a raw value, so sticking with the current "update a raw value" approach would be preferable for my use cases. One thing to note is that this would encourage using I also agree with @dnalot about not reusing math operators. In JS, I played around with a bunch of alternatives and the one I really liked was Before: case update of
NoOp ->
model
SetViewMode mode ->
{ model | viewMode <- mode }
OpenDocId id ->
{ model |
currentDocId <- Just id,
viewMode <- CurrentDocMode
}After: case update of
NoOp ->
model
SetViewMode mode ->
model |> viewMode: mode
OpenDocId id ->
model
|> currentDocId: (Just id)
|> viewMode: CurrentDocModeThis has the advantages of looking nice and having (I suspect) a very low learning curve, as what it does closely resembles what a suffixed The disadvantage is that this is valid syntax for an object literal in CoffeeScript or a hash literal in Ruby, so there's still some potential for confusion. One other thing to note is that this would presumably require a compiler optimization in order to achieve performance parity with current record updates. By default, I would assume the generated code for updating two fields is more efficient than the code for updating the one and then doing a separate update for the other, but I could be wrong. I don't have any thoughts on the "add a field" and "remove a field" cases, as I have only ever had occasion to use "update a field." |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 10, 2015
Contributor
The +x and -x operators don't just look like arithmetic, they are arithmetic. 2 +x == 2 + x and -x is unary negation. It may be possible to overload them but I don't think it's a good idea.
Also, I don't want to use : for anything other than has-type. Even if that means not placing it anywhere in the term language. What about field= which is similar to Ruby setters? And as for update... field.=?
So maybe something like this?
model |> r.= (undo now) |> theta.= (undo now) |> state= ShrinkingFor deletion, perhaps ~field or field~.
I also thought about the performance implications of the |> chain. Multiple updates are no longer atomic and an intermediate but inaccessible (directly) object is created. I don't think this is a deal breaker - we shouldn't let a small performance hit get in the way of a good idea - but I agree that optimizing |> chains would be nice, especially given that they're not limited to this operator. Alternatively, does function composition avoid the issue?
|
The Also, I don't want to use So maybe something like this? model |> r.= (undo now) |> theta.= (undo now) |> state= ShrinkingFor deletion, perhaps I also thought about the performance implications of the |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rtfeldman
Jul 10, 2015
Member
I believe function composition would still require a compiler optimization; each function would still get called, and would still perform only one update.
|
I believe function composition would still require a compiler optimization; each function would still get called, and would still perform only one update. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 10, 2015
Contributor
I started reading the paper, thought about it, and decided that I have a love-hate relationship with extension adding a duplicate field (a scope). On one hand, I don't want to throw away a type system feature, and every alternative I try to work out has problems. On the other, it's rarely used (records typically stay type-stable) and it's really not intuitive. Adding a copy of a field each iteration of foldp is not what I want.
With that in mind, try this:
-- read
.x : { r | x : a } -> a
-- update with constant
x= : b -> { r | x : a } -> { r | x : b }
-- update with function (readwrite)
x.= : (a -> b) -> { r | x : a } -> { r | x : b }
-- extend -- like update, but will force a duplicate, creating a scope
x=! : a -> r -> { r | a }
-- remove
x~ : { r | x : a } -> rThis retains all the current semantics, it just makes the unusual extend operator less visible by putting the bang on it. I like the = operator since it mimics the {x = 3} literal syntax.
|
I started reading the paper, thought about it, and decided that I have a love-hate relationship with extension adding a duplicate field (a scope). On one hand, I don't want to throw away a type system feature, and every alternative I try to work out has problems. On the other, it's rarely used (records typically stay type-stable) and it's really not intuitive. Adding a copy of a field each iteration of With that in mind, try this: -- read
.x : { r | x : a } -> a
-- update with constant
x= : b -> { r | x : a } -> { r | x : b }
-- update with function (readwrite)
x.= : (a -> b) -> { r | x : a } -> { r | x : b }
-- extend -- like update, but will force a duplicate, creating a scope
x=! : a -> r -> { r | a }
-- remove
x~ : { r | x : a } -> rThis retains all the current semantics, it just makes the unusual extend operator less visible by putting the bang on it. I like the |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
texastoland
Jul 10, 2015
Beginning to look compelling. Do you recommend the paper? Is there a quicker read? Concerns:
- Start a modified TodoMVC like @rtfeldman in #979.
=!looks incongruent to.=and potentially unintuitive.|=?- How much danger is there of conflating referential transparency using
=?
Very interesting @TheSeamau5!
texastoland
commented
Jul 10, 2015
|
Beginning to look compelling. Do you recommend the paper? Is there a quicker read? Concerns:
Very interesting @TheSeamau5! |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
dobesv
Jul 10, 2015
Contributor
I thought it would be great if you could pass .foo to some other function to convert it to a setter, updater, or deleter. Those functions would have to be a bit magic, though, and the type inference would be messy.
|
I thought it would be great if you could pass .foo to some other function to convert it to a setter, updater, or deleter. Those functions would have to be a bit magic, though, and the type inference would be messy. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Apanatshka
Jul 10, 2015
Contributor
Actually @dobesv, I was thinking the same, but by leveraging a Focus.
:x : Focus { r | x:a } a
The trouble is that Focus has no concept of add/remove, only get/set/update. Any ideas for solving that? Having only one syntactic construct, and a normal library otherwise would be so much better than adding lots of little syntax to replace other syntax.
|
Actually @dobesv, I was thinking the same, but by leveraging a
The trouble is that |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
texastoland
Jul 10, 2015
Focus has no concept of add/remove, only get/set/update.
Has anyone presented a use case for that or was it added for JS compatibility?
one syntactic construct, and a normal library otherwise would be so much better than adding lots of little syntax to replace other.
texastoland
commented
Jul 10, 2015
Has anyone presented a use case for that or was it added for JS compatibility?
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 10, 2015
Contributor
Like many papers, it gets more complicated as it goes on. I recommend reading up through the end of section two, which shouldn't be too hard for anyone on the thread. Just be aware that their notation for extension is backwards compared to Elm, with the existing record on the right of the pipe.
I'm worried that x|= will look like x||=, a Ruby construct that is commonly used for memoization. It looks too "safe" to me.
I'm not worried about referential transparency. That said, foo= 4 is currently an allowed assignment and I guess we'd have to disallow it (hopefully with a good compiler warning).
I'm skeptical that a library could do everything syntax could, and it wouldn't be available by default, but it's worth pursuing.
|
Like many papers, it gets more complicated as it goes on. I recommend reading up through the end of section two, which shouldn't be too hard for anyone on the thread. Just be aware that their notation for extension is backwards compared to Elm, with the existing record on the right of the pipe. I'm worried that I'm not worried about referential transparency. That said, I'm skeptical that a library could do everything syntax could, and it wouldn't be available by default, but it's worth pursuing. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
texastoland
Jul 10, 2015
x|= will look like x||=, a Ruby construct
It's used in JS like Ruby. It resembles our existing update syntax though.
I'm not worried about referential transparency.
I guess we'd have to disallow it
@Apanatshka Would you foresee any parsing ambiguities? @rtfeldman Language inconsistencies? It's still preferable to prefix which looks like operator application.
it wouldn't be available by default
It should be added to the default import.
texastoland
commented
Jul 10, 2015
It's used in JS like Ruby. It resembles our existing update syntax though.
@Apanatshka Would you foresee any parsing ambiguities? @rtfeldman Language inconsistencies? It's still preferable to prefix which looks like operator application.
It should be added to the default import. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rtfeldman
Jul 10, 2015
Member
I'm worried about foo= bar appearing to perform a reassignment...I've seen newbies read onClick address (Delete id) and struggle with the assumption that Delete was an imperative side-effecting function just based on the name. If you've had years of foo= bar performing a reassignment, I don't think that's going to be an easy shift.
|
I'm worried about |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
texastoland
Jul 10, 2015
I'm worried about foo= bar appearing to perform a reassignment
Strongly agree. I've seen backwards arrow <- but that's bad too. A single syntax plus library sounds much better.
texastoland
commented
Jul 10, 2015
Strongly agree. I've seen backwards arrow |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
dobesv
Jul 10, 2015
Contributor
If records were "callable" or you could extend a function with record
fields to have a callable record then you could put the other operations
into the .foo value as record fields. This does require some tricky type
system changes but it might work.
Having magic functions I think would also need a change to the tyoes,
because the magic functions must only be passed one of these getters.
Separate syntax at least leaves the type system untouched, but I find it
less elegant.
On Thu, Jul 9, 2015, 6:52 PM Texas Toland notifications@github.com wrote:
I'm worried about foo= bar appearing to perform a reassignment
Strongly agree. I've seen backwards arrow <- but that's bad too. A single
syntax plus library sounds much better.—
Reply to this email directly or view it on GitHub
#984 (comment)
.
|
If records were "callable" or you could extend a function with record Having magic functions I think would also need a change to the tyoes, Separate syntax at least leaves the type system untouched, but I find it On Thu, Jul 9, 2015, 6:52 PM Texas Toland notifications@github.com wrote:
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 10, 2015
Contributor
I'm not worried about referential transparency
Sorry, hair-brained incomplete thought. I'm not worried that foo= bar will impair the reasoning associated with referential transparency. However, given the concerns about parsing (by both human and machine), I think we can probably move on.
I think the crux of the matter is that extensible records are defined by their primitive operations - get, extend, and delete - but the commonly-used interface is get, update, and update-as-a-function-of-the-previous-value.
And you know what? That's exactly the interface a Focus provides. So I think I see where you're coming from. My concerns are (1) get, set, update, (=>), and create are all very generic names to dump into all programs by default, and (2) Evan seems very hesitant about the library's potential to harm abstraction boundaries and performance.
I think we can solve the first one by making Focus an exposed record type with get, set, and update. If @x creates a focus, then my example becomes
model |> @r.update (undo now) |> @theta.update (undo now) |> @state.set ShrinkingNot too bad. (I'm assuming .x would be shorthand for @x.get but we could try a variation.) Under this plan, I wouldn't import create unqualified and I'm undecided on =>. We also would keep the existing syntax for those who want to leverage scoping and such (and Focus would use them under the hood).
I'm using @x instead of :x because I don't like overloading colon from has-type. You're getting "at x" so I think @ works, and the Ruby instance variable association isn't too far off.
Sorry, hair-brained incomplete thought. I'm not worried that I think the crux of the matter is that extensible records are defined by their primitive operations - get, extend, and delete - but the commonly-used interface is get, update, and update-as-a-function-of-the-previous-value. And you know what? That's exactly the interface a Focus provides. So I think I see where you're coming from. My concerns are (1) I think we can solve the first one by making Focus an exposed record type with model |> @r.update (undo now) |> @theta.update (undo now) |> @state.set ShrinkingNot too bad. (I'm assuming I'm using |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
Apanatshka
Jul 10, 2015
Contributor
@ prefix. If you provide a record using the @ syntax, you could simply add the add and remove to that record too, and make extendable Focus big small recordExtension. Composing would then be: (=>) : Focus big medium r1 -> Focus medium small r2 -> Focus big small {}
|
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
avh4
Jul 10, 2015
Member
We need to get away from obscure punctuation as operators. I would propose changing .x from being the getter function to being a special RecordField type that can be used with new special functions update, set, get, etc:
import Record exposing (update, set, get, add, remove)
model |> update .x ((+) 1) |> set .y 7 |> get .zHowever, the obvious problem here is that the types for update, set, get, etc cannot be written.
|
We need to get away from obscure punctuation as operators. I would propose changing import Record exposing (update, set, get, add, remove)
model |> update .x ((+) 1) |> set .y 7 |> get .zHowever, the obvious problem here is that the types for update, set, get, etc cannot be written. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
TheSeamau5
Jul 10, 2015
Here's a rewrite of the todomvc according to my original proposal.
https://gist.github.com/TheSeamau5/2779c6cd6d1480993508
TheSeamau5
commented
Jul 10, 2015
|
Here's a rewrite of the todomvc according to my original proposal. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 10, 2015
Contributor
@Apanatshka I'm glad you like @ but I don't quite follow how add and remove can be added to the record. Sounds promising though.
@avh4 Your RecordField sounds very similar to a Focus. I've already said I don't like having get, set, and update be reserved words. I do like how nicely yours reads though, and . is much less visually obtrusive than @. We'd probably need extend and remove as well, and if this is the only syntax, you'd need a native implementation. Maybe is update was setWith, a bit like List.sortWith? I think a lot of people use update as a function name (and set : Set isn't unreasonable).
@TheSeamau5 Wow, that's a lot cleaner, and the occasional always isn't too bad. This would provide an update : (small -> small) -> big -> big which can create a Focus, which is still an optional, third-party, non-native library. It's probably the least invasive change, and I even like how .x and !x have the visual similarity of the dot on the lower-left.
Everyone: I'm not yet convinced whether it will be better to have dedicated syntax or a Focus-like library, and we should continue to explore both options.
|
@Apanatshka I'm glad you like @avh4 Your @TheSeamau5 Wow, that's a lot cleaner, and the occasional Everyone: I'm not yet convinced whether it will be better to have dedicated syntax or a Focus-like library, and we should continue to explore both options. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
dobesv
Jul 10, 2015
Contributor
@mgold @avh4 If the .foo syntax returned a record instead of a simple function, then the fields of that record are pretty easy to figure out using existing Elm syntax. You can still write functions that pull out the record fields and use them if you prefer prefix rather than dot notation.
.foo == {
get = \ r -> r.foo
update = \ v ->\ r -> { r | foo <- v }
map = \ f -> \ r -> { r | foo <- f r.foo }
add = \ v -> \ r -> { r | foo = v }
del = \ r -> ???
}
(note, the above is more like pseudocode, I doubt I got the syntaxes right)
|
@mgold @avh4 If the
(note, the above is more like pseudocode, I doubt I got the syntaxes right) |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 10, 2015
Contributor
Very nice @dobesv. Here's a minor variation:
.foo == {
get r = r.foo
set v r = { r | foo <- v }
map f r = { r | foo <- f r.foo }
add v r = { r | foo = v }
del r = {r - foo}
}Aside from sugaring those lambdas and implementing del, the only change I made was to rename update to set. I think the consistency of three-letter names is helpful, and I don't think it will be too confusing. Update/set is far more common that add, and add is also in the record.
Other question: would this be syntactic sugar or would the old record syntax disappear?
|
Very nice @dobesv. Here's a minor variation: .foo == {
get r = r.foo
set v r = { r | foo <- v }
map f r = { r | foo <- f r.foo }
add v r = { r | foo = v }
del r = {r - foo}
}Aside from sugaring those lambdas and implementing Other question: would this be syntactic sugar or would the old record syntax disappear? |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
texastoland
Jul 10, 2015
Focus has no concept of add/remove, only get/set/update.
Has anyone presented a use case for that or was it added for JS compatibility?
I suspect add/delete won't persist. I don't think two syntaxes would be a good idea either. Otherwise
texastoland
commented
Jul 10, 2015
I suspect |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rtfeldman
Jul 10, 2015
Member
Since it only took a single find/replace regexp, I took Hassan's example and tried the : sigil as a suffix:
I think this one reads the best for the most common use case.
|
Since it only took a single find/replace regexp, I took Hassan's example and tried the I think this one reads the best for the most common use case. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
evancz
Jul 10, 2015
Member
After some talks with Joey, I had been thinking about removing syntax for field addition and deletion. I'll start an issue going through the logic of this. This is what @dnalot is referring to.
Is it possible to write Focus.(=>) using @dobesv's proposal?
|
After some talks with Joey, I had been thinking about removing syntax for field addition and deletion. I'll start an issue going through the logic of this. This is what @dnalot is referring to. Is it possible to write |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
dobesv
Jul 10, 2015
Contributor
@evancz I think map can be used for the => implementation, assuming I understand => correctly.
compose outer inner = {
get = outer.map inner.get
set = outer.map inner.set
map = outer.map inner.map
add = outer.map inner.add
del = outer.map inner.del
}
|
@evancz I think
|
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
What's the type of that |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 10, 2015
Contributor
@rtfeldman Thanks for the demos. I think what @dobesv had in mind was .uid.set not set .uid. I like the former a lot better since it means set etc are not exposed at the top level.
@evancz I agree that 95% of the time, update and delete aren't used. But given that type system features are so hard to come by, I'm not convinced we should give one up. (It's good that Elm hasn't added everything from the last 20 years of ICFP, but I'm also worried about further restricting what is possible. Unless this buys us "complexity budget" for something else?)
Would this work? [EDIT: Beaten to it! Except I think Dobe's get is wrong.]
(=>) bigger smaller =
{
get r = bigger.get>>smaller.get
set v = bigger.map (smaller.set v)
map f = bigger.map (smaller.map f)
add v = bigger.map (smaller.add v)
del = bigger.map smaller.del
}|
@rtfeldman Thanks for the demos. I think what @dobesv had in mind was @evancz I agree that 95% of the time, update and delete aren't used. But given that type system features are so hard to come by, I'm not convinced we should give one up. (It's good that Elm hasn't added everything from the last 20 years of ICFP, but I'm also worried about further restricting what is possible. Unless this buys us "complexity budget" for something else?) Would this work? [EDIT: Beaten to it! Except I think Dobe's (=>) bigger smaller =
{
get r = bigger.get>>smaller.get
set v = bigger.map (smaller.set v)
map f = bigger.map (smaller.map f)
add v = bigger.map (smaller.add v)
del = bigger.map smaller.del
} |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
evancz
Jul 10, 2015
Member
See the proposal about field addition and removal in #985. Type system does not change at all. Just removing syntax.
What's the type of your (=>) function?
|
See the proposal about field addition and removal in #985. Type system does not change at all. Just removing syntax. What's the type of your |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
dobesv
Jul 10, 2015
Contributor
@evancz Something like ...
{ get : 'outer -> 'a
, set : 'outer -> 'a -> 'outer
, map : ('a -> 'x) -> 'outer -> 'outer
, ... } ->
{ get : 'inner -> 'b
, set : 'inner -> 'b -> 'inner
. map : ('b -> 'y) -> 'inner -> 'inner
, ... } ->
{ get : 'outer -> 'b
, set : 'outer -> 'b -> 'outer
. map : ('b -> 'y) -> 'outer -> 'outer
, ... }
?
|
@evancz Something like ...
? |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
@mgold Ah, gotcha. Here it is with that syntax: |
Apanatshka
referenced this issue
Jul 10, 2015
Closed
Proposal: remove record syntax for field addition and field deletion #985
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rtfeldman
Jul 10, 2015
Member
If we're only considering the "update" case, I'll throw something out there that sounds really nice but may be impossible to type-check: what if there were a function set that let you do this?
> set { a = "updated", b = "changed" } { a = "foo", b = "bar", c = "baz" }
{ a = "updated", b = "changed", c = "baz" } : { a : String, b : String, c : String }This is a function I've been enjoying for years in JS, but obviously JS does not have the challenge of trying to put a type on that thing.
If this could be solved somehow, it would offer:
- the composition benefits of the original proposal
- updating multiple fields at once, just like the status quo
- less syntax to learn
Is there any way to solve this, or is it just innately impossible?
|
If we're only considering the "update" case, I'll throw something out there that sounds really nice but may be impossible to type-check: what if there were a function > set { a = "updated", b = "changed" } { a = "foo", b = "bar", c = "baz" }
{ a = "updated", b = "changed", c = "baz" } : { a : String, b : String, c : String }This is a function I've been enjoying for years in JS, but obviously JS does not have the challenge of trying to put a type on that thing. If this could be solved somehow, it would offer:
Is there any way to solve this, or is it just innately impossible? |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rtfeldman
Jul 10, 2015
Member
The previous comment, combined with something @evancz said in #984, just gave me an idea.
What if we just took the current record update syntax...and curried it?
Status Quo:
case update of
NoOp ->
model
SetViewMode mode ->
{ model | viewMode <- mode }
OpenDocId id ->
{ model |
currentDocId <- Just id,
viewMode <- CurrentDocMode
}New Proposal:
case update of
NoOp ->
model
SetViewMode mode ->
{ viewMode <- mode } model
OpenDocId id ->
{ currentDocId <- (Just id), viewMode <- CurrentDocMode } modelI'm assuming this can be type-checked because the compiler has just as much access (albeit perhaps less trivially) to all the pieces of { foo | a <- "updated" } as it does to { a <- "updated" } foo, for any given foo and any given update.
I guess it might look weird in elm-repl though; if you just typed in > { foo <- "bar" } it would probably give you back <function> : a -> b or something?
|
The previous comment, combined with something @evancz said in #984, just gave me an idea. What if we just took the current record update syntax...and curried it? Status Quo: case update of
NoOp ->
model
SetViewMode mode ->
{ model | viewMode <- mode }
OpenDocId id ->
{ model |
currentDocId <- Just id,
viewMode <- CurrentDocMode
}New Proposal: case update of
NoOp ->
model
SetViewMode mode ->
{ viewMode <- mode } model
OpenDocId id ->
{ currentDocId <- (Just id), viewMode <- CurrentDocMode } modelI'm assuming this can be type-checked because the compiler has just as much access (albeit perhaps less trivially) to all the pieces of I guess it might look weird in |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 10, 2015
Contributor
That's a really interesting idea, and it certainly solves the one-at-a-time problem. Would we have different syntax for map vs. update/set?
Also, design in microcosm: just because the previous idea may be impossible to implement doesn't mean it's valueless.
|
That's a really interesting idea, and it certainly solves the one-at-a-time problem. Would we have different syntax for map vs. update/set? Also, design in microcosm: just because the previous idea may be impossible to implement doesn't mean it's valueless. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
Here's the "curry the status quo" proposal in TodoMVC: |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rtfeldman
Jul 10, 2015
Member
@mgold not sure about map, but personally I'm not convinced that use case is so common that it merits special syntax.
|
@mgold not sure about map, but personally I'm not convinced that use case is so common that it merits special syntax. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rtfeldman
Jul 10, 2015
Member
...of course, having said that, one just occurred to me: use <<- instead of <-, like so:
incrementCounts records =
List.map { count <<- (+1) } recordsA mnemonic: "it's like << but for <-"
|
...of course, having said that, one just occurred to me: use incrementCounts records =
List.map { count <<- (+1) } recordsA mnemonic: "it's like |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 10, 2015
Contributor
That's interesting, but I prefer = (I actually just spend a good 5 minutes debugging using = instead of <-). Also, <<- looks too much like the dreaded bind operator, for those who know just a little Haskell. .= and |= were suggested aways up the thread.
|
That's interesting, but I prefer |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
rtfeldman
Jul 10, 2015
Member
Well, = is taken by current syntax, e.g. { foo = "bar" } is a Record, not a function that takes a Record like I'm proposing { foo <- "bar" } would be.
What about <~? e.g.
incrementCounts records =
List.map { count <~ (+1) } recordsPresumably you could also mix and match setting and map-setting, e.g.
incrementCounts records =
List.map { count <~ (+1), incremented <- True } records|
Well, What about incrementCounts records =
List.map { count <~ (+1) } recordsPresumably you could also mix and match setting and map-setting, e.g. incrementCounts records =
List.map { count <~ (+1), incremented <- True } records |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
Please no reusing |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jul 11, 2015
Contributor
Ah right. Hmm. I'm with Jeff, no overloading <~ even if it should be deprecated.
|
Ah right. Hmm. I'm with Jeff, no overloading |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
evancz
Aug 5, 2015
Member
I'm gonna close this with the resolution of "we can't do this right this second, but we should revisit it after folks starting working with the component library I am working on and before 0.16 when we make changes like #985".
|
I'm gonna close this with the resolution of "we can't do this right this second, but we should revisit it after folks starting working with the component library I am working on and before 0.16 when we make changes like #985". |
evancz
closed this
Aug 5, 2015
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
willnwhite
Jun 18, 2016
The idea of this proposal is very simple, to have a shorthand syntax for setters that is analogous to the function form of getters.
I'm new. Why can't we do record.field = value?
willnwhite
commented
Jun 18, 2016
•
I'm new. Why can't we do |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jun 19, 2016
Contributor
Why can't we do
record.field = value?
Because = is a definition, not an assignment. It seems like you want say, "before record.field had this value, but now it has this other value". But that's mutation and you can't do that. Instead you have to create a copy of the record with the value changed, and then use = to define some unused name to that new record.
Because |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
willnwhite
Jun 19, 2016
Why couldn't record.field = value do the same things behind the scenes as { record | field = value }, i.e. return a new record with field updated to value?
willnwhite
commented
Jun 19, 2016
•
|
Why couldn't |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
willnwhite
Jun 19, 2016
I may have answered my own question. I guess { record | field = value } is more explicit about what it's returning, and is like using the object spread syntax in JavaScript: { ...record, field: value }. You might expect record.field = value to return value, not a new record with updated field.
willnwhite
commented
Jun 19, 2016
•
|
I may have answered my own question. I guess |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
|
Moreover, how would you interpret |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
willnwhite
Jun 20, 2016
Well, let's say we did make record.field = value return an updated record, not value. Then wouldn't Elm just evaluate right-to-left and make newRecord refer to the updated record?
willnwhite
commented
Jun 20, 2016
|
Well, let's say we did make |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mgold
Jun 21, 2016
Contributor
Technically that might work, but it will look very weird to someone coming from JS, Python, or Ruby (where a = b = 5 sets a and b to 5).
|
Technically that might work, but it will look very weird to someone coming from JS, Python, or Ruby (where |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
willnwhite
Jun 21, 2016
Thanks Max. I'd like to slot this into the syntax documentation. I'm sure developers used to dot notation for updates will miss it and wonder why it's not possible.
willnwhite
commented
Jun 21, 2016
•
|
Thanks Max. I'd like to slot this into the syntax documentation. I'm sure developers used to dot notation for updates will miss it and wonder why it's not possible. |
added a commit
to willnwhite/elm-lang.org
that referenced
this issue
Jun 21, 2016
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
maackle
Oct 7, 2016
It's one year after this was closed, and I don't see much more discussion on this. Also, we're well past 0.16 now. Is there any news on this front? Any other issues one can point me to? If not, can we revisit this? I'm really feeling the pain. Focus helps a bit, but it's just as verbose, and just feels like a workaround to this basic lack in the language. Every time I want the equivalent of !foo as mentioned above, I have to first type:
foo =
Focus.create .foo (\f r -> { r | foo = f r.foo })The boilerplate is so painfully apparent. Can't the language just give us this for free?
maackle
commented
Oct 7, 2016
|
It's one year after this was closed, and I don't see much more discussion on this. Also, we're well past 0.16 now. Is there any news on this front? Any other issues one can point me to? If not, can we revisit this? I'm really feeling the pain. foo =
Focus.create .foo (\f r -> { r | foo = f r.foo })The boilerplate is so painfully apparent. Can't the language just give us this for free? |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mltsy
May 3, 2017
Would it be good to open a new issue for this, now that we are 2 (almost 3?) releases past the most recent milestone mentioned here? Or will it be reopened when it is time? I don't see any tracking of this ticket and I haven't seen a refusal to implement this language feature... but also haven't seen any discussion on it for quite a while. Is it time to revisit this @evancz? (I just don't know who else could answer this question and it was your suggestion to revisit it later)
I've also been trying to consider alternative solutions, like perhaps having a representation of a field that can be passed around so that it would be easier to define abstract setters, but none of them come out as elegant as just having a field update syntax.
mltsy
commented
May 3, 2017
|
Would it be good to open a new issue for this, now that we are 2 (almost 3?) releases past the most recent milestone mentioned here? Or will it be reopened when it is time? I don't see any tracking of this ticket and I haven't seen a refusal to implement this language feature... but also haven't seen any discussion on it for quite a while. Is it time to revisit this @evancz? (I just don't know who else could answer this question and it was your suggestion to revisit it later) I've also been trying to consider alternative solutions, like perhaps having a representation of a field that can be passed around so that it would be easier to define abstract setters, but none of them come out as elegant as just having a field update syntax. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
markevans
Jul 3, 2017
I agree the update syntax would remove a lot of unnecessary boilerplate. My thoughts on it are as follows, though I'm new to Elm (coming from Ruby) and am fully aware that I may have missed important aspects of how this needs to work, so apologies if so.
Given that .x gives a getter function, the most intuitive for me would be having both a raw setter .x=
.x= : b -> { r | x : a } -> { r | x : b }e.g.
position |> .x= 5and an updater .x:
.x: : (a -> b) -> { r | x : a } -> { r | x : b }e.g.
position |> .x: ((+) 1)Then as @TheSeamau5 noted at the beginning of the thread, with function composition, we have a neat syntax for updating nested records, e.g. to update name in
model = { session = { user = { name = "Barry" } } }we just do (with the raw setter)
model |> ( .session: << .user: << .name= ) "Helen"or (with the updater)
model |> ( .session: << .user: << .name: ) String.reverseFor setting multiple fields at a time, you wouldn't need special syntax, but could just have something like
merge : a -> r -> { r | a }and use as
import Record exposing (merge)
model |> ( .session: << .user: << merge ) { name = "Zod" }I hope that makes sense, and that I'm not missing anything glaring.
markevans
commented
Jul 3, 2017
•
|
I agree the update syntax would remove a lot of unnecessary boilerplate. My thoughts on it are as follows, though I'm new to Elm (coming from Ruby) and am fully aware that I may have missed important aspects of how this needs to work, so apologies if so. Given that .x= : b -> { r | x : a } -> { r | x : b }e.g. position |> .x= 5and an updater .x: : (a -> b) -> { r | x : a } -> { r | x : b }e.g. position |> .x: ((+) 1)Then as @TheSeamau5 noted at the beginning of the thread, with function composition, we have a neat syntax for updating nested records, e.g. to update model = { session = { user = { name = "Barry" } } }we just do (with the raw setter) model |> ( .session: << .user: << .name= ) "Helen"or (with the updater) model |> ( .session: << .user: << .name: ) String.reverseFor setting multiple fields at a time, you wouldn't need special syntax, but could just have something like merge : a -> r -> { r | a }and use as import Record exposing (merge)
model |> ( .session: << .user: << merge ) { name = "Zod" }I hope that makes sense, and that I'm not missing anything glaring. |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mltsy
Jul 3, 2017
@markevans I would only have a problem with types here. I think we'd want the input and output types to be constricted to the same type, so we're getting the same type of record out of the setter that you put into it, but other than that... just make all the bs into as and it actually doesn't look bad... :)
mltsy
commented
Jul 3, 2017
|
@markevans I would only have a problem with types here. I think we'd want the input and output types to be constricted to the same type, so we're getting the same type of record out of the setter that you put into it, but other than that... just make all the |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
dobesv
Jul 3, 2017
Contributor
My concern about this approach is that it is not particularly easy to parse these combinations of symbols with our limited human brains. This will be especially true for people new to the language, or new to programming. The solution would be find for very advanced programmers but very challenging for new ones.
I think Scala (and Haskell) run into this same challenge where they are trying to make things really terse by using a lot of special syntax and operators, but then the code becomes very dense and hard to understand. When code is harder to read it is more likely to hide bugs.
For example, the visual difference between .foo= and .foo: is very small, yet very significant. I admit you will probably get an error back if you get these mixed up, but it may take some time for someone to understand the error.
Another example, people often have a hard time understand the results of function compositions, so parsing (.session: << .user: << .name:) is going to slow people down.
Personally I would advocate for a more verbose approach. For example in JavaScript using lodash you might say _.set(foo, "a.b.c", "George") And if "a.b.c" were a variable of some sort then you could pass that in.
For Elm's purposes this might translate to:
(set foo .a.b.c "George") where .a.b.c could have been a variable.
Of course this isn't that easy to add to Elm because the type of .a.b.c would have to be something different than it is currently, without losing it's current feature of being a callable getter.
|
My concern about this approach is that it is not particularly easy to parse these combinations of symbols with our limited human brains. This will be especially true for people new to the language, or new to programming. The solution would be find for very advanced programmers but very challenging for new ones. I think Scala (and Haskell) run into this same challenge where they are trying to make things really terse by using a lot of special syntax and operators, but then the code becomes very dense and hard to understand. When code is harder to read it is more likely to hide bugs. For example, the visual difference between Another example, people often have a hard time understand the results of function compositions, so parsing Personally I would advocate for a more verbose approach. For example in JavaScript using lodash you might say For Elm's purposes this might translate to:
Of course this isn't that easy to add to Elm because the type of |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
mltsy
Jul 3, 2017
Yeah, I mean if there were an objectively more eloquent syntax that would actually work, that would be great, but I haven't seen one... any ideas?
mltsy
commented
Jul 3, 2017
|
Yeah, I mean if there were an objectively more eloquent syntax that would actually work, that would be great, but I haven't seen one... any ideas? |
This comment has been minimized.
Show comment
Hide comment
This comment has been minimized.
markevans
Jul 3, 2017
@mltsy makes sense (about restricting input/output types) - I don't know enough to make much judgement on that but I guess that'd be up to @evancz and co. if they ever implemented something like this.
@dobesv fair point about being tricky to parse mentally. and perhaps there would be a better syntax than .x: (though personally I quite like it)
One problem I can imagine with something like (set foo .a.b.c "George"), however, is that it assumes that each layer is a record (as far as I can see), whereas composing individual functions would allow you to have one layer as a Dict or something (and replace the .foo: setter with an appropriate function) so is a bit more flexible
markevans
commented
Jul 3, 2017
•
|
@mltsy makes sense (about restricting input/output types) - I don't know enough to make much judgement on that but I guess that'd be up to @evancz and co. if they ever implemented something like this. @dobesv fair point about being tricky to parse mentally. and perhaps there would be a better syntax than |
TheSeamau5 commentedJul 9, 2015
Suppose you have the record:
You can access a field from the record in one of two ways
But, on the other hand, you can "set" a field in a record in one way
which can get pretty verbose when wanting to pass this updating as a function
The idea of this proposal is very simple, to have a shorthand syntax for setters that is analogous to the function form of getters.
For example, setting the field x could be done via the
!xfunction whereThis function would take a mapping which would be applied to the value of the "x" field.
As with the getters, this syntax would be present in the language and thus can be used for fields of any names.
Furthermore, nested updates are trivial
Suppose you have the following type
and you have a value of type Entity called
entityand you wish to increment only the x value. You do it via function composition
Under this proposal, function composition suffices to update values as deeply nested as you want. One only needs to remember that the composition starts from the inner-most field you wish to update.