Records

Now onto creating our own types!

I'm sure you're familiar with structs from C or records from Pascal. They simply group multiple values into a single value. Records in Funky are very much the same.

They're defined like this:

record Name = field : type, ...

For example:

record Person =
    name : String,
    age  : Int,

Note. Trailing comma is allowed.

The Person type is now a new, non-reproductive way of creating people (pretty revolutionary if you think about it). How do we create one?

Whenever we define a record, Funky generates a constructor function with the same name. Let's see!

$ funkycmd -types person.fn
> Person
String -> Int -> Person

Basically, it takes values for the name and the age and gives us a fully fledged person. Great!

record Person =
    name : String,
    age  : Int,

func main : IO =
    let (Person "Joe" 28) \joe
    quit

We've got Joe! How do we use him?

In addition to the constructor function, Funky generates two functions per field: a getter and an updater. Name of both of the functions is the same as the name of the field:

$ funkycmd -types person.fn
> name
Person -> String
(String -> String) -> Person -> Person

The type Person -> String is the getter. It works just as you'd expect:

record Person =
    name : String,
    age  : Int,

func main : IO =
    let (Person "Joe" 28) \joe
    println (name joe);
    quit

Running it:

$ funkycmd person.fn
Joe

The updater with the type (String -> String) -> Person -> Person is a little more... funky.

Here's what it does: it takes a function and a person. It returns a new person that is the same as the old one, except with the field modified by the function.

Let's see:

record Person =
    name : String,
    age  : Int,

func main : IO =
    let (Person "Joe" 28)       \joe
    let (name (++ ", yo!") joe) \joe  # shadows the previous joe variable
    println (name joe);
    let (age (+ 1) joe) \joe          # Joe had a birthday!
    println (string; age joe);
    quit

And run it:

$ funkycmd person.fn
Joe, yo!
29

Note. If you want to replace the field's value by some other value, you can use the const function like this: name (const "Joseph") joe (changes the name to "Joseph"). The function const makes a function that, taking any input, always evaluates to a constant. So, (const "Joseph") 12 evaluates to "Joseph".

Composing getters and updaters

The signatures of the getters and the updaters are not random. They enable some really cool stuff! We'll check it out now.

I'll show it to you on these two records:

record Point =
    x : Int,
    y : Int,

record Line =
    from : Point,
    to   : Point,

Okay, now, say we have a Point, let's call it pt. To get the X coordinate we do x pt and to get the Y coordinate we do y pt. Easy! To move the X coordinate 10 units to the right, we do x (+ 10) pt and to set the Y coordinate to 0 we do y (const 0) pt.

Okay, now say we have Line. Let's call it line. The line has two points: from and to. What if we want to move the X coordinate of the first point by 10?

Now, here comes the cool part! All we need to do it to realize, that

x (+ 10)

is a partial application of the x updater and has type Point -> Point. But, any Point -> Point can be used in the from updater:

from (x (+ 10)) line

Lastly, we can clean it up with the function composition operator:

(from . x) (+ 10) line

Note. The function composition operator . works like this: (f . g) x = f (g x). So you see we've just rewritten the previous expression using this formula.

Details. In Funky, the function composition operator is overloaded a few times in the standard library, namely for when the second function takes multiple arguments. For example, one of the overloaded versions works like this: (f . g) x y = f (g x y). That way, writing (concat . map) works exactly as you'd like to. The type checker can always tell which version fits the context.

And that's it! That's pretty clean, isn't it?

Getters can be composed similarly, except in the opposite order:

(y . to) line

That's the Y coordinate of the last point.

Getters and updaters also have a very important role when it comes to expressing state flow with the State type as we'll cover in the part about expressing imperative algorithms. In short, they enable record fields to act as mutable variables in an imperative algorithm.