A type proposal

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

2 Likes

I would agree with the only thing in your proposal:

I (almost) never use record constructors because they are dependent on the order of fields and therefore error-prone, so I wouldn’t miss them if they are removed. All other parts seem complicated to me and I don’t really understand what problems they solve. :slight_smile:

For example I don’t understand why

const  = Const  | ()
value  = Value  | 12
record = Record | "bar"
tuple  = Tuple  | True False

is better than

const  = Const
value  = Value 12
record = Record { foo = "bar" }
tuple  = Tuple ( True, False )

and why would you want union type tags to have no more than one element.

For the first point : I constantly use Record & Union constructors, to build up wrapping values (like Decoders or Validation). This is called the Applicative pattern, it is used in the Decoder Pipeline for example.

For the second point : In this specific context, the second is more readable, so I’d say better. But it depends on the context. The first code is better for function composition. My point is that you’d be given the choice, but you’re right, I should have given an example with Function composition.

Ok, but didn’t you propose to remove record constructors?

-- record aliases do not generate constructors anymore
type alias NoConstructor =
  { name : String
  , email : String
  }

In the case of writing decoders, if you have the following code

type alias Person =
  { firstName : String
  , lastName : String
  , email : String
  }

personDecoder : Decoder Person
personDecoder =
  decode Person
    |> required "firstName" string
    |> required "lastName" string
    |> required "email" string

and then you change the order of fields in Person, you would introduce a bug (unless you have tests). So I personally think it’s safer to write

personDecoder : Decoder Person
personDecoder =
  decode
    (\firstName lastName email ->
        { firstName = firstName
        , lastName = lastName
        , email = email
        }
    )
    |> required "firstName" string
    |> required "lastName" string
    |> required "email" string

Remove the constructor from aliases only. Then the union types would have it both ways. your example could become :

type Person =
  Person
  { firstName : String
  , lastName : String
  , email : String
  }

personDecoder : Decoder Person
personDecoder =
  Person | decode 
    |> required "firstName" string
    |> required "lastName" string
    |> required "email" string

Doesn’t having multiple elements make them dependent on the order of those elements and therefore error-prone, same as record constructors?

@sebn if elements have different types then no (the same is true for records).

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.