Unions

The second way of creating new types is unions. While a record is a collection of fields, a union defines multiple alternative forms.

Here's the general form:

union Name = alternative argument ... | ...

The three dots after argument mean that a single alternative can contain any number of arguments (fields) and the three dots after | mean that a union can have any number of alternatives.

Unlike records, arguments for alternatives have no names, only types.

Here's a simple union:

union Bool = true | false

This is the actual definition of the type Bool from the standard library. The alternatives have no arguments in this case.

Here's another example:

record Point = x : Float, y : Float

union Shape =
    line Point Point   |
    circle Point Float |

Note. Trailing | is allowed.

The Shape type has two alternatives: the line alternatives, which is composed of two points, and the circle alternatives, which has a point (the center) and a float (the radius).

When we define a union, Funky generates a constructor function for each alternative. The function simply takes all the arguments in order. Let's check them out!

$ funkycmd -types shapes.fn
> line
Point -> Point -> Shape
> circle
Point -> Float -> Shape
> line (Point 1.0 8.0) (Point 12.5 -4.9)
Shape
> circle (Point 0.0 0.0) 7.0
Shape

They definitely work!

To examine a Shape value and make a decision based on whether it's a line or a circle, Funky provides the switch/case construct. It's best shown by an example. Here's a function that calculates the length of a line or a circumference of a circle:

func length : Shape -> Float =
    \shape
    switch shape
    case line \from \to
        hypot (x to - x from) (y to - y from)
    case circle \center \radius
        2.0 * pi * radius

Note. The hypot function stands for 'hypotenuse' and is used for calculating the length of the long side of a right triangle using the Pythagorean theorem: hypot x y = sqrt ((x ^ 2.0) + (y ^ 2.0)).

As you can see, the switch/case construct begins with the switch keyword followed by the value we're switching on. After that, we see a series of case branches. Each branch starts with the case keyword followed by the name of the alternative. That is followed by a function that takes the arguments to the alternative and evaluates to the overall result.

Of course, all case branches must evaluate to the same type: Float, in our case.

Note. Currently, all alternatives must be present in switch/case and they must be listed in the same order as they are in the definition of the union. This will be fixed.

The names of the alternatives can also be infix if they don't contain any letters. When they are infix, they can be placed between their arguments in the definition.

For example, here's a nice, recursive union for building up arithmetic expressions:

union Expr =
    num Int     |
    Expr + Expr |
    Expr * Expr |

It's either just a number, like num 12, or it's an addition, or a multiplication: num 12 + num 7, or (num 1 + num 9) * ((num 4 + num 3) + (num 2 * num 2)).

Note. The alternatives can be called + and * without a problem thanks to overloading.

To evaluate an expression like that, we make use of switch/case again:

func eval : Expr -> Int =
    \expr
    switch expr
    case num
        self
    case (+) \left \right
        eval left + eval right
    case (*) \left \right
        eval left * eval right

Have you noticed that no lambda comes after case num? That's because the case bodies need not explicitly contain lambdas, all they need is a function. In this case, self (the identity function) is the right choice because it just evaluates to the number under num.

Let's see if eval works right!

union Expr =
    num Int     |
    Expr + Expr |
    Expr * Expr |

func eval : Expr -> Int =
    \expr
    switch expr
    case num
        self
    case (+) \left \right
        eval left + eval right
    case (*) \left \right
        eval left * eval right

func main : IO =
    let ((num 1 + num 9) * ((num 4 + num 3) + (num 2 * num 2))) \expr
    println (string; eval expr);
    quit

And run it:

$ funkycmd expr.fn
110

Works great!

Type variables in types

So far we've only defined types that were concrete, with no type variables anywhere. But it's also possible (and very useful) to define generic types: types parameterized by one or more type variables.

It's really simple.

All we need to do is to list the type variables after the name of the type in the definition:

record Pair a b =
    first  : a,
    second : b,

That's an actual definition of the Pair type from the standard library.

Or here's another example:

union Maybe a = none | some a

That is, likewise, the definition of the Maybe type from the standard library.

This way, Pair and Maybe don't define a single type each. Instead, each defines a family of types: Pair Int Float, Pair String Bool, Maybe (String -> String), Maybe (Pair Int Int) all belong to those families.

Note. 'Type family' is not a concept in Funky, it's just a phrase we use to talk about types. Pair without its type variables filled in is always an error.