User:Ashalkhakov/Functional Programming
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
tofalse
- since the precondition is
false
, we evaluatex
, which is just6
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 thann
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.