1.2 Hackett Essentials
Before leaping into Hackett’s language features, this section will establish some essential concepts and terminology. Readers already familiar with both Scheme and Haskell can safely skip this section, but it can be a useful overview.
Hackett, like most programming languages, is a language for manipulation of values. A value is anything that exists at runtime, like a number, a string, a list, or a function. Every valid Hackett value also has a type, which can be thought of as a “description” of the value. When an expression is evaluated in the Hackett REPL, the value’s type will be printed just before the value itself:
> 42
: Integer
42
> "Hello, world!"
: String
"Hello, world!"
Generally, a single type describes many values, sometimes infinitely many! Hackett can represent any integer that will fit in memory, and all of them have the Integer type. Similarly, there are infinitely many possible arrangements of characters, and all of them have the String type.
In Hackett, types are exclusively a compile-time concept; they never persist at runtime. After a program has passed the typechecker, type information is thrown away; this process is known as type erasure. It is not possible to dynamically query the type of a value at runtime, since that information simply does not exist. Type erasure is possible because any program that would incorrectly use a value as the wrong type will be detected and prevented by the typechecker; programs that pass the typechecking process are considered well-typed.
Hackett programs are built out of series of nested function applications, which have the following syntax:
(function-expr arg-expr) Examples:
The above syntax applies function-expr to arg-expr, evaluating to the function’s body. As mentioned in Quick Start, Hackett functions are curried, which means they only ever take a single argument, but multi-argument functions are simulated by functions that return other functions. To make curried functions more pleasant to work with, function application syntax actually accepts more than one argument at a time:
(function-expr arg-expr ...+) Example:
> (+ 1 2)
: Integer
3
This syntax will be translated into a sequence of nested function applications, each of which only involves application of a single arg-expr at a time.
In certain locations in Hackett programs, such as when providing a type annotation using :, the programmer is expected to specify a type rather than a value. The syntax of types is similar to the syntax of values, but be careful to never confuse the two: remember that types are evaluated at compile-time, and they will never mix with runtime values, they simply describe them.
The simplest types are just names. For example, Integer, Bool, and String are all types. These can be successfully used anywhere a type is expected:
> (: 42 Integer)
: Integer
42
> (: False Bool)
: Bool
False
Some types, however, are more complex. For example, consider the type of a list. It would be silly to have many different types for all the different sorts of list one might need—that would require completely separate types for things like Integer-List, Bool-List, and String-List. Instead, there is only a single List type, but List is not actually a type on its own. Rather, List is combined with another type to produce a new type, such as (List Integer) or (List String).
This means that List isn’t really a type, since types describe values, and List is not a valid type on its own. Instead, List is known as a type constructor, which can be applied to other types to produce a type.
1.2.1 Infix Syntax
Hackett supports a limited form of infix syntax, which allows binary functions (that is, functions that accept two arguments) to be applied by placing the function between its two operands, rather than before them as in the usual prefix notation generally used by Hackett. This means that a function application of the following form:
( ‹function expr› ‹arg expr› ‹arg expr› )
…can be equivalently written in an alternate form:
{ ‹arg expr› ‹function expr› ‹arg expr› }
Note the curly braces ({}), which are significant in Hackett. When used as expressions, parentheses and curly braces are not interchangeable. Use of curly braces in an expression enters infix mode, which alters function application syntax to support infix syntax.
Infix syntax is most useful for presenting mathematical notation, which is traditionally written using infix symbolic operators. Hackett’s infix syntax can emulate this:
> {1 + 2}
: Integer
3
> {2 * 3}
: Integer
6
Any function of arity two can be applied using infix syntax, even those defined as entirely normal functions; there is no syntactic difference between an “operator” and any other function. For example, it would be equally possible to use a function named add in an infix expression:
> (def add +) > {1 add 2}
: Integer
3
In fact, there is not even any restriction that functions used in infix expressions must be identifiers. Arbitrary expressions that produce functions may also be used infix:
> {1 (λ [x _] x) 2}
: Integer
1
Infix syntax can also be used to chain multiple operators together in the same expression, so the general syntax of infix mode can be described with the following grammar:
{ ‹arg expr› {‹function expr› ‹arg expr›}+ }
…where each ‹function expr› is known as an infix operator.
Astute readers might notice that operators chained in this way create a minor ambiguity. Is {x f y g z} grouped like this?
(g (f x y) z)
…or like this?
(f x (g y z))
Both interpretations are potentially reasonable. For operators like +, the grouping does not matter, because + is associative, so the result will be the same whichever grouping is picked. For other operators, however, the grouping is meaningful. For example, - can produce very different results depending on which grouping is picked:
> {{10 - 15} - 6}
: Integer
-11
> {10 - {15 - 6}}
: Integer
1
How does Hackett determine which grouping to use? Well, it uses a notion of operator fixity to decide on a case-by-case basis. Some operators should be grouped the first way (they “associate left”) while others should be grouped the second way (they “associate right”). The - operator, for example, associates left, while the :: operator associates right:
> {10 - 15 - 6}
: Integer
-11
> {1 :: 2 :: 3 :: Nil}
: (List Integer)
{1 :: 2 :: 3 :: Nil}
Operator fixity can be specified when a binding is defined by providing a fixity annotation, which is either #:fixity left or #:fixity right. Using a fixity annotation, it is possible to write a version of - that associates right:
> (def -/r #:fixity right -) > {10 -/r 15 -/r 6}
: Integer
1
If no fixity annotation is specified, the default fixity is left.
Additionally, infix syntax can be used in types as well as expressions, and it works the same way. Type constructors may also have operator fixity, most notably ->, which associates right. This makes writing type signatures for curried functions much more palatable, since {a -> b -> c} tends to be easier to visually scan than (-> a (-> b c)), especially when the argument types are long or function types are nested in argument positions.