Language design around record types

377 views
Skip to first unread message

Janis Voigtländer

unread,
Mar 10, 2016, 2:38:28 AM3/10/16
to elm...@googlegroups.com

Elm currently has this feature:

type alias B a = { a | b1 : String, b2 : Bool }

and then lets one write B A for “the record type A plus the B-fields”.

But actually using that feature is problematic: https://groups.google.com/d/msg/elm-discuss/AaL8iLjhEdU/JSAXV2oACgAJ

Its availability leads people to want to do things that are not supported and not intended: https://github.com/elm-lang/elm-compiler/issues/1283

Evan has said he knows no examples where using that feature in the above way (creating B A from A) is the best way to model something: https://groups.google.com/d/msg/elm-discuss/AaL8iLjhEdU/pBe29vQdCgAJJ

And yet the feature is in the language, so tempts people into “wrong modelling” etc.

(See also https://github.com/elm-lang/elm-compiler/issues/1308.)

What about weakening the feature in the following way to address these issues but keep uses that are actually intended?

My proposal is to not anymore have syntax { a | b1 : String, b2 : Bool } for “the record type that is a extended with b1 and b2 fields”, but instead have the syntax { _ | b1 : String, b2 : Bool } for “some record type containing b1 and b2 fields”.

I’m seeking answers to the following question:

Does anybody have an example that can be written in Elm 0.16 and that represents a “good” (sanctioned/intended) use of extensible records that could not anymore be written after this proposal is implemented?

As a first sanity check, note that intended/motivated uses like the one present in the docs page would still be supported:

origin =
  { x = 0
  , y = 0
  }

lady =
  { name = "Lois Lane"
  , age = 31
  }

dude =
  { x = 0
  , y = 0
  , name = "Clark Kent"
  , velocity = 42
  , angle = degrees 30
  }

type alias Named =
  { _ | name : String }

getName : Named -> String
getName { name } =
  name

names : List String
names =
  [ getName dude, getName lady ]

type alias Positioned =
  { _ | x : Float, y : Float }

getPos : Positioned -> (Float,Float)
getPos { x, y } =
  (x,y)

positions : List (Float,Float)
positions =
  [ getPos origin, getPos dude ]

Peter Damoc

unread,
Mar 10, 2016, 3:26:56 AM3/10/16
to elm...@googlegroups.com
Janis, 

In your proposal `Named` is a full type, not a type constructor. 
This means that you could conceivably have `List Named`  

But what would this mean in the example bellow 

type alias Widget = 
  { _ | size : (Int, Int), pos : (Int, Int), repr : Html }

type alias Button =
 { pressed : Bool, label : String , size : (Int, Int), pos : (Int, Int), repr : Html }

type alias Label =
 { label : String , size : (Int, Int), pos : (Int, Int), repr : Html }

join : List Widget 
join xs = div [] (List.map .repr xs)
 

could I use join with a list mixed with Buttons and Labels? 
In the current system the answer is simple: No, because Widget would not be a full type and when you add the rest of stuff you end up with different types. 
How do you view the answer in your proposal? 





--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elm-dev/CAGkFuyDBW33ty_xUi9wW4dXm_f_thekj7WitP1T3sJFjM5ajxA%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.



--
There is NO FATE, we are the creators.
blog: http://damoc.ro/

Janis Voigtländer

unread,
Mar 10, 2016, 3:43:19 AM3/10/16
to elm...@googlegroups.com

I assume you meant

join : List Widget -> Html

join xs = div [] (List.map .repr xs)

(not join : List Widget).

The answer to your question is that no, it would not be possible to apply join to a mixed list containing Buttons and Labels. It follows directly from the fact that the type would desugar to

join : List { _ | size : (Int, Int), pos : (Int, Int), repr : Html } -> Html

There’s no intent (and no potential in the implementation of all this that I envisage) for interpreting the _ in two different ways in two different list elements.

More generally, note that the question I asked was:

Does anybody have an example that can be written in Elm 0.16 and that represents a “good” (sanctioned/intended) use of extensible records that could not anymore be written after this proposal is implemented?

I didn’t ask the converse question whether there are examples that cannot be written in Elm 0.16 but could be written after my proposal is implemented. I did not ask this converse question because I know the answer to that question. It is: No.


Janis Voigtländer

unread,
Mar 10, 2016, 4:14:42 AM3/10/16
to elm...@googlegroups.com

Motivated by Peter’s question, let me amend my proposal.

In addition to:

My proposal is to not anymore have syntax { a | b1 : String, b2 : Bool } for “the record type that is a extended with b1 and b2 fields”, but instead have the syntax { _ | b1 : String, b2 : Bool } for “some record type containing b1 and b2 fields”.

there is now also:

  • No _ can occur in a type or type alias declaration.

How the example from the docs page then looks like is given further below.

Let me reiterate that I am not trying to make more examples valid than are currently valid. I am trying to weaken, not strengthen, what one can do with record types. Because what one can currently do with them tempts people into a direction that is undesired (from a conceptual/modelling perspective) and that, if they follow it nevertheless, sometimes leads them into a dead end eventually.

The question still is whether I am weakining the feature too much:

Does anybody have an example that can be written in Elm 0.16 and that represents a “good” (sanctioned/intended) use of extensible records that could not anymore be written after this proposal is implemented?

origin =
  { x = 0

  , y = 0
  }

lady =
  { name = "Lois Lane"
  , age = 31
  }

dude =
  { x = 0
  , y = 0
  , name = "Clark Kent"
  , velocity = 42
  , angle = degrees 30
  }

getName : { _ | name : String } -> String

getName { name } =
  name

names : List String
names =
  [ getName dude, getName lady ]

getPos : { _ | x : Float, y : Float } -> (Float,Float)

Daniel Bachler

unread,
Mar 10, 2016, 6:11:03 AM3/10/16
to elm-dev
I think I have a counter example for your amended proposal that seems valid (to me at least :) ). I have two elm programs that share a lot of code - a Slideshow Editor and a Viewer. Their model overlaps to a certain degree but also differs significantly. For this use case, I currenently use extensible records, like so:

type alias PlayerViewModel a =
 
{ a
   
| playStatus : PlayStatus
   
, currentTimeAtPlayStart : Time.Time
 
}

and there are several functions that operate on any "PlayerViewModel a" that can thus be used both with the Editors Model as well as the Viewers. If I understand your amended proposal correctly, I could no longer type alias extensible records. This would make the signatures of these functions quite unwieldy IMHO.

Janis Voigtländer

unread,
Mar 10, 2016, 6:47:58 AM3/10/16
to elm...@googlegroups.com

You could model your situation as follows:

type alias PlayerViewModel a =

  { base : a
  , playStatus : PlayStatus
  , currentTimeAtPlayStart : Time.Time
  }

That would give you all the advantages (abstraction, aliasing, short signatures) that you describe you have with your current modelling, right?

Evan has stated that he hasn’t seen any example where record nesting is not better than record extension.

If your example is a counter-example to my proposal, it is also a counter-example of the kind that Evan asked for in that other thread (with consequences for the dicussion topic over there). In other words, the current conclusion of that other thread seems to imply that your example is actually a not-desirable use case. Which in turn would imply that my proposal making it invalid would be a good thing, not a bad thing.


--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

Janis Voigtländer

unread,
Mar 10, 2016, 7:09:17 AM3/10/16
to elm...@googlegroups.com

Yet another alternative (maybe better than the one from my previous message) that would also be compatible with my proposal would be:

Don’t have the PlayerViewModel alias.

Have:

type alias Playing = 
  { playStatus : PlayStatus
  , currentTimeAtPlayStart : Time.Time
  }

Let the models of the Slideshow Editor and Viewer contain a field playing : Playing instead of two fields playStatus : PlayStatus and currentTimeAtPlayStart : Time.Time.

Where previously you had a signature like

fun : PlayerViewModel a -> ...

now have a signature like

fun : { _ | playing : Playing } -> ...

What would be downsides of this approach compared to your current one? Do you have signatures in your project that couldn’t be handled nicely that way?

Daniel Bachler

unread,
Mar 10, 2016, 7:41:32 AM3/10/16
to elm-dev
Yes you are right, I could model it this way. I wasn't aware composition was favoured so clearly by Evan. The main thing that makes composition annoying is nested updates - this acutally works nicer with extensible types ATM because you just update field in the record directly. If we make extensible records less powerful, I would argue that the following should be made possible to aid with updating deeply nested models:

{ model.playing
| playStatus = Stopped --assuming PlayStatus to be a simple Union of Stopped | Playing
}



or, if playStatus where a record itself:

{ model.playing.playStatus
| isPlaying = False --assuming PlayStatus to have a Bool field isPlaying
}



I am aware that this is orthogonal to your initial suggestion, but I feel that if I were to switch from extensible records to record composition for all cases I would really want a nicer nested update syntax.

Max Goldstein

unread,
Mar 25, 2016, 11:27:16 AM3/25/16
to elm-dev
Here is another reason to support this proposal. Do you see the problem with the type annotation below?

planarDistance : { a | x : Float, y : Float} -> { a | x : Float, y : Float} -> Float

The annotation is not as general as it could be. Both records are extended from a which means that if you pass a 2D point and a 3D point it won't compile. The fully general defintion is to replace one a with b. I don't know of any good reason to constrain the record fields you don't care about to be the same, i.e. a reason to write the less general annotation intentionally. Using underscores solves this problem (or ought to).

Regarding Peter's objection and the amended proposal: I don't think Peter is correct and I find the second restriction harmful. Consider xs = [ { x = 2}, { x = 3, y = 1} ]. There is no way get this to type-check. Even though you could write xs : List { a | x : number}, it won't work. If you aliased the record and used the alias, it won't work. Even if you removed the y field to make the list without an annotation valid, you can't annotate the list with { a | syntax.

Although a function can take a List Widget, there is no way to construct such a list. Any particular call to the function will take Buttons or Labels.

Now consider this defintion: type alias Named a = { a | first : String, last : String }. It's reasonable to want to write functions that act on named things. fullName : Named a -> String for example. With the type alias and underscore restriction, I can no longer alias "the type of a record with at least these fields"; I have to write that type exactly every time. Which is a pity, since otherwise I could write the even more concise annotation Named -> String i.e. dropping the type variable.

I think this proposal as originally submitted makes a lot of sense. It also removes the concern of not having record constructors for { a | style records. Or if you like, it's now more sensible to have Named : String -> String -> Named, i.e. the smallest Named you can have.

Joey Eremondi

unread,
Mar 25, 2016, 12:52:58 PM3/25/16
to elm-dev

Wouldn't there be cases where you want to specify that the extended type is the same, especially for the return value? Like, if you have a function that sets the x coordinate to 0, you want a guarantee that it doesn't change the number of fields. But it's also something you'd want to work for 2d and 3d vectors polymorphically.

--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.

Janis Voigtländer

unread,
Mar 25, 2016, 4:05:03 PM3/25/16
to elm...@googlegroups.com

Joey’s example convinces me that my proposal is not viable.

We want to be able to give a meaningful type to this function:

setXtoZero rec = { rec | x = 0.0 }

(Incidentally, the type Elm currently infers for this function is not the one it should be according to the specification of “there’s no record field extension or deletion in the expression syntax”.)


Max Goldstein

unread,
Mar 25, 2016, 5:15:14 PM3/25/16
to elm-dev
A pity things didn't work out.

Expanding on the parenthetical remark for the benefit of others:

> setXtoZero rec = { rec | x = 0.0 }

<function> : { b | x : a } -> { b | x : Float }

> setXtoZero { x = "huh?" }

{ x = 0 } : { x : Float }


The compiler correctly constrains the unspecified fields to be the same type, b. (So now I see where that is desirable; contrast my previous post.) However it should restrict the type of x. Janis should open an issue.

Janis Voigtländer

unread,
Mar 25, 2016, 5:47:08 PM3/25/16
to elm...@googlegroups.com

--
You received this message because you are subscribed to the Google Groups "elm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-dev+u...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages