1.1 Functions and Procedures
1.1.1 Felix functions
When you see code like this:
println$ "Square of " + str 1 + " is " + str (1 * 1); println$ "Square of " + str 2 + " is " + str (2 * 2); println$ "Sguare of " + str 3 + " is " + str (3 * 1);
Square of 1 is 1 Square of 2 is 4 Sguare of 3 is 3
You know you need a function (notice the typos on the last line?) Basic Felix functions are easy:
fun square(x:int)=>x * x; // define a function println$ "Square of " + str 1 + " is " + str (square 1); println$ "Square of " + str 2 + " is " + str 2.square; println$ "Sguare of " + str 3 + " is " + (str$ square 3);
Square of 1 is 1 Square of 2 is 4 Sguare of 3 is 9
We fixed the bug in the calculation of the square of 3, and
we're also showing the three ways to apply a function:
high precedence application using operator whitespace
which needs surrounding parentheses for correct grouping,
reverse application using operator dot, which is even higher
precedence than whitespace, and so does not, and finally we're
applying the str
function to a whitespace application
using operator dollar, which has lower precedence than whitespace.
There's a longer way to write a function:
fun long-winded-square(x:int):int = { val s = x * x; return x; }
This way allows complicated expressions to be broken up into arbitrary code.
Functions are not allowed to have side effects.
1.1.2 Felix Procedures
Still, there must be a way to repeat the print: there is, by using a procedure.
proc ps(x:int) { println$ "Square of " + str x + " is " + str x.square; } ps 1; ps 2; ps 3;
Square of 1 is 1 Square of 2 is 4 Square of 3 is 9
A procedure is like a function, except it returns no value, and it may and indeed should have side effects.
1.1.3 Multiple arguments
A function (or procedure) always has exactly one argument. However you can simulate multiple arguments with a tuple:
fun addup (x:int, y:int) => x + y; println$ addup (1,2); // 3 val pair = 1,2; println$ addup pair; // 3
3 3
The last line shows clearly that addup
takes a single argument
which is a pair (tuple with two components).
1.1.4 Function types
A function value has a type:
int -> int // type of square int -> void // type of ps int * int -> int // type of addup
1.1.5 Generators
There is a special kind of procedure that looks like a function and may have side-effects, it is called a generator:
var x =0; gen incx():int = { val tmp = x; ++x; return tmp; } println$ incx(), incx();
(0, 1)
When an expression contains one or more generator application, the applications are lifted out and evaluated in pre-order: this is basically left to right order of writing for terms at the same level, except that if a generator has an argument that is, or contains a generator application, that will have to be evaluated first. The serialisation of generator application is deterministic and based on the structure of the expression being evaluated after syntactic sugar is removed. For example
gen g1 (y:int) = { ++x; return y + x; } gen g2 (y:int) = { ++x; return y + 2 * x; } x = 0; var r1 = (g1 1).g2; println$ r1;
6
r
will be evaluated as:
x = 0; var a1 = g1 1; // 2 (x=1) var r2 = g2 a1; // 6 (x=2) println$ r2;
6
noting reverse application is just sugar for a swapped forward application.
Generators have the same type as a function. This is a design compromise in Felix.
1.1.6 C functions
The function and procedure:
cfun f(x:int) => x; cproc p(x:int) { println x; }
have types
int --> int int --> void
respectively, which are the types of classic C functions:
int (*)(int) void (*)(int)
You can use this kind of function to force generation of a classic C style function for performance, so you that know the type exactly, and so you can pass it as an argument to a C library. In particular such a function or procedure should not reference Felix globals (because this requires passing the thread frame pointer), nor do anything directly or indirectly requiring garbage collection (which also requires thread frame pointer since that in turn contains a pointer to the garbage collector).
C functions can be used wherever a Felix function is required, a wrapper is generated automatically.
C functions of this kind are still defined with Felix code: they're Felix functions with an enforced C interface. In fact many ordinary Felix functions will be reduced to C functions by the optimiser, but this form enforces the use of C interface.
It seems unfortunate that whilst a cfun
or cproc
has a C interface it
is defined in Felix, whereas a cstruct
is a Felix interface to a
struct defined in C, and cenum
and cflag
provide Felix models of enumerations
or sets of integer constants defined in C. The role reversal here seems confusing,
especially as Felix functions and types can be defined in C using fun
, proc
and type
keywords and C code given as a string, and ctypes
provides a shorthand form for
multiple instances of type
eliding the strings. In turn cenum
may be considered
a shorthand for a type
binding and a list of const
definitions. Also, a shorthand
for lifting C functions from libraries using a fun
or proc
without a
defining string can be used. The situation is further complicated by the
callback
feature.
1.2 Higher Order Functions
In Felix, functions can be nested:
fun f(x:int) = { fun g(y:long)=> x.long + y; return g; }
The function f
here has type int -> (long -> long)
. This type is
the same as int -> long -> long
since the arrow operator is right
associative. There's a simpler way to write the function f
:
fun f(x:int) (y:long) => x.long + y;
Here one says f
has arity 2. Note it still has only
one argument, namely x
of type int
. A function with arity
2 or greater is called a higher order function
or Hof for short.
Note that in:
proc h(x:int) (y:long) { println$ x.long + y; }
h
is a function, not a procedure: it has type
int -> (long -> void)
so that rather, h
is a function returning a procedure.
It is possible to partially apply a higher order function.
fun f2(x:int) (y:long) => x.long + y; val g = f2 1; println$ g 2l;
3
Here g
is just the value returned by f
when applied
to 1, which is a function accepting a long
and returning
a long
.
1.2.1 A style issue
Many functions of "two" arguments can be written either accepting a tuple of two components or as a higher order function of arity 2. Without optimisation, higher order functions are extremely inefficient: tuples are faster, but they cannot be partially applied. However, the higher order form does not permit the "arguments" to be "compressed" into a single value which the tuple form does.
However, Felix is pretty good at optimisation and both version of f
given above will likely reduce to a simple addition after inlining.
Therefore the question really is: are you most likely to aggregate the arguments, or the function with the first argument? Use a tuple in the first case and a higher order function in the second.
Of course, you should chose the form of a function which itself has the type required for it to be an argument of a higher order function such as an iteration, map or fold of some data structure.
The library module categ
defines a way to convert between these styles
for 2 or 3 "arguments". The function curry
takes a function accepting
a tuple of 2 or 3 components and makes it into a higher order function
with arity 2 or 3. The functions uncurry2
and uncurry3
reverse
this transformation and convert a higher order function of arity 2 or 3,
into a function accepting a tuple of 2 or 3 components, respectively.
1.2.2 Anonymous function values
You can write a function without a name in an expression:
val three = (fun (x:int)=> x + 1) 2;
There is a special short hand for procedures of unit argument:
val hello1 = { println "Hello"; };
This is an expression equivalent to
val hello2 = proc () { println "Hello"; };
There's a similar shorthand for function or unit argument:
val y = 1; val add1 = { val x = y + 1; return x; };
distinguished from the procedure case by ending with a return statement.