Curry Syntax

Curry’s syntax is nearly identical to Haskell’s. This section provides a very brief introduction that may help the unfamiliar get started. The purpose is to provide enough information to understand this document, plus the tests and examples that come with Sprite.

Built-in Data Types

Curry provides the following three fundamental types:

  • Int: Integer literals are written as one expects, e.g., 0 or 1, though negative integers are enclosed with parentheses, as in (-1). Syntactically, negative integers are simply negated positive integers.

  • Char: Character literals are enclosed in single quotes, as in 'a' or 'A'. Unicode and other escape sequences are recognized, as in some other programming languages. So, for example, '\0' is the character with numeric code zero, usually referred to a NULL.

  • Float: Floating-point literals are written in the customary way; 1.0, 1. or 3.14159265, for example.

Many other built-in types are provided by Curry. Among these is a type Bool with constructors True and False. Curry also provides built-in lists and tuples. Lists are writen in one of two ways. A complete list may be witten enclosed in square brackets, as in [1, 2]. The empty list is written []. Alternatively, a nonempty list may be written in constructor notation, as h:t, where h is the head element and t is another list called the tail. Tuples are written enclosed within parentheses, as in ('a', 1). Curry has no 1-tuple.

User-Defined Data Types

Data types are defined with the data keyword. For example, a type representing a string of binary digits might be written as:

data Binary = O Binary | I Binary | Null

Binary has three constructors, namely O, I, and Null. These represent the binary digits 0 and 1, and the end of the string, respectively. O and I each take one argument of type Binary, whereas Null takes none. Note that the self-reference makes Binary a recursive type, and that a binary string of any length can be written. To create an instance, expressions of type Binary expressions are juxtaposed. A few instances of this type are as follows: Null, I Null, I (O Null).

Data type definitions may contain type variables, in which case the type defined is a polytype. The following definition of a list type illustrates:

data List a = Cons a (List a) | Nil

Constructor Cons takes two arguments, an element of type a and a tail of type List a. Nil terminates the list. Like Binary, List is a recursive type and a list of any length can be defined. Unlike Binary, the free type parameter allows one to build a list over any type. A monomorphic (i.e., concrete) list is formed by substituting a with a monomorphic type. For instance, List Int describes a list of integers, which is monomorphic.

Defined Operations

User-defined operations are written using the = symbol. For example:

f = 42

f evaluates to the integer 42. For simplicity in this primer, we can refer to defined operations as functions without losing much precision.

Functions are often annotated with their type. The type annotation for f can be written as follows:

f :: Int

This says that f evaluates to type Int.

Function arguments are written to the left of the = symbol. Below, succ evaluates to the integer successor of a number, n:

succ n = n + 1

The type annotation of this function can be written as:

succ :: Int -> Int

This means succ takes an Int and returns an Int.

Rule Guards

Functions may be made conditional by one or more Boolean expressions called guards. Guards are separated from the left-hand side of the definition by a vertical bar, |. The use of guards makes is possible to define functions in piecewise fashion. A function for calculating the absolute value of a number, for instance, may be defined as follows:

abs n | n<0  = -n
      | True = n

The guard expressions, n<0 and True are evaluated sequentially from top to bottom and when one evaluates to true the corresponding rule is fired.

Pattern Matching

So far, each function argument has been a variable. More generally, the arguments in a function definition are patterns. A pattern is an expression involving variables and/or constructors. By using multiple patterns, a function can be written as a set rules. When evaluating such a function, the patterns are compared with the actual argument provided to determine which rule to fire. (For now, we will assume that only one rule ever matches, though this is a simplification.)

To illustrate, we can write write a function isNull that indicates whether a Binary argument has any digits at all:

isNull Null  = True
isNull (I _) = False
isNull (O _) = False

Note

_ is a placeholder for an anonymous variable.

When isNull is called, the Binary argument is inspected and only the matching rule is fired. Thus, isNull Null evaluates to True, whereas isNull (I Null) evaluates to False.

Curry supports another kind of function definition. When a pattern contains a function application, it is called a functional pattern. This provides a useful way to describe data constructively.

The following example implements a data store as a list of key-value pairs. Function find uses a functional pattern to select all values with a given key:

type Store a = [(String, a)]

find :: Data a => String -> Store a -> a
find key (_ ++ [(key, value)] ++ _) = value

This pattern uses the list append function, ++, to build any list containing a pair with the supplied key. When this rule matches, find evaluates to the corresponding value; with two pairs under the key, it has two values:

find "b" [("a", 1), ("b", 2), ("c", 3)]   -- 2
find "b" [("a", 1), ("b", 2), ("b", 3)]   -- 2, 3

The constraint Data a is required: the match unifies the pattern with the argument, and the front end of PAKCS 3.4.1 rejects the signature without it.

Note

The variables that a functional pattern binds stay in the result. A rule such as source (stem ++ ".o") = stem ++ ".c" returns a list of narrowed variables, and every later use of that name resolves them again: a binding is private to one alternative of the search, so the runtime copies the spine of the evaluation from its root to the variable and restarts. The cost is paid at every comparison of the name and grows with the depth of the context. Evaluating the result to normal form with $## does not remove it: the normal form lives in the private copy, and the next reader of the shared name starts over. To get plain data, read the result through a set function:

source :: String -> String
source (stem ++ ".o") = stem ++ ".c"

sourceOf :: String -> String
sourceOf t = case sortValues (set1 source t) of [s] -> s

The capsule of the set function resolves the variables once, and its values leave it as plain data. One name compared 200 times took 222141 rewrite steps through source, 180362 with $## on the result, and 14723 through sourceOf on the C++ backend; the forks of the first two grew with the comparisons and those of the third stayed at the match (tests/unit_funcpat_narrowed.py). The build system of example 24 reads its rules this way.

Other Constructs

Curry inherts other syntactic constructs from Haskell. A few of these are discussed below (though the list is by no means exhaustive):

let expressions

let expressions introduce and bind local variables.

Example:

poly n = let a=n+1 in a*a

The above defines a function poly that evaluates to the polynomial expression (n+1) * (n+1). a is a local variable.

Multiple bindings are permitted, as in:

poly' n = let a=(n+1), b=(n-1) in a*b

case expressions

A case expression performs pattern matching in an expression context. For example, another way to write isNull is as follows:

Example:

isNull'' binary = case binary of
    (O _) -> False
    (I _) -> False
    Null  -> True

Note

Single quotes may appear at the end of an identifier. This is often used to suggest multiple versions of an entity.

where clauses

A block of local definitions can be introduced by the where keyword. The definitions are visible only to the enclosing function.

Example:

The following code block defines a function to return every other element of a list. An auxiliary function, skip, is defined in a where clause:

everyOther [] = []
everyOther (h:t) = h:everyOther (skip t)
    where skip [] = []
          skip (h':t') = t'

If-Then-Else expressions

This construct is syntactic sugar for a pattern match over a Boolean expression. abs, for example, can be written as follows:

abs' n = if n<0 then (-n) else n

This could be written equivalently using a case expression as:

abs'' n = case n<0 of
    True  -> (-n)
    False ->   n