New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Field update/map syntax #984

Closed
TheSeamau5 opened this Issue Jul 9, 2015 · 66 comments

Comments

Projects
None yet
@TheSeamau5

TheSeamau5 commented Jul 9, 2015

Suppose you have the record:

p = { x = 1 , y = 2 }

You can access a field from the record in one of two ways

-- 1) direct dot syntax on the variable
p.x 

-- 2) as a getter function
.x p

-- The getter function is particularly useful as a function
-- that can be passed to other functions (such as `List.map`)

But, on the other hand, you can "set" a field in a record in one way

{ p | x <- someValue }

which can get pretty verbose when wanting to pass this updating as a function

setX someValue p = { p | x <- someValue }

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 !x function where

!x :  (a -> b) -> { r | x : a } -> { r | x : b }

This 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.

!x (always 5) p == { x = 5, y = 2 }

!x ((+) 1) p == { x = 1, y = 2 }

Furthermore, nested updates are trivial

Suppose you have the following type

type alias Entity = 
  { position : { x : Float , y : Float } }

and you have a value of type Entity called entity

entity : Entity
entity = 
  { position = { x = 0, y = 1 } }

and you wish to increment only the x value. You do it via function composition

(!x >> !position) ((+1)) entity == { position = { x = 1, y = 1 } }

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.

@Apanatshka

This comment has been minimized.

Show comment
Hide comment
@Apanatshka

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.

Contributor

Apanatshka commented Jul 9, 2015

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.

@Apanatshka

This comment has been minimized.

Show comment
Hide comment
@Apanatshka

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 }
Contributor

Apanatshka commented Jul 9, 2015

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 }
@TheSeamau5

This comment has been minimized.

Show comment
Hide comment
@TheSeamau5

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 TheSeamau5 changed the title from Proposal: Field setter syntax to Proposal: Field update/map syntax Jul 9, 2015

@karldray

This comment has been minimized.

Show comment
Hide comment
@karldray

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 always.

@TheSeamau5

This comment has been minimized.

Show comment
Hide comment
@TheSeamau5

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
@Apanatshka

This comment has been minimized.

Show comment
Hide comment
@Apanatshka

Apanatshka Jul 9, 2015

Contributor

You could add syntax for both 😁

Contributor

Apanatshka commented Jul 9, 2015

You could add syntax for both 😁

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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".

Contributor

mgold commented Jul 9, 2015

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".

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

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
Member

rtfeldman commented Jul 9, 2015

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
@Apanatshka

This comment has been minimized.

Show comment
Hide comment
@Apanatshka

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
Contributor

Apanatshka commented Jul 9, 2015

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
@texastoland

This comment has been minimized.

Show comment
Hide comment
@texastoland

texastoland Jul 9, 2015

  1. Do you have a real world example you can share where this would vastly improve readability?
  2. 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

  1. Do you have a real world example you can share where this would vastly improve readability?
  2. I'm against reconstituting math operators. That's not going to help anyone learn the language. It might even confuse me.
@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

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: CurrentDocMode

This 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."

Member

rtfeldman commented Jul 9, 2015

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: CurrentDocMode

This 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."

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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= Shrinking

For 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?

Contributor

mgold commented Jul 10, 2015

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= Shrinking

For 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?

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

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.

Member

rtfeldman commented Jul 10, 2015

I believe function composition would still require a compiler optimization; each function would still get called, and would still perform only one update.

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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 } -> r

This 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.

Contributor

mgold commented Jul 10, 2015

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 } -> r

This 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.

@texastoland

This comment has been minimized.

Show comment
Hide comment
@texastoland

texastoland Jul 10, 2015

Beginning to look compelling. Do you recommend the paper? Is there a quicker read? Concerns:

  1. Start a modified TodoMVC like @rtfeldman in #979.
  2. =! looks incongruent to .= and potentially unintuitive. |=?
  3. 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:

  1. Start a modified TodoMVC like @rtfeldman in #979.
  2. =! looks incongruent to .= and potentially unintuitive. |=?
  3. How much danger is there of conflating referential transparency using =?

Very interesting @TheSeamau5!

@dobesv

This comment has been minimized.

Show comment
Hide comment
@dobesv

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.

Contributor

dobesv commented Jul 10, 2015

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.

@Apanatshka

This comment has been minimized.

Show comment
Hide comment
@Apanatshka

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.

Contributor

Apanatshka commented Jul 10, 2015

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.

@texastoland

This comment has been minimized.

Show comment
Hide comment
@texastoland

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

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.

👍

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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.

Contributor

mgold commented Jul 10, 2015

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.

@texastoland

This comment has been minimized.

Show comment
Hide comment
@texastoland

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

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.

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

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.

Member

rtfeldman commented Jul 10, 2015

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.

@texastoland

This comment has been minimized.

Show comment
Hide comment
@texastoland

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

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.

@dobesv

This comment has been minimized.

Show comment
Hide comment
@dobesv

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

Contributor

dobesv commented Jul 10, 2015

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

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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 Shrinking

Not 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.

Contributor

mgold commented Jul 10, 2015

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 Shrinking

Not 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.

@Apanatshka

This comment has been minimized.

Show comment
Hide comment
@Apanatshka

Apanatshka Jul 10, 2015

Contributor

👍 for the @ 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 {}

Contributor

Apanatshka commented Jul 10, 2015

👍 for the @ 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 {}

@avh4

This comment has been minimized.

Show comment
Hide comment
@avh4

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 .z

However, the obvious problem here is that the types for update, set, get, etc cannot be written.

Member

avh4 commented Jul 10, 2015

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 .z

However, the obvious problem here is that the types for update, set, get, etc cannot be written.

@TheSeamau5

This comment has been minimized.

Show comment
Hide comment
@TheSeamau5

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.
https://gist.github.com/TheSeamau5/2779c6cd6d1480993508

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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.

Contributor

mgold commented Jul 10, 2015

@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.

@dobesv

This comment has been minimized.

Show comment
Hide comment
@dobesv

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)

Contributor

dobesv commented Jul 10, 2015

@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

This comment has been minimized.

Show comment
Hide comment
@mgold

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?

Contributor

mgold commented Jul 10, 2015

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?

@texastoland

This comment has been minimized.

Show comment
Hide comment
@texastoland

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

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 👍

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

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:

TodoMVC with : sigil suffix

I think this one reads the best for the most common use case. 😃

Member

rtfeldman commented Jul 10, 2015

Since it only took a single find/replace regexp, I took Hassan's example and tried the : sigil as a suffix:

TodoMVC with : sigil suffix

I think this one reads the best for the most common use case. 😃

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

rtfeldman Jul 10, 2015

Member

Since it took another 5 seconds, I also tried it out with set as proposed in @avh4's comment and @mgold's comment:

TodoMVC with set .field record

Member

rtfeldman commented Jul 10, 2015

Since it took another 5 seconds, I also tried it out with set as proposed in @avh4's comment and @mgold's comment:

TodoMVC with set .field record

@evancz

This comment has been minimized.

Show comment
Hide comment
@evancz

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?

Member

evancz commented Jul 10, 2015

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?

@dobesv

This comment has been minimized.

Show comment
Hide comment
@dobesv

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
}
Contributor

dobesv commented Jul 10, 2015

@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

This comment has been minimized.

Show comment
Hide comment
@evancz

evancz Jul 10, 2015

Member

What's the type of that compose function?

Member

evancz commented Jul 10, 2015

What's the type of that compose function?

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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
    }
Contributor

mgold commented Jul 10, 2015

@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
    }
@evancz

This comment has been minimized.

Show comment
Hide comment
@evancz

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?

Member

evancz commented Jul 10, 2015

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?

@dobesv

This comment has been minimized.

Show comment
Hide comment
@dobesv

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
, ... }

?

Contributor

dobesv commented Jul 10, 2015

@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
, ... }

?

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

rtfeldman Jul 10, 2015

Member

@mgold Ah, gotcha. Here it is with that syntax:

TodoMVC with .field.set record

Member

rtfeldman commented Jul 10, 2015

@mgold Ah, gotcha. Here it is with that syntax:

TodoMVC with .field.set record

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

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?

Member

rtfeldman commented Jul 10, 2015

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?

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

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 } model

I'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?

Member

rtfeldman commented Jul 10, 2015

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 } model

I'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?

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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.

Contributor

mgold commented Jul 10, 2015

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.

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

rtfeldman Jul 10, 2015

Member

Here's the "curry the status quo" proposal in TodoMVC:

TodoMVC with curried record updates

Member

rtfeldman commented Jul 10, 2015

Here's the "curry the status quo" proposal in TodoMVC:

TodoMVC with curried record updates

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

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.

Member

rtfeldman commented Jul 10, 2015

@mgold not sure about map, but personally I'm not convinced that use case is so common that it merits special syntax.

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

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) } records

A mnemonic: "it's like << but for <-"

Member

rtfeldman commented Jul 10, 2015

...of course, having said that, one just occurred to me: use <<- instead of <-, like so:

incrementCounts records =
  List.map { count <<- (+1) } records

A mnemonic: "it's like << but for <-"

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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.

Contributor

mgold commented Jul 10, 2015

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.

@rtfeldman

This comment has been minimized.

Show comment
Hide comment
@rtfeldman

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) } records

Presumably you could also mix and match setting and map-setting, e.g.

incrementCounts records =
  List.map { count <~ (+1), incremented <- True } records
Member

rtfeldman commented Jul 10, 2015

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) } records

Presumably you could also mix and match setting and map-setting, e.g.

incrementCounts records =
  List.map { count <~ (+1), incremented <- True } records
@Apanatshka

This comment has been minimized.

Show comment
Hide comment
@Apanatshka

Apanatshka Jul 10, 2015

Contributor

Please no reusing <~, it's already used for Signal.map

Contributor

Apanatshka commented Jul 10, 2015

Please no reusing <~, it's already used for Signal.map

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

mgold Jul 11, 2015

Contributor

Ah right. Hmm. I'm with Jeff, no overloading <~ even if it should be deprecated.

Contributor

mgold commented Jul 11, 2015

Ah right. Hmm. I'm with Jeff, no overloading <~ even if it should be deprecated.

@evancz

This comment has been minimized.

Show comment
Hide comment
@evancz

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".

Member

evancz commented Aug 5, 2015

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 evancz closed this Aug 5, 2015

@Apanatshka Apanatshka referenced this issue Nov 7, 2015

Closed

Record ideas #1068

@willnwhite

This comment has been minimized.

Show comment
Hide comment
@willnwhite

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

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?

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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.

Contributor

mgold commented Jun 19, 2016

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.

@willnwhite

This comment has been minimized.

Show comment
Hide comment
@willnwhite

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 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

This comment has been minimized.

Show comment
Hide comment
@willnwhite

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 { 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.

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

mgold Jun 19, 2016

Contributor

Moreover, how would you interpret newRecord = record.field = value?

Contributor

mgold commented Jun 19, 2016

Moreover, how would you interpret newRecord = record.field = value?

@willnwhite

This comment has been minimized.

Show comment
Hide comment
@willnwhite

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 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?

@mgold

This comment has been minimized.

Show comment
Hide comment
@mgold

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

Contributor

mgold commented Jun 21, 2016

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

@willnwhite

This comment has been minimized.

Show comment
Hide comment
@willnwhite

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.

willnwhite added a commit to willnwhite/elm-lang.org that referenced this issue Jun 21, 2016

@maackle

This comment has been minimized.

Show comment
Hide comment
@maackle

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

@mltsy

This comment has been minimized.

Show comment
Hide comment
@mltsy

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.

@markevans

This comment has been minimized.

Show comment
Hide comment
@markevans

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= 5

and 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.reverse

For 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 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= 5

and 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.reverse

For 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.

@mltsy

This comment has been minimized.

Show comment
Hide comment
@mltsy

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 bs into as and it actually doesn't look bad... :)

@dobesv

This comment has been minimized.

Show comment
Hide comment
@dobesv

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.

Contributor

dobesv commented Jul 3, 2017

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.

@mltsy

This comment has been minimized.

Show comment
Hide comment
@mltsy

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?

@markevans

This comment has been minimized.

Show comment
Hide comment
@markevans

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 .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

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