The empty parentheses ()
are what's known as a unit type, that is, a type that can only ever have a single value. A tuple type with at least one item can have any number of values (for example, a type defined as a tuple of one Int
could have an infinite number of values, from (-∞)
to (+∞)
). But how many possible values of "the empty tuple" are there? Just one, hence why it's called a unit type.
The value of a unit type is that you can use it in places where other languages would have a null
or void
type, while avoiding the problems that null
carries with it. For example, as you've noticed, it's often used in places where you want to say "I don't care what the value is". But there is a value there; it's not the absence of a value.
This has many advantages; for example, you can say "ALL functions return a value", and this will be true. Sometimes the value is one you don't care about, but all functions will return a value. In languages with a void
type, you can't say that. For example, C# has two different ways to declare a function delegate: Func<T>
for a function returning type T
, and Action
for a function returning void
. In Elm (and F#, and other functional languages that use a unit type), there's no need for that distinction: all functions return a value, so all functions could be treated as the equivalent of Func<T>
. Sometimes type T
is the empty tuple, but it means that you don't have to write two versions of, say, map
.
Another advantage is that it lets you compose functions more easily. Here's an example taken from the Wikipedia page on unit types. In C, you can't do this:
void f(void) {}
void g(void) {}
int main(void)
{
f(g()); // compile-time error here
return 0;
}
This is an error, because you're not allowed to pass void
as a parameter: it's the absence of a value. Which means that you can't use standard function composition to chain functions f
and g
together. But here's another snippet from the same article, of C++ this time, showing what it would look like if you gave C++ an Elm-like unit type:
class unit_type {};
const unit_type the_unit;
unit_type f(unit_type) { return the_unit; }
unit_type g(unit_type) { return the_unit; }
int main()
{
f(g(the_unit));
return 0;
}
Now, because g
returns a real value (even though it's one you don't care about), you can compose f
and g
meaningfully. This makes your program more elegant and easy to read in the long run.
Some further reading that might help you:
- https://en.wikipedia.org/wiki/Unit_type (which I've already linked once in this answer)
- Why do I need to use the unit type in F# if it supports the void type?
Hope this helps!