Chapter 4: Functions and Definitions

Where you will learn how to build your own Lux code.


OK, so you've seen several explanations and details so far, but you haven't really seen how to make use of all of this information.

No worries. You're about to find out.

First, let's talk about how to make your own functions.

(lambda [x] (i.* x x))

Here's the first example. This humble function multiplies an Int by itself. You may have heard of it before; it's called the square function.

What is it's type?

Well, I'm glad you asked.

(: (-> Int Int) (lambda [x] (i.* x x)))

That -> thingie you see there is a macro for generating function types. It works like this:

(-> arg-1T arg-2T ... arg-nT returnT)

The types of the arguments and the return type can be any type you want (even other function types, but more on that later).

How do we use our function? Just put it at the beginning for a form:

((lambda [x] (i.* x x)) 5)
=> 25

Cool, but... inconvenient.

It would be awful to have to use functions that way.

How do we use the square function without having to inline its definition (kinda like the log! function we used previously)?

Well, we just need to define it!

(def: square
  (lambda [x]
    (i.* x x)))

Or, alternatively:

(def: square
  (-> Int Int)
  (lambda [x]
    (i.* x x)))

Notice how the def: macro can take the type of its value before the value itself, so we don't need to wrap it in the type-annotation : macro.

Now, we can use the square function more conveniently.

(square 7)
=> 49

Nice!

Also, I forgot to mention another form of the def: macro which is even more convenient:

(def: (square x)
  (-> Int Int)
  (i.* x x))

The def: macro is very versatile, and it allows us to define constants and functions.

If you omit the type, the compiler will try to infer it for you, and you'll get an error if there are any ambiguities.

You'll also get an error if you add the types but there's something funny with your code and things don't match up.

Error messages keep improving on each release, but in general you'll be getting the file, line and column on which an error occurs, and if it's a type-checking error, you'll usually get the type that was expected and the actual type of the offending expression... in multiple levels, as the type-checker analyses things in several steps. That way, you can figure out what's going on by seeing the more localized error alongside the more general, larger-scope error.


Functions, of course, can take more than one argument, and you can even refer to a function within its own body (also known as recursion).

Check this one out:

(def: (factorial' acc n)
  (-> Nat Nat Nat)
  (if (n.= +0 n)
    acc
    (factorial' (n.* n acc) (n.dec n))))

(def: (factorial n)
  (-> Nat Nat)
  (factorial' +1 n))

And if we just had the function expression itself, it would look like this:

(lambda factorial' [acc n]
  (if (n.= +0 n)
    acc
    (factorial' (n.* n acc) (n.dec n))))

Yep. Lambda expressions can have optional names.

Here, we're defining the factorial function by counting down on the input and multiplying some accumulated value on each step. We're using an intermediary function factorial' to have access to an accumulator for keeping the in-transit output value, and we're using an if expression (one of the many macros in the lux module) coupled with a recursive call to iterate until the input is 0 and we can just return the accumulated value.

As it is (hopefully) easy to see, the if expression takes a test as it's first argument, a "then" expression as it's second argument, and an "else" expression as it's third argument.

Both the n.= and the n.* functions operate on nats, and n.dec is a function for decreasing nats; that is, to subtract +1 from the nat.

You might be wondering what's up with those i. and n. prefixes.

The reason they exist is that Lux math functions are not polymorphic on the numeric types, and so there are similar functions for each type, with different prefixes to distinguish them (in particular, n. for nats, i. for ints, r. for reals, and f. for fracs).

I know it looks annoying, but later in the book you'll discover a way to do math on any Lux number without having to worry about types and prefixes.

Also, it might be good to explain that Lux functions can be partially applied. This means that if a function takes N arguments, and you give it M arguments, where M < N, then instead of getting a compilation error, you'll just get a new function that takes the remaining arguments and then runs as expected.

That means, our factorial function could have been implemented like this:

(def: factorial
  (-> Nat Nat)
  (factorial' +1))

Or, to make it shorter:

(def: factorial (factorial' +1))

Nice, huh?

You might be wondering why the function-definition macro is called lambda, instead of function or fun or fn.

The reason is mostly historical: older lisps named their function-definition macros lambda, plus the theoretical foundation of both lisps and functional programming is the Lambda Calculus, so it just felt more natural to call it lambda.


We've seen how to make our own definitions, which are the fundamental components in Lux programs.

We've also seen how to make functions, which is how you make your programs do things.

Next, we'll make things more interesting, with branching, loops and pattern-matching!

See you in the next chapter!

results matching ""

    No results matching ""