User:Ashalkhakov/Functional Programming

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Chapter one: programming with functions.

Outline:

  • arithmetic: constants and operations
  • closing over constants to define functions
  • combining functions

The notion of a function, a description of relationship between inputs and outputs of processes, predates modern digital computing by at least four centuries. This notion was found to be very useful by mathematicians, physicists and engineers of the past. Computer programs are also descriptions of how a result can be obtained from some inputs. So a natural question to ask is: can we define programs with (mathematical) functions? The answer is "yes", and the corresponding style of programming is called "functional programming".

In fact, there is no need to constrain ourselves with numeric functions. We can as well define functions that have some compound structures (e.g. sequences of numbers) as argument or result. Functions can also be arguments and results of other functions.

Programming with functions[edit | edit source]

"Main" function[edit | edit source]

Let us write a simple expression the result of which is to be computed:

implement main () = print (2 + 2 * 2)

Unfortunately, to get a working program you must supply the "main" function, and we haven't even talked about functions in ATS yet! At this point, it can be helpful to not pay attention to what the expression implement main () = print (_) actually does. The whole program will print the result of evaluating 2 + 2 * 2, 6, to the terminal. Other functions have to be applied in order to evaluate this expression: in this case, the operators * and +. These operators are actually ordinary functions (with special notation) which have been predefined in the prelude (the standard library) which is part of the ATS system.

The prelude defines some commonly used functions and operators. For example, sqrt is one such function. When the following expression

implement main () = print (sqrt (2.0))

is evaluated, the value 1.414214 is displayed to the user.

It should also be noted that application of a function f to an argument x can be written as juxtaposition f x instead of a more familiar f(x). The reason for this digression is perhaps that function application is more frequent than multiplication in functional programming, so omission of some braces helps to reduce the notational burden. The operation of function application binds tighter than anything else, so f 1 + 2 stands for (f 1) + 2 rather than f (1 + 2). Multiplication is written explicitly using *.

Here is an example involving application:

implement main () = print (sqrt 2.0 + sin 0.3)

As expected, several applications can be combined in a single expression. f (g x) means that f should be applied to the result of evaluation of g x. Parenthesis around (g x) are necessary to indicate that (g x) as a whole is an argument to f.

Modules[edit | edit source]

It is common in programming to solve big problems by dividing them into smaller constituent parts, solving each in isolation, then combining your solutions into the one that solves your original problem. For example, if you write a calculator with command-line interface, then it makes sense to treat input (lexical and syntactic analysis) separately from expression evaluation and output. This program organization makes it easier to understand, develop, test, and change your program as you can do so incrementally, on a part-by-part basis.

ATS has a concept of "modules" which helps to organize a program in files. Each module contains the definition of a collection of functions, constants and operators that can be related to each other in some way. Modules are also indispensable for separate compilation. As a matter of fact, the prelude is nothing but a variety of modules. The program you write yourself is a kind of a module as well. Therefore, it should have a name, say test and should be stored in a file which in that case must have a name test.dats.

Defining variables[edit | edit source]

It is sometimes the case that some expression is very long and complicated, which makes it unwieldy to handle. We can break it up into smaller subexpressions and refer to each by its name. A variable declaration consists of a name and an expression that it stands for. This is how one can introduce a variable:

val x = 2 + 2 * 2

To the left of the equals sign we have a special keyword val and can be read as "the value of variable x is". At the right-hand side we write an expression. We say that "x is bound to 2 + 2 * 2" (or that x is a binding) in this compound expression. We may also say that val x = 2 + 2 * 2 introduces x, or that val x is the binding occurrence of x.

You might write the following in Pascal:

var x: integer;
begin
  x := 2 + 2 * 2;
end.

or C:

int x = 2 + 2 * 2;

There is an important difference however. In Pascal and C x is a (mutable) variable that is assigned the result of 2 + 2 * 2. In the ATS program above, x is merely a shorthand for 2 + 2 * 2. So variables in C and Pascal are representations for memory cells of the underlying machine, whereas variables in ATS are representations of mathematical variables. Variables in ATS are assigned to exactly once, at the point of definition, in contrast to variables in imperative programming.

Right now, what we call variables are actually constants. Don't let this terminological struggle confuse you. Variables have two roles in mathematics (and in functional programming as well). For example, in algebra, a variable can stand for some known or unknown (arbitrary) quantity. When it stands for a known quantity, we also call it a constant.

How variables interact with each other? For example, we write

val x = 15

and now that x is defined, we can use it in other expressions that follow, thus:

val y = x * x * x * x
val z = y + y
implement main () = print z

Following our intuition that variables are abbreviations of expressions, this program is equivalent the one below, where occurrences of variables x, y, z are replaced with their corresponding expressions:

implement main () = print (15 * 15 * 15 * 15 + 15 * 15 * 15 * 15)

In other words, to evaluate a program containing variables, replace every variable with what it stands for and then proceed as usual (since there will be no variables left).

"Let" expressions[edit | edit source]

Expressions we discussed previously (constants, arithmetic operators and some functions) have a property that they can nest into each other. Variable definitions as discussed in the previous section can nest as well, and the let construct is for doing exactly that.

Here is an example expression:

let val x = 7 in x * x end

It is a compound expression. First, x is bound to 7. After that, x * x is evaluated, keeping in mind that x is defined to be the same as 7. So the result of evaluation of this expression is what we write between in and end keywords.

Here is another example:

let
  val x = 7
in
  let val y = x * 2 in y + 1 end
end

To evaluate this expression is the same as to evaluate let val y = x * 2 in y + 1 end with 7 substituted for x. In other words, we substitute 7 for all occurrences of x in let val y = x * 2 in y + 1 end.

Here's a more realistic example:

val a = let
    val pi = 3.14
    val r = 5.0
  in
    pi * r * r
  end
implement main () = print a

In this program, a stands for calculated area of a circle of radius 5. As you can see, a let expression can introduce more than one variable.

The program above can also be rewritten thus:

val pi = 3.14
val a = let
    val r = 5.0
  in
    pi * r * r
  end
implement main () = print a

Defining new functions[edit | edit source]

In a functional programming language you can surely define new functions by yourself. User-defined functions can be used exactly as the predefined ones.

A fun keyword is for introducing functions. For example, we define a function circle_area for calculating an area of a circle as follows:

fun circle_area (r: double): double = 3.14 * r * r

So, a function definition consists of a name for a function, a list of arguments, and a body (the expression to the right of the equals sign).

The arguments appearing within parenthesis on the left-hand side of a function definition, such as r: double in our example above, are called the formal arguments (or formal parameters) of the function. In order to use a function, one applies it to actual arguments (also called actual parameters). For example, in circle_area 10.0 the function circle_area is applied to the actual argument 10.0. The actual argument corresponding to r is 10.0. Function arguments are variables the value of which we will only get to know when we apply a function to an actual argument: they stand for arbitrary expressions.

The function circle_area takes radius as an argument. Both radius and result must be of the type double, that is to say, circle_area cannot be applied to anything other than a double (double-precision floating-point numbers, actually a machine representation for "ideal" real numbers) and it is guaranteed that an application like circle_area 5.0 will also produce a double. Like Pascal, ATS is very strict about types. Unlike C, ATS does not support implicit coercions of numeric types. For example, 5 + 5.0 is not a well-typed expression in ATS (whereas it is in C).

circle_area can be used just like any predefined function. For instance:

implement main () = print (0.5 * circle_area 10.0)

When run, this program prints half the area of a circle of radius 10.

It should be noted that a function without argument is just a constant.

Program evaluation with functions[edit | edit source]

We learned that a functional program generally consists of a collection of function definitions and one special expression (namely, the implement main () = print (X) expression). The execution of such program begins with the evaluation of X. This initial expression is repeatedly replaced by its result after evaluating a function application. This process is also called reduction. One step of the process, evaluating a single function application, is called a reduction step. To evaluate an expression f x (called a redex, from reducible expression), take the body of f and replace all occurrences of a formal argument of f in it with x. When the expression contains no reducible sub-expressions, the reduction stops, and we say that such an expression is in a normal form. Evaluating an expression can be seen as searching for its normal form.

A simple example illustrating function evaluation follows. Suppose we have a program:

fun double_succ (x: int): int = 2 * x + 1
implement main () = print (double_succ (3 * 4))

The task is to evaluate (reduce) double_succ (3 * 4). It is of the form f x mentioned above (to be sure, note that f can be identified with double_succ and x with (3 * 4), respectively) and consists of two applications: first 3 and 4 are supplied as actual arguments to *, and the result is the argument to double_succ. Thus we have two redexes here. First we obtain 12 from 3 * 4 by arithmetic. Our initial expression double_succ (3 * 4) becomes double_succ 12. Since it is still a redex, we substitute 12 for all occurrences of x in the body of double_succ to obtain 2 * 12 + 1, in other words, our expression double_succ 12 becomes 2 * 12 + 1. This can be reduced further by rules of arithmetic, and the expression becomes 25 which is a constant that we cannot simplify any more. Hence 25 is a normal form of double_succ (3 * 4).

We can represent the above description with formulae. We write between two expressions to show that the expression on the left-hand side is reduced to the one on the right-hand side (that is, we denote a reduction step with an arrow), and underline redexes in an expression to show what are we going to do next. The is left-associative, that is to say, reduction steps are carried from left to right, and should be read (we omit braces for clarity and write each step on a new line).

Standard functions[edit | edit source]

Names of functions and operators[edit | edit source]

Many standard functions are predefined in the prelude, and we will discuss some of them in the following sections.

Here we give informal lexical rules of function naming. Function names start with a letter and are followed by zero or more letters, digits, or the symbol _ (underscore). Both lower and upper case letters are allowed: these are treated as distinct (thus A and a are not the same). For example, the following names are accepted by ATS:

 x, imbuf, camelCase, string_of_int

To make long names easier to read, either underscore or the "camel case" are used. However, the style adopted by the prelude is to use underscores.

Carefully chosen names are helping to convey the intended meaning behind functions and variables. People are more willing to understand programs with well-chosen names. However, in ATS we also use various abbreviations; names of function arguments may as well be very short. If you are troubled with this, don't worry as the necessary skill is acquired very quickly.

As is common in mathematics, we may also use various symbols instead of names: this is very useful for defining your own operators, such as + and ~. Application of functions taking one argument can be written in postfix as well as prefix form, while application of functions of two arguments can be written infix. This kind of syntax extensibility is very uncommon in languages such as Pascal and C. We should note that, for example, the syntax for + is not that hard-coded into the ATS compiler as it is in C. The mechanisms of symbol overloading and fixity declarations are used instead; we will cover these later.

Predefined functions on numbers[edit | edit source]

Numbers in ATS are of two kinds: integers (such as 42, 0 and ~2) and floating-point (such as 0.45, ~1.0, 0.0, 0.7e2 and 0.1e-3). The character e in floating-point numbers means `times ten to the power of'. Thus, 0.7e2 is read "0.7 times ten to the power of 2", that is, . The number 0.1e-3 is in fact . The prelude modules integer and float define some common functions on integers and floating-point numbers, respectively. The four arithmetic operators +, -, *, / are defined for both integers and floating-point numbers, for instance:

implement main () = print (40 - 1)

and

implement main () = print (3.0 * 0.5)

Arguments to an arithmetic operator must both be of the same type: either integer or floating-point. For example, the expression 1 + 1.0 is not accepted by the compiler. You must use the standard (overloaded) functions that convert numbers. double_of_int is a function that converts an integer to a double precision floating-point number; int_of_double is for doing the reverse. Thus the following is acceptable:

implement main () = print (1 + double_of_int 1.0)

Some standard functions on integers are:

abs
the absolute value of a number
neg
negation of a number (also written ~)
succ
the successor of a number
pred
the predecessor of a number
mod
the remainder of division of one number by another (also called modulo)
gcd
the greatest common divisor of two numbers
pow
raise the first number into n-th integer power (where n is greater than or equal to 0)

These functions (excluding mod and gcd) will also work on float-point numbers. We say that these names are overloaded to mean that they may stand for different functions in different contexts. For example, consider succ 1 and succ 1.0: these are two expressions that apply succ to numbers of different type. In both cases, we have that different functions have the same name. Which is which, then? In the first case (succ 1) the argument is an integer, thus succ is a function on integers in this context. This is similar to mathematics, where we write and but mean different operations in each case.

Predefined functions on booleans[edit | edit source]

Booleans can be regarded as truth values, we write true to denote truth and false for falsity. We may also say that true and false are introducing values of boolean type, bool. For example, the operator < is for determining whether a number is smaller than another number: a < b evaluates to true iff a is strictly less than b, and to false otherwise. To wit, the value of 4 < 5 is true.

Operators for comparing numbers also include > (greater than), <= (less than or equal to), >= (greater than or equal to), = (equality) and <> (inequality).

Standard logic operations such as conjunction (logical "and") and disjunction (logical "or") are available and written as && and ||, respectively. These can be used for combining boolean expressions. The operator && evaluates to true iff both of its operands evaluate to true, and the operator || evaluates to true iff at least one of its operands evaluates to true. For example, 10 < 5 && 5 > 4 evaluates to false because one of the operands (namely, 10<5) evaluates to false. On the other hand, 4 < 5 && 10 < 4 evaluates to true. An operator ~ negates a boolean: given true, ~true is the same as false, and ~false evaluates to true (you can of course substitute any boolean expression for boolean constants in the example). We can also compare booleans using the = operator, it works analogously to that of numbers, although we stress again that = for booleans is an operation different from = for integers despite the name.

Suppose we are given a boolean expression the value of which we do not know (that is, we do not know precisely if it is true or false). We need an operation that can do something useful with such values by examining them. In ATS, there is a built-in operation if ... then ... else ... which takes three arguments. It's called the conditional function, and its first argument is called "precondition". Let's demonstrate its use by example. The result of evaluating the following program:

fun compute (x: int): int = if x <= 10 then 42 else 34
implement main () = print (compute 4)

is 42 printed in terminal. The general rule is that if precondition (x <= 10 in our example) evaluates to true, then the result of evaluation of the conditional is the result of evaluation if its second argument. If, however, precondition is evaluates to false, then the result of evaluation of the conditional is the result of evaluation of its third argument. The types of second and fourth arguments must match (in the same way that types of operands to + must match).

It should be pointed out that in contrary to "normal" functions, a conditional doesn't evaluate both second and third argument (and this follows from rules outlined in the previous paragraph). Let's try to sketch an evaluation of if x < 5 then x+y else x, assuming that x = 6 and y = 3:

  • we evaluate x < 5 to false
  • since the precondition is false, we evaluate x, which is just 6

Thus the value of expression is 6. This minor detail is not very useful to know at this point, and we will revisit it later, after introducing other features. This process can also be illustrated with the notation we introduced in Program evaluation with functions:

Operators analogous to that of numbers are also available. You can think of booleans as of integers: 0 is the same as false and 1 is the same as true. Here are some operations on booleans that you

Defining functions[edit | edit source]

In this section, we'll take a closer look at how a function is defined. There are many ways to do so, for example using a graph to visually illustrate dependency of a result upon an argument. As another example, a table can be used instead: in the simplest case, you simply enumerate arguments and the intended result. However, this is quickly getting tedious and impractical, so you might want to try something else, such as finding rules (laws) that turn argument into the result. Since we're doing programming here, we'll focus on this latter method. It is by no means the only one, neither it is the best for all circumstances. We leave it up to the reader to decide.

Definition by cases[edit | edit source]

It is sometimes necessary to distinguish a number of cases in a function definition. As an example, consider a function to take the absolute value of a number: for negative arguments the result is calculated differently than for positive ones. We can use an if-expression to achieve that in ATS:

fun absolute (x: int): int = if x < 0 then ~x else x

If you want to distinguish more than two cases, that can be obtained by nesting if-expressions. For example, the following is a function sign that we want to return ~1 if its argument is negative, 0 if it is zero, and 1 otherwise:

fun sign (x: int): int = if x < 0 then ~1 else (if x = 0 then 0 else 1)

We have enclosed the second if in parenthesis to stress the order of evaluation, it's perfectly legal to omit them because this is also the order used by convention. In such a compound expression, we first check if input is negative, then see if it equals zero. We leave it as an exercise for the reader to make sure that this is consistent with the rules for evaluating if-expressions we have outlined in one of the previous sections. For clarity, you may also write this function as follows:

fun sign (x: int): int = if x < 0 then ~1
                         else if x = 0 then 0
                         else 1

Definition using patterns[edit | edit source]

Most programmers having an exposure to either C or Pascal are well accustomed to the fact that variable names can only be introduced (bound) in two ways: through function definitions (variables also become formal arguments), or through variable definitions (such as the val we discussed earlier). Now, there is a mechanism in ATS called pattern matching the meaning of which is twofold: it allows you to *deconstruct* a value (to see if it happens to be of the form you anticipate), and it can also bind the parts of the value to some variables. An example should make things clearer.

We'd like to implement a function on booleans that returns true iff both of its arguments are true (that is, we are going to implement the operation of conjunction). Here's one way, using a case expression:

fun conj (x: bool, y: bool): bool = case x of
  | true () => y
  | _ => false

The body of the function conj consists of a single case expression, which lists two clauses. Each clause consists of a pattern expression to the left of the => symbol and another expression (also called the body of the clause) to the right.

This seems strange to a new-timer, so we will try to make sense of this new case construct. Booleans can be seen as values that can only be constructed (introduced) in two ways: you either use true or you use false and it's safe to think that there are no more values of the type bool except the two mentioned. What pattern matching allows you to do in this case, is to discriminate the two cases, just like an if expression. The code written above can be interpreted as "if the value of the first argument is true, then the result of conj (x, y) is just whatever y is, otherwise it is false". We find it's best to think of a case expression as a generalization of if expression. Whereas an if expression only works for booleans, a case expression will work on any datatype that you define (we will get to datatypes a bit later). For now, it suffices to know that an if expression can be replaced by the corresponding case. Thus the function given above is the same as the one below:

fun conj (x: bool, y: bool): bool = if x = true then y else false

We say that true and false are constructors of the type bool. In a case expression, you can enumerate all possible constructors of a datatype, and what is to be done if the value matches the pattern. In our case, a pattern is simply a constructor written with two parentheses: (). Why the parentheses, then? This is because a pattern can also be a variable. For example, here is an example involving variables introduced in a pattern:

fun disj (x: bool, y: bool): bool = case x of
  | true () => true ()
  | z => y

Here we implemented a disjunction with the help of a case expression. As you see, true and true () are actually the same thing in an expression. However, when used in a pattern, true is the variable binding (and by the way, the _ aka underscore we used in the previous example, is also a variable, but it is hidden: you can't use it an expression).

In the first branch, we know that x is the same as true (), whereas in the second branch x is renamed to z. However, we don't use that binding.

Generally, a case expression follows this syntax: case EXPR of PATTERN1 => EXPR1 | PATTERN2 => EXPR2 | ... | PATTERNn => EXPRn (as you can see, the leading pipe | can be dropped, this is to make the life of a programmer simpler).

Let us try to find the rules for evaluating case expressions. First off, a case expression operates on some expression: after evaluating that expression to its normal form, we try the patterns one by one, top to bottom (or left-to-right, if you write the clauses in one line), to find the first pattern that matches the value of the expression.

When we find a clause with a matching pattern, the result of evaluating the body of the clause is the result of evaluating the whole case expression. If there is no match, it is a runtime error and the program evaluation must terminate. So, even if the program goes wrong, it will be stopped. We will cover the topic of mitigating failures of pattern matching at some other point.

You will certainly appreciate this feature after we introduce datatypes. For now, here's another example:

fun sign (x: int): int = case x of
  | 0 => 0
  | x => if x < 0 then ~1 else 1

As you see, we need not account for x being 0 in the last clause of the case expression, because that case is already covered by pattern matching. This intuition is based on the fact that this function is equivalent to the one under the same name that we discussed in one of the previous sections.

To recap, the evaluation of a case expression involves the following:

  • evaluate what you will match your patterns against
  • try to match against the pattern of each clause, starting from the first one (in the program text)
  • on finding a match, evaluate the body of the pattern -- this is the intended result of evaluation

Definition by induction or recursion[edit | edit source]

So far, all the definitions we gave have a characteristic common to all of them. They are non-recursive. Syntactically, a non-recursive function never ever refers to itself, directly or indirectly. In the definition of a recursive function, the name of the function can 're-(oc)cur' in its body. Here is an example of a recursive definition:

fun fac (n: int): int = case n of
  | 0 => 1
  | n => n * fac (n-1)

The name of this function occurs in the body of the second clause of the case expression. Another example could be a solution to the problem stated as "given two integers a and b, compute a raised to the power of b". We present it as follows:

fun power (a: int, b: int): int = case b of
  | 0 => 1
  | _ => a * power (a, b-1)

Here is another curious function:

fun f (x: int): int = f x

Can you say what is the result of evaluating this function? There is no result! Thus we see that not all expressions in ATS have a meaningful normal form, some, such as f, do not. We also say that evaluation of f does not terminate.

This leads us to think that we need to place some restrictions on the definition of recursive functions. For now, take a look at the overall structure of the two "valid" recursive functions we introduced in this section and try to notice what is common among them. Both operate on one of their arguments. Both handle two cases for such an argument, distinguishing 0 from everything else. The body of the case that handles 0 is not recursive at all (we call it the base case). The actual argument for the recursive call is getting closer to the base case (in our case, smaller than the corresponding formal argument of the function being defined).

In the definition of fac given above, the base case is covered by the first clause of the case expression; in this case, the result can be determined directly (in other words, without a recursive call). In the second clause the following should be noted:

  • we know that the formal argument n is different from zero
  • in the recursive call, namely fac (n-1), n-1 is smaller than n

Definition by combination[edit | edit source]

By far the easiest way to define functions is by making use of other, previously defined functions and operators. For example, consider:

fun square (x: int): int = x * x
fun over (n: int, k: int): int = fac n / (fac k * fac (n-k))

The function over depends upon a prior definition of fac. Moreover, we shall stress that many functions are partial, that is, not defined for all arguments, but only for some. For example, the evaluation of fac ~1 (as defined in the previous section) will not produce the wanted result (it will simply evaluate forever!). We leave it as an exercise for the reader to figure out the conditions on inputs necessary for over to produce the intended result.

Some examples on predicates follow:

fun negative (x: int): bool = x < 0
fun positive (x: int): bool = x >= 0
fun is_zero (x: int): bool = x = 0

Note that, in the last example, the two occurrences of = mean different things: the same symbol is used for both defining functions and performing an equality test.

Scope[edit | edit source]

Intuitively, a variable must be defined before it can be used. We have as of yet glossed over the rules for determining where a variable can be used. Every variable is associated with some set of expressions where its occurrence is legal. This set is also called the scope of variable.

Consider this program:

val x = 5
fun main () = print x

Here, the scope of x is any expression after the binding site. To put it differently, x can occur in any expression whatsoever, provided that it follows the val x = 5 expression in the program source code.

The scope of a variable introduced using the let expression is confined to expressions within in ... end. For example, in this expression:

let
  val x = 32
in
  x + x
end

x is always 32, no matter what the surrounding context may be. For example, let val x = 32 in let val x = 5 in x+x end end always evaluates to 10.

Likewise for let, the scope of formal arguments to a function is the function body (that is, an expression to the right of =).

So, the following (contrived) function simply returns its argument:

fun f (x: int): int = let
  val x = x+5
in
  x-5
end

The rule is that every time you see a variable, the "nearest" (innermost) binding takes precedence over others. In the example above, x is bound by a function (in other words, x is a formal argument), but in the body of the function it is rebound by a val expression. In the first non-binding occurrence, x+5, x stands for whatever is passed to the function, in the second non-binding occurrence, x-5 stands for x passed to the function and incremented by 5.

Comments[edit | edit source]

Sometimes it is necessary to elucidate the program text for human reader. We use comments for that, a comment is a part of the program text that is ignored by the compiler as it is only for people to rea . There are two ways to mark a section of program text as a comment:

  • with symbols // a comment is marked that extends to the end of the line
  • with symbols (* a comment is marked that extends to the matching symbols *)
  • with symbols //// a comment is marked that extends to the end of the file (essentially, //// marks the end of file for the compiler that doesn't quite match the "real" one)

Comments marked the second way may be nested, that is, contain a comment themselves. For example, the following does not define a function as everything is considered a comment:

(* (* hey *) fun f (x: int): int = x *)

The comment is finished only when every (* is closed with a matching *). This is analogous to the habit of closing any opening parenthesis.

Finally, a //// comment is useful when, say, you are experimenting with some code.