On this page:
1.1.1 Simple Definitions

1.1 Quick Start

The easiest way to get started with Hackett is by experimenting in the REPL. Using DrRacket, you can quickly get a REPL by writing #lang hackett at the top of the definitions window and pressing the Run button. Alternatively, you can start a REPL from the command-line by running the following command:

  racket -iI hackett

Once you have a REPL started, you can try evaluating some simple expressions:

> 3

: Integer

3

> True

: Bool

True

Note that the result is printed out, such as 3, but so is the type, such as Integer. In Hackett, all valid expressions have a type, and the type can usually be inferred by the typechecker.

The above expressions were very simple, just simple constants, so they are immediately returned without any additional evaluation. Calling some functions is slightly more interesting:

> (+ 1 2)

: Integer

3

> (not True)

: Bool

False

In Hackett, like any other Lisp, function calls are syntactically represented by surrounding subexpressions with parentheses. In any expression (f x y z), f is a function expression to apply, and x, y, and z are arguments that will be passed to the function.

So, what is a function? Well, a function is any value with a function type. We could try to look at the type of not by evaluating it in the REPL, but that will produce an error, since functions aren’t printable:

> not

eval:2:0: typechecker: could not deduce (Show {Bool ->

Bool})

  in: (@%app1 show not2)

However, we can ask Hackett to only print the type of an expression by wrapping it with (#:type expr), which will allow us to inspect the type of not:

> (#:type not)

: {Bool -> Bool}

The type of not is a function type, which is represented by ->. The type can be read as “a function that takes a Bool and produces (or returns) a Bool”. If you attempt to apply something that is not a function, like 3, the typechecker will reject the expression as ill-typed:

> (3 True)

eval:2:0: True: cannot apply expression of type Integer to

expression True

  in: True

The type of + is slightly more complicated:

> (#:type +)

: {Integer -> Integer -> Integer}

This type has two -> constructors in it, and it actually represents a function that returns another function. This is because all functions in Hackett are curried—that is, all functions actually only take a single argument, and multi-argument functions are simulated by writing functions that return other functions.

To make this easier to understand, it may be helpful to observe the following expressions and their types:

> (#:type +)

: {Integer -> Integer -> Integer}

> (#:type (+ 1))

: {Integer -> Integer}

> ((+ 1) 2)

: Integer

3

This technique of representing multi-argument functions with single-argument functions scales to any finite number of arguments, and it aids reuse and function composition by simplifying function types and making it easy to partially apply functions.

Remember that, although + is curried, it was possible to successfully evaluate (+ 1 2), which produces the same result as ((+ 1) 2). This is because, in Hackett, (f x y) is automatically translated to ((f x) y). The same pattern of nesting also applies to any number of arguments greater than two. This makes applying multi-argument functions considerably more palatable, as otherwise the number of parentheses required by nested applications would be difficult to visually parse.

1.1.1 Simple Definitions

Simply evaluating expressions is not terribly exciting. For any practical program it is necessary to be able to write your own definitions. A binding can be defined with the def form:

> (def x 5)
> (* x x)

: Integer

25

All bindings in Hackett are immutable: once something has been defined, its value cannot be changed. This may sound like a severe limitation, but it is not as austere as you might think. In practice, it is not only possible but often pleasant to write entire programs without mutable variables.

Definitions with def are simple enough, but it is much more interesting to define functions. This can be accomplished using the similar defn form:

Those familiar with languages with first-class functions may find this distinction between def and defn unsatisfying. Indeed, defn is just a shorthand for a useful, common combination of def, lambda, and case*. Using defn to define functions is, however, much preferred.

> (defn square
    [[x] (* x x)])
> (square 5)

: Integer

25

This defines a one-argument function called square, which (unsurprisingly) squares its argument. Notably, we did not provide a type signature for square, but its type was still successfully inferred. We can see the inferred type by evaluating it in the REPL:

> (#:type square)

: {Integer -> Integer}

Even though type signatures are not usually required, it’s generally a good idea to provide type annotations for all top-level definitions. Even when the types can be inferred, adding explicit signatures to top-level bindings helps to produce much more understandable type errors, and they can serve as extremely useful documentation to people reading the code.

It’s possible to add a type signature to any definition by placing a type annotation after its name:

> (defn square : (-> Integer Integer)
    [[x] (* x x)])
> (square 5)

: Integer

25

This definition is equivalent to the previous definition of square, but its type is validated by the typechecker. If a type annotation is provided, but the expression does not actually have the expected type, the typechecker will raise an error at compile time:

> (def x : Integer "not an integer")

eval:2:0: typechecker: type mismatch

  between: String

      and: Integer

  in: "not an integer"