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.,0or1, 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.or3.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