Unit type purpose

Why does Elm have the unit type/empty tuple ()? Can’t we just use empty record {} instead?

2 Likes

http://faq.elm-community.org/#what-does--mean

And yes, you can use {} instead of (). One is empty tuple another one is empty record.

Interestingly, it is allowed to use () to fill an extensible record.

type alias Field a =
  {a|field : Bool }

type alias MyRecord =
  Field ()

myRecord : MyRecord
myRecord =
    {field = True}

This would have been my #1 reason why {} exists.

Also, the unit type is more common than the empty record, so it would make more sense to remove {}.

I don’t think that’s an argument for removing the empty record rather than the empty tuple. From a type theory perspective, the unit type is exactly the same as the empty tuple is exactly the same as the empty record.

I don’t actually see the purpose of having anonymous tuples in elm at all.

Case expressions where you depend on more than one value. For example:

case (isDriving, isDrunk) of
    ( True, True) ->
            Crime
    _ ->
            NotACrime

Of course in this silly example you would just use && but there are definitely times when I need to check 2 values in the same case expression.

Though you could just allow multiple expressions and patterns separated by commas in exactly this position, rather than tuples more generally. Alternatively if you could pattern match on record values such as:

case { isDriving = isDriving, isDrunk = isDrunk } of
      { isDriving = True, isDrunk = True } ->
           ....

I also use tuples when defining 2/3 values simultaneously in a let binding, but I’m sure I could use records for that if I had to.

You can use custom types to do the same thing

type MyTuple a b = MyTuple a b

a = 
    case MyTuple isDriving isDrunk of 
        MyTuple True True -> 
            Crime
        _ -> 
            NotACrime

In fact there is a package that already does this for you toop 1.0.1

2 Likes

For what it’s worth, I find it elegant that you can model something starting with an empty record and then add to it one by one. I would find it a little clunky if {} was invalid.

For example, when I start a new app I will sometimes get it compiling with

type alias Model = {}

init flags = ({}, Cmd.none)

That gives a nice place to iterate from.

Regarding (), I think there is value to having a type that is known to have the semantics of representing nothing. Same with Never. You could always define Never and Unit on your own, since they don’t have APIs (unlike Maybe, Result, etc.). However, it seems valuable to have a standard type that can be used across codebases and understood without looking up the definition.

That said, () could just as well be replaced by type Unit = Unit defined in Basics so it’s always available. It would have the same usability, while being more descriptive possibly (or easier to search for at least), and it would require less special case syntax and parsing logic. I also think it’s reasonable in its current state.

2 Likes

Ah good point, I hadn’t thought of that approach.

In a similar way to finding {} clunky, I also find it a slight anomaly that:
I’ve always found it to be something of an anomoly (not just in Elm but most functional languages) that you have this progression:

  • () a tuple of length zero (or the unit value).
  • (1) not a tuple at all
  • (1,2) a tuple of length two.
  • (1,2,3) a tuple of length three.

I realise that that’s only because I’m calling () a tuple of length zero rather than ‘unit’.

Another important use of tuples is as keys for Dicts. They require comparable which only (some) basic types and tuples have. Right now, you can at least create Dicts for custom types by turning their fields into components of a tuple. If tuples were removed, the only option would be to turn them into Strings which seems stupid to me.

2 Likes

@eike That’s a good point since it’s something that actually can’t be done without tuples in current elm.

Interestingly, elm lets you define dictionaries with keys that are not comparable, but it doesn’t let you get or insert on those objects, which makes them useless.

Good point. Though records could be added to comparable. I think due to extensible records it might be tricky, but I don’t know.

Sure. Custom types could be comparable, too (order first by constructor, either in defined or in alphabetical order, then by the fields similar to tuples).

I think the problem is that it’s harder to say what the order should be: For a given record, you want to order the fields in some way and then sort lexicographically (with the first field having the highest priority, just like the first entry in a tuple takes precedence). Now, { a : Int, b : String } and { b: String, a : Int } are the same record type, so you wouldn’t want to sort the fields by source order. The only reasonable alternative is to order them alphabetically by their name, which might give strange results (for example, a record { lastName : String, firstName : String } would be ordered by first names first, although in many circumstances you might to order them by last name).

This problem is amplified by the fact that currently, the < operator is bound to comparable as well and it somehow demands a “natural” order.

From a purely theoretic point of view, I think there should be (at least) two versions of comparable: One expressing some kind of “natural” order, which would only be implemented for types were something like this exists, and a second one that serves as an “arbitrary” order for purposes of efficient algorithms and data structures, which would be implemented for all (or most) types. One might want to require that a type with a natural order also uses that as its arbitrary order, to reduce confusion. (Counterpoint: The natural order for some types might be expensive to compute, so being able to choose a different arbitrary order might be a plus.) It might also be possible to replace the second by hashable, although the data structures have slightly different trade-offs in that case.

2 Likes

Yeah all good points.

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