nominal vs. structural typing

84 views
Skip to first unread message

Janis Voigtländer

unread,
Aug 29, 2014, 8:12:30 AM8/29/14
to elm-d...@googlegroups.com
This consideration comes out of the other thread, but is more about the general role of "type aliasing" in "type-directed macros".

Say we have:

type AnInt = { i : Int }
type AFloat = { f : Float }

type AddAnInt r = { r | i : Int }
type AddAFloat r = { r | f : Float }

anIntAndAFloat : { i : Int, f : Float}
anIntAndAFloat = ...

Assume you have a mechanism in the compiler that allows to add deriving JSON  or whatever other "type class" to type declarations. A user does that for AddAnInt and AddAFloat. Then they try to use whatever functionality is created by that deriving statement on the value anIntAndAFloat. Note that that value can currently be typed to both AddAnInt AFloat and AddAFloat AnInt. So which implementation of the JSON (or whatever) functionality is picked by the compiler? And how do you guarantee to the user that the behavior of the program does not differ depending on which of the two choices is picked?

This kind of problem does not exist in Haskell, precisely since type there is just creating aliases while data/newtype create really new types, and class instances are only attached to data/newtype-created stuff.

So, is type in Elm *really* just creating an alias, or (in some circumstances, possibly in the future, when you want deriving functionality) actually new types?

Evan Czaplicki

unread,
Aug 29, 2014, 3:33:02 PM8/29/14
to elm-d...@googlegroups.com
I think the key thing is that it's not working the same way as a type class. I tried to write down how I think it should work in this updated design document. The situation you set up does not come up the way I am thinking of this.

To answer the nominal vs structural question, types would still all be structural, but the functions that are derived would use the explicitly stated names as a way to guide the derivation process.


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

Janis Voigtländer

unread,
Aug 29, 2014, 4:00:46 PM8/29/14
to elm-d...@googlegroups.com
Yes, this description makes it clear that it is overall less like type class instantiation than it first appeared. So, if you have


type AnInt = { i : Int }
type AFloat = { f : Float }




type AddAnInt r = { r | i : Int }
type AddAFloat r = { r | f : Float }

anIntAndAFloat : { i : Int, f : Float}
anIntAndAFloat = ...

Janis Voigtländer

unread,
Aug 29, 2014, 4:04:11 PM8/29/14
to elm-d...@googlegroups.com
Sorry, sent that too quickly. What I was trying to say was:

It's indeed not like type class instantiation, since if one has


type AnInt = { i : Int }
type AFloat = { f : Float }

that does give two functions which both are applicable to

anIntAndAFloat : { i : Int, f : Float}
anIntAndAFloat = ...

but the compiler doesn't get to choose which one to apply. Instead, the programmer decides to either write

AnInt.Json.encode anIntAndAFloat

or

AFloat.Json.encode anIntAndAFloat

The outcomes will be different, but it's under the programmer's control.

Janis Voigtländer

unread,
Aug 29, 2014, 4:16:03 PM8/29/14
to elm-d...@googlegroups.com
A small downside, maybe:

What if one has a reuse of the same structural type in several places of another type, like:

enum T = A { i : Int, f : Float} | B { i : Int, f : Float} deriving JSON

Under the scheme as described, this would probably create twice the same code for serializing an i and an f, once for the A variant and once for the B variant.

The only way to avoid this code duplication would seem to be to introduce a shared name explicitly:

type P = { i : Int, f : Float} deriving JSON
enum T = A P | B P deriving JSON

In contrast, with instance resolution a la type classes like in Haskell, no extra precautions by the programmer are needed to ensure that

data T = A (Int, Float) | B (Int, Float) deriving JSON

does not create duplicate code for serializing an (Int, Float) pair. That would be guaranteed automatically, since by the design of the type class mechanism there cannot even exist more than one instance of the type class for (Int, Float).

But probably this issue is not all too dramatic. Replacing

enum T = A { i : Int, f : Float} | B { i : Int, f : Float} deriving JSON

by

type P = { i : Int, f : Float} deriving JSON
enum T = A P | B P deriving JSON

might be a worthwhile refactoring for other reasons anyway.

Evan Czaplicki

unread,
Aug 29, 2014, 4:18:32 PM8/29/14
to elm-d...@googlegroups.com
Yes, exactly! I think some cool things will come from being more value oriented in Elm, and I think the fact that you can serialize a type in many different ways is a nice example of this.

With this proposal, there'd be no need to write a custom Aeson parser, so no monad knowledge is necessary. I think this is another nice part.

Evan Czaplicki

unread,
Aug 29, 2014, 4:21:38 PM8/29/14
to elm-d...@googlegroups.com
As for the potential for extra code generation, it may be possible to have a global conversion table for basic types?

I'm not sure exactly how that'd work in practice, especially if we want to open up the deriving mechanism for user defined macros.

Without having considered this too carefully, it seems plausible to have implementations indexed by TypeRep so that even user-defined macros would not repeat themselves. I think user-defined stuff is way further off though. I'm really not sure what that'd look like.
Reply all
Reply to author
Forward
0 new messages