3 Main Features of Curry

3.13 Local Definitions

The syntax of Curry implicitly associates a scope to each identifier, whether a function, a type, a variable, etc. Roughly speaking the scope of an identifier is where in a program the identifier can be used. For example, the scope of a variable occurring in the left-hand side of a rule is the rule itself, which includes the right-hand side and the condition, if any. In the following code:

square x = x * x
cube   x = x * square x

the variable identified by “x” in the definition of “square” is completely separated from the variable identified by “x” as well in the definition of “cube”. Although these variables share the same name, they are completely independent of each other.

Curry is statically scoped, which means that the scope of an identifier is a static property of a program, i.e., the scope depends on the textual layout of a program rather than on an execution of the program.

The scope of an identifier is the region of text of a program in which the identifier can be referenced.

In most cases, the programmer has no control on the scope of an identifier—and this is a good thing. The scope rules are designed to make the job of the programmer as easy and safe as possible. The context in which an identifier occurs determines the identifier’s scope. However, there are a couple of situations where the programmer can limit, by mean of syntactical constructs provided by the language, the scope of an identifier. Limiting the scope of an identifier is convenient in some situations. For example, it prevents potential name clashes and/or it makes it clearer that a function is introduced only to simplify the definition of another function. A limited scope, which is referred to as a local scope, is the subject of this section.

Curry has two syntactic constructs for defining a local scope: the “where” clause and the “let” clause. They are explained next.

3.13.1 Where Clauses

A “where” clause creates a scope nested within a rewrite rule. The following example defines an infix operator, “**”, for integer exponentiation [Browse Program][Download Program]:

infixl 8 **
a ** b | b >= 0 = accum 1 a b
   where accum x y z | z == 0    = x
                     | otherwise = accum aux (y * y) (z div 2)
           where aux = if (z mod 2 == 1) then x * y else x

For example, 2 ** 5=25=32. There are several noteworthy points in the above code fragment. The scope of the function “accum” is limited to the rewrite rule of “**”. This is convenient since the purpose of the former is only to simplify the definition of the latter. There would be no gain in making the function “accum” accessible from other portions of a program. The function “accum” is nested inside the function “**”, which is nestingaccum”.

The rewrite rule defining “accum” is conditional. Pattern matching of the arguments and non-determinism can occur as well in local scopes. Finally, there is yet another local scope nested within the rewrite rule of the function “accum”. The identifier “aux” is defined in this scope and can be referenced from either condition or right-hand side of the rewrite rule of the function “accum”.

The right-hand side of the rewrite rule defining “aux” references the variables “x”, “y” and “z” that are arguments of “accum” rather than “aux” itself. This is not surprising since the scope of these variables is the rewrite rule of “accum” and “aux” is defined within this rule.

The identifier “aux” takes no arguments. Because it occurs in a local scope, “aux” is considered a local variable instead of a nullary function. The language does not make this distiction for non-local identifiers, i.e., identifiers defined at the top level. The evaluation of local variables differs from that of local functions. All the occurrences of a variable, whether or not local, share the same value. This policy may affect both the efficiency of a program execution and the result of computations involving non-deterministic functions. The following example clarifies this subtle point [Browse Program][Download Program]:

coin = 0
coin = 1
g = (x,x) where x = coin
f = (coin,coin)

The values of “g” are (0,0) and (1,1) only, whereas the values of “f” also include (0,1) and (1,0). The reason of this difference is that the two occurrences of “coin” in the rule of “f” are evaluated independently, hence they may have different values, whereas the two occurrences of “x” in the rule of “g” are “shared,” hence they have the same value.

There is one final important aspect of local scoping. A local scope can declare an identifier already declared in a nesting scope—a condition referred to as shadowing. An example of showing is shown below:

f x = x where x = 0

The variable “x” introduced in the where clause shadows the variable with the same name introduced in the rewrite rule left-hand side. The occurrence of “x” in the right-hand side is bound to the former. Hence, the value “f 1” is 0. This situation may be a source of confusion for the beginner. The PAKCS compiler/interpreter detects this situation and warns the programmer as follows [Browse Program][Download Program]:


Prelude> :l shadow
...
shadow.curry, line 1.3: Warning:
    Unused declaration of variable ‘x’
shadow.curry, line 1.15: Warning:
    Shadowing symbol ‘x’, bound at: shadow.curry, line 1.3

The second warning reports that the identifier in line 1, column 15, the variable “x” in the local scope, shadows some identifier(s) with the same name. The first warning reports that the identifier in line 1, column 3, the variable “x” argument of “f”, is not used. This is a consequence of its shadowing and gives an important clue that the occurrence of “x” in the right-hand side of the rewrite rule of “f” is bound to the local variable rather than the argument.

3.13.2 Let Clauses

A “let” clause creates a scope nested within an expression. The concept is very similar to a “where” clause, but the granularity of the scope is finer. For example, the program for integer exponentiation presented earlier can be coded using “let” clauses as well [Browse Program][Download Program]:

infixl 8 **
a ** b | b >= 0 =
  let accum x y z | z == 0    = x
                  | otherwise =
                       let aux = if (z mod 2 == 1) then x * y else x
                       in  accum aux (y * y) (z div 2)
  in  accum 1 a b

Using a “let” declaration is more appropriate than a “where” declaration for the definition of operation “aux”. With a “let” declaration, the scope of the identifier “aux” is the right-hand side of the second conditional rule of the function “accum” instead of the whole rule.

3.13.3 Layout

By contrast to most languages, Curry programs do not use a printable character to separate syntactic constructs, e.g., one rewrite rule from the next. Similar to Haskell, Curry programs use a combination of an end-of-line and the indentation of the next line, if any. A Curry construct, e.g., a “data” declaration or a rewrite rule, terminates at the end of a line, unless the following line is more indented. For example, consider the following layout:

f = g
 h...

Since “f” starts in column 1 and “h” starts in column 2, the right-hand side of the rule defining “f” consists in the application of “g” to “h” to “...” By contrast, with the following layout:

f = g
h...

the right-hand side of the rule defining “f” consists of “g” only. Since “h” starts in the same column as “f”, this line is intended as a new declaration.

The layout style described above goes under the name “off-side rule”. The examples of Sections 3.13.1 and 3.13.2 shows how the off-side rule applies to “where” and “let” clauses.