Request: loosen syntactic constraints on base-record in record extend/update syntax

411 views
Skip to first unread message

Jeff Smits

unread,
Aug 17, 2013, 1:16:43 PM8/17/13
to elm-discuss
(Google groups doesn't want to post this apparently :( retrying through email. Hopefully I won't cause double thread this way... )
General Problem

This is a relatively small request I guess. Right now record extend and update syntax both use: { baseRecord | ... }.
The baseRecord may only be a simple identifier right now. I would like a bit more flexibility there.

Specific Use-Cases
There are two that I have in mind. The first is simpler than the second.

1.
Say you have a nested record:

type Graph = {nodes: [Node], edges: [Edge]}
type ProgramState = {graph: Graph, some:Int, other:(Int,Int), stuff:[(Int,Int)]}

and you want to update that sub-record:

looseEdges : ProgramState -> Graph
looseEdges programState = { programState.graph | nodes <- [] }
                            ^ type error because of the dot (.)


It feels silly to have to write:

looseEdges programState =
  let g = programState.graph
  in { g | nodes <- [] }


2. (Note that this example is about loosening constraints on the type-level syntax, not the value-level syntax)
Say you have:

type Two a = { one: a, two: a }

And you want to extend that type with three and name it Three. What I would like but can't do is:

type Three a = { Two a | three: a }

Specific Approach
Syntactically allowing any expression between { and | would be most intuitive to me. (Obviously the expression should be type-checked to yield a record type. )

Background Materials
N.A.

Alternative Solutions

  1. Loosening the syntax to only allow these specific cases is another solution.
  2. Or maybe allowing everything except record extend/update syntax.
  3. Perhaps a nice subset of expressions to allow is: polymorphic type(s/aliases), function calls, record/module access
Known Issues
Allowing arbitrary expressions gives the ability to write things like:

newRecord = { { { { this=1 } | is=2 } | ridiculously=3 } | complex=4 }

Not really something I'd like to read and probably also complicated to parse..

Justification
Allowing the syntax in use case #1 would make the syntax more intuitive IMHO. I feel stupid having to write a let expression for something as simple as the example I gave.
Allowing the syntax in use case #2 will make it easier to emulate Haskell-like typeclasses through explicit dictionary-passing. I was planning on posting something about that to the mailing-list but then I came across this :(

Evan Czaplicki

unread,
Aug 18, 2013, 4:45:03 PM8/18/13
to elm-d...@googlegroups.com
The main concern is as follows. Which of the following meanings is correct?

{ programState.graph | nodes <- [] } : ProgramState
{ programState.graph | nodes <- [] } : [Node]

If you allow arbitrary expressions in the left part, the latter is the correct choice. You update the specific record you were given. Your example assumes the former.

Spiros recommended something like this:

{ graph in programState | nodes <- [] } : ProgramState
{ graph of programState | nodes <- [] } : ProgramState

For deep changes. It seems reasonable, but kind of crazy.

I'd like to see how this all fits with the discussion of update and extend syntax from a while ago. I'd also be curious what the lenses people would have to say.


--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.

John Mayer (gmail)

unread,
Aug 18, 2013, 6:11:44 PM8/18/13
to elm-d...@googlegroups.com
I've run into (2); I would definitely like to be able to define type synonyms like this:

B = { A | x : Int }

(1) I think that Jeff's type annotation was correct. It returned a Graph. And I agree that it's the expected behavior. I rather like being able to put an arbitrary expression to the left of the pipe.

I feel that both of these have strong merits independent of any new update/extend syntax.

Sent from my iPhone

Evan Czaplicki

unread,
Aug 18, 2013, 6:21:45 PM8/18/13
to elm-d...@googlegroups.com
Oops, yeah, I got mixed up reading the types. Jeff's annotation is correct, and that means we are in agreement about what it should mean.

Max Goldstein

unread,
Aug 18, 2013, 10:23:03 PM8/18/13
to elm-d...@googlegroups.com
I ran into something similar with

{(dashed gray) | width <- 10}

In this use case I would expect to get back the LineStyle record with the change made.

Alexander Noriega

unread,
Aug 21, 2013, 3:20:31 PM8/21/13
to elm-d...@googlegroups.com
I was asking myself about this (lenses) the other day. Maybe Elm's Basics module could include a bit of generic Lens-related functionality.

I've been using some lense-ish type aliases for dealing with nested things in records. It helps me be a bit DRYer.


-- Basic lib
type Lens r f = { get:(r -> f), set:(r -> f -> r) }
(|+|) : Lens a b -> Lens b c -> Lens a c
(|+|) l1 l2 = Lens (l2.get . l1.get) (\r v -> l1.set r ((l2.set (l1.get r) v)))

-- Game lib
type Controls = { shoot:Char, power:Char }
type Settings = { controls:Controls }
type Game = { settings:Settings }
settingsL : Lens Game Settings
settingsL = Lens .settings (\g s -> { g | settings <- s })
controlsL : Lens Settings Controls
controlsL = Lens .controls (\s c -> { s | controls <- c })
shootL : Lens Controls Char
shootL = Lens .shoot (\c s -> { c | shoot <- s })

-- Example usage
update : Game -> Game
update g = let settingsShootL = settingsL |+| controlsL |+| shootL
           in settingsShootL.set g 'Z'

main = asText <| update (Game (Settings (Controls 'X' 'C')))

Jeff Smits

unread,
Sep 5, 2013, 3:53:07 AM9/5/13
to elm-d...@googlegroups.com
So... Was the general idea of this request accepted?
I revised my opinion when I re-read my request. I think I prefer alternative solution 3.

John Mayer (gmail)

unread,
Sep 5, 2013, 10:19:48 AM9/5/13
to elm-d...@googlegroups.com
I still think it's worthwhile, I've run into both of these now in my code. Can you expand upon the specifics of alternative 3?

Sent from my iPhone

Max Goldstein

unread,
Sep 5, 2013, 6:52:48 PM9/5/13
to elm-d...@googlegroups.com
I also think that it's worthwhile. I haven't fully grasped all the relevant theory, but I would imagine that

{ expr | name <- val}

should be valid whenever expr (which has higher precedence that the record modification) has type record, with a field named name that has the same type as val.

Evan Czaplicki

unread,
Sep 5, 2013, 9:09:19 PM9/5/13
to elm-d...@googlegroups.com
I think this should be allowed, but I am worried about how this code would look as expressions got longer. It also makes things a bit tougher for the parser. For example, here are two valid records when expressions are allowed:

{ f a b c d e | x <- 10 }
{ f a b c d e = 42 }

I think the update and extend syntax proposed a while ago clears this up quite a bit, but I am not confident that that is a good direction. This is not a feature that other functional languages tend to have, so I have very little experiential data. Basically, there are a couple compelling ways to do this, and it is not clear to me at all which is better. Thus the inaction.

I think if we can write a believable example application that does a lot of things that are awkward with the current restrictions, we'd be able to see which proposal is most pleasant to use in real life. Perhaps a github project where we can have a branch for each proposed syntax or something? I think that'd help a lot actually.

John Mayer (gmail)

unread,
Sep 5, 2013, 9:36:52 PM9/5/13
to elm-d...@googlegroups.com
Sorry, but that second expression? Going back to Max's post, 'expr' could be anything, but not 'name'.

Sent from my iPhone

Evan Czaplicki

unread,
Sep 5, 2013, 10:45:51 PM9/5/13
to elm-d...@googlegroups.com
Oh, maybe I didn't publicize that feature too much. If so, shhhhh :P

You can define functions in a record. So the following two things are the same:

group  = { zero = 0, plus = \n m -> n + m }
group' = { zero = 0, plus n m = n + m }

Max Goldstein

unread,
Sep 5, 2013, 11:11:31 PM9/5/13
to elm-d...@googlegroups.com
Oooo. Functions as first-class citizens indeed.

So the trouble is, should we evaluate f and get a record back to modify, or should we store f as a definition? I'm not writing the parser so I can't say definitively, but it doesn't look impossible or ambiguous. A tricky case:

{name = f 42, f' a = 42}

Evan Czaplicki

unread,
Sep 5, 2013, 11:31:29 PM9/5/13
to elm-d...@googlegroups.com
That one is not terrible because you can tell what is happening as soon as the equals sign comes. It's kind of the same as the example I showed above.

In any case, let's ignore parsing issues for now. Let's just assume it can be sorted out. I think doing the code comparison is the real blocker.

David Sargeant

unread,
Jul 26, 2014, 8:26:02 AM7/26/14
to elm-d...@googlegroups.com
Seems a little strange that the following does not work:

{ {} | attrs = [1] }

I know it's not really useful, but I guess it goes back to the issue of allowing arbitrary expressions to the left of the vertical bar.

Jeff Smits

unread,
Jul 26, 2014, 12:08:22 PM7/26/14
to elm-discuss
Yeah, I really want this feature. But I guess I forgot about the "code comparison".

@Evan: can you say more concretely what kind of code comparison you want to see before moving forward on this feature?

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Martin Cerny

unread,
Sep 11, 2016, 4:37:10 PM9/11/16
to Elm Discuss
Hi,
hope its polite to bump up an old thread like this - if not, please accept my apologies. I have been struggling with the record update syntax for a while and wanted to share another use case which I think could be made better.
So let's say I want to populate a list of objects of the same type which have a lot of properties, most of which are kept default, e.g.

type alias Customer =
  { id : Int,
    Name : String ,
    -- Lots of other stuff
  }

and I have a "constructor" function to build instances with default values, but never forget to fill in the id, e.g.

defaultCustomer : Int -> Customer
defaultCustomer id =
  { id = id,
   Name = "",
   -- lots of other initialization
  }

now I would like to write things like
[
  { (defaultCustomer 1) | Name = "Smith" },
  { (defaultCustomer 2) | Name = "Jones" }
]

Which is not possible.

Now I have two options:
a) do not use a "constructor" and always write the records in full (not nice since they have a lot of fields which are mostly left default)
b) just have a template defaultCostumer : Customer and hope I will never forget to fill in the id (used in messages)
c) have a long "let" clause before defining the list

Neither of which seems nice.

I'll probably go with b), but if anyone has a nice suggestion how to enforce filling in a record in this way with the current syntax it would be very welcome.

And thanks for the work on elm - I am learning it and having fun with it!

Martin





 but I tried to figure out where on GitHub should this be discussed and kind of failed...
(is it https://github.com/elm-lang/elm-plans/issues/16 or

Nick H

unread,
Sep 12, 2016, 11:50:21 AM9/12/16
to elm-d...@googlegroups.com
In this case, I think the best thing to do would be to make a construction function that takes a name as well as an id.

defaultCustomer : Int -> String -> Customer
defaultCustomer id name =
  { id = id,
  , name = name,

   -- lots of other initialization
  }


You constructor function should never return an incomplete/invalid record. If your customers always need names, make sure they always get names!



To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

Martin Cerny

unread,
Sep 13, 2016, 10:03:36 AM9/13/16
to Elm Discuss
Hi,
thanks for the suggestion, but I think you are answering to a different problem than the one I had in mind, I'll try to be more clear to avoid confusion.
In my use case, I have a lot of fields, which usually can be kept with their default values, but some of them aren't. So not all customers need names. Maybe a better example of the code I want to write would be:

type alias Customer =
 
{ id : Int,

    name
: String ,
    occupation
: String,
    nickname
: String,
    height
: Float,

   
-- Lots of other stuff
 
}



defaultCustomer
: Int -> Customer

defaultCustomer id
=
 
{ id = id,

   name
= "",
   occupation = "",
   nickname
"N/A",
   height
: 1.6,
    -- lots of other initialization
 
}

[
 
{ (defaultCustomer 1) | name = "Smith", occupation = "Clerk" },
 
{ (defaultCustomer 2) | name = "Jones" }
 
{ (defaultCustomer 3) | nickname = "R2-D2", height = 0.5 }
]



Martin

Nick H

unread,
Sep 13, 2016, 1:49:44 PM9/13/16
to elm-d...@googlegroups.com
OK, this is a more complex issue than what I had in mind. If you have a small number of desired initializations (maybe 2 or 3 cases besides defaultCustomer), I think it would make sense to have multiple construction functions. But of course that won't scale at all. 


To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

Nick H

unread,
Sep 13, 2016, 2:03:14 PM9/13/16
to elm-d...@googlegroups.com
Another solution might be to define some setters, so that you could write something like:

[
 
defaultCustomer 1 |> setName "Smith" |> setOccupation |> "Clerk",
 
defaultCustomer 2 |> setName "Jones",
 
defaultCustomer 3 |> setNickname "R2-D2" |> setHeight 0.5
]


Unfortunately, defining all those setters is verbose. But I often find them much nicer to work with than Elm's record-updating syntax.

I am willing to jump through a lot of hoops in order to use pipe operators :-)

Martin Cerny

unread,
Sep 14, 2016, 10:41:04 AM9/14/16
to Elm Discuss
The pipe solution seems OKish - thanks for the suggestion!

Martin

Jaap Bouma

unread,
Sep 14, 2016, 11:29:31 AM9/14/16
to elm-d...@googlegroups.com
How about this:

type alias Customer =
    { id : Int
    , name : String
    , height: Float
    }

customerDefaults = { name = "", height = 0.0 }

newCustomer : Int -> { a | name: String, height: Float } -> Customer
newCustomer id opts =
    { id = id
    , name = opts.name
    , height = opts.height
    }

customers =
    [ newCustomer 1 { customerDefaults | name = "Bill" }
    , newCustomer 2 { customerDefaults | height = 2.0 }
    ]


To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--
Jaap Bouma
Oosterstraat 9
8011 GM  Zwolle
06 2951 2694

Jaap Bouma

unread,
Sep 14, 2016, 11:32:31 AM9/14/16
to elm-d...@googlegroups.com
.. or depending on your wider context, if you don't want the customerDefaults value polluting your namespace:

customers =
    let
        customerDefaults = { name = "", height = 0.0 }
    in
        [ newCustomer 1 { customerDefaults | name = "Bill" }
        ...
        ]
Reply all
Reply to author
Forward
0 new messages