Sorry for all the edits. I’m done now!
It has always bothered me that record aliases generate a Constructor, while all the other aliases do not. If we take Union types into account, it means that some Constructors are {x,y,z} -> type
, where others are x -> y -> z -> type
, and it doesn’t feel entirely consistent. Overall, I think that the entire type story can be improved with few changes.
The current situation
type alias Tuple =
( String, Int)
Tuple : ø
type alias Record =
{ name : String
, age : Int
}
Record : String -> Int -> Record
-- But with union types
type MyType
= Record { name : String, age : Int }
| Multi String Int
| Tuple (String, Int)
Record : { name : String, age : Int } -> MyType
Multi : String -> Int -> MyType
Tuple : (String, Int) -> MyType
I think it’s hard to draw a clear picture of the types and constructors, their relations, in this context. We should be able to choose wether we want to expand the parameters to a larger function, or just giving the value!
A type proposal
-- regular aliases are unchanged
type alias MaybeFloat = Maybe Float
-- record aliases do not generate constructors anymore
type alias NoConstructor =
{ name : String
, email : String
}
-- union types are different in two ways:
-- - Records and Tuples have unified `Constructors` and `Taggers`
-- - Union types have UP TO ONE member
type MyType a
= Const
| Value a
| Record { foo : String }
| Tuple (a, a)
type Enums
= Enum1
| Enum2
The true changes lay in there:
-- Type Taggers:
Const : MyType a
Value : a -> MyType a
Record : { foo: String } -> MyType a
Tuple : (a, a) -> MyType a
-- Type Constructors:
(|) Const : () -> MyType a
(|) Value : a -> MyType a
(|) Record : String -> MyType a
(|) Tuple : a -> a -> MyType a
-- Examples:
const = Const | () == Const
value = Value | 12 == Value 12
record = Record | "bar" == Record { foo: "bar" }
tuple = Tuple | True False == Tuple (True, False)
Variant
I feel obliged to mention that in the process, I dismissed a variant because it was even more breaking the current state. I also admit that I enjoyed myself a little too much with the syntax, but I just think it looks slick!
MaybeFloat = Maybe Float
NoConstructor =
{ name : String
, email : String
}
MyType a
| Const
| Value a
| Record { foo : String }
| Tuple (a, a)
Enums
| Enum1
| Enum2
Then the main difference is that Taggers
are the one called with the pipe, while Constructors
are the default.
-- Type Constructors are always expanded
Const : MyType a
Value : a -> MyType a
Record : String -> MyType a
Tuple : a -> a -> MyType a
-- Type Taggers always have 1 param
(|) Const : () -> MyType a
(|) Value : a -> MyType a
(|) Record : { foo: String } -> MyType a
(|) Tuple : (a, a) -> MyType a
Finaly, some syntactic sugar:
Single a
-- equivalent to:
Single a
| Single a
Simple
{ value: Single }
-- equivalent to:
Simple
| Simple { value: Single }
Paire
(Int, Int)
-- equivalent to:
Paire
| Paire (Int, Int)
Disclaimer
I love elm, and I have been enjoying it a lot for a few years (I still do). However, I do find it hard to give feedback without feeling bad about how to present it or overstepping. As a result, it feels that some of my concerns that I’ve for quite some time were not addressed. I don’t blame anyone but me, I don’t claim that this proposal is perfect, it probably has many holes in its design, but this as better feedback as I was able to come up with. I just truly hope to reach out in the community, see if others share the same opinions.
I really hope I’m not overstepping, and can’t wait for your feedback