fble-0.5 (2025-07-13,fble-0.4-212-ga8f8ad0f)
This tutorial does a deep dive into functions. By the end of this tutorial, you'll be fully versed in section 5 of the fble language spec on functions.
A function computes a result based on the value of one or more arguments. You create a function by listing the types and names of the arguments and the code to execute when the function is called. You can later invoke the function by supplying the arguments to execute the function with.
Here are examples of functions we've already seen:
(Bit@, Bit@) { Bit@; } And = (Bit@ a, Bit@ b) { a.?(0: 0, 1: b); }; (Bit4@, Bit4@) { Bit4@; } And4 = (Bit4@ a, Bit4@ b) { Bit4@(And(a.3, b.3), And(a.2, b.2), And(a.1, b.1), And(a.0, b.0)); }; Bit4@ Z = And4(X, Y);
A function type describes the argument and return types of a function. The argument types are listed in parenthesis. The return type follows the argument list and is in braces. For example, here is the type of an integer comparision function:
(Int@, Int@) { Bool@; }
This describes a function of two arguments. The types of the arguments are
both Int@
. The function returns a value of type Bool@
.
Notice the names of the function arguments are not part of the type. Function argument names have no impact on how the function is used, so they need not be included in the function type.
The return type of the function is specified in braces after the argument list to match the same layout used when defining a function, where the body of the function is specified in braces after the arguments.
Functions can have one or more arguments. The arguments can be different types.
There is no such thing as a zero argument function in fble. Just use the body of the function as a value directly, or write a function with a single unused argument.
A function value is described by listing the argument types and names
followed by the body of the function in braces. The body of a function is
normal fble code describing the value to return from the function. There is
no explicit return
statement. For example:
(Bit@ a, Bit@ b) { a.?(0: 0, 1: b); }
This describes a function value of type (Bit@, Bit@) { Bit@; }
. The
compiler can deduce the return type of the function based on the type of the
function body.
You can do anything in a function body that you can do elsewhere in fble, including defining types, variables other function and referencing other modules.
Here's an example showing use of local variables in a function:
(Bit4@, Bit4@) { Bit4@; } And4 = (Bit4@ a, Bit4@ b) { Bit@ 3 = And(a.3, b.3); Bit@ 2 = And(a.2, b.2); Bit@ 1 = And(a.1, b.1); Bit@ 0 = And(a.0, b.0); Bit4@(3, 2, 1, 0); };
The return value of this function is the Bit4@
value constructed in the
last line of the body.
Here's an example of a function that defines a local helper type and function:
(Bit2@, Bit2@) { Bit2@; } Add = (Bit2@ x, Bit2@ y) { @ Result@ = *(Bit@ carry, Bit@ sum); (Bit@, Bit@) { Result@; } Add1 = (Bit@ a, Bit@ b, Bit@ c) { a.?( 0: b.?( 0: c.?(0: Result@(0, 0), 1: Result@(0, 1)), 1: c.?(0: Result@(0, 1), 1: Result@(1, 0))), 1: b.?( 0: c.?(0: Result@(0, 1), 1: Result@(1, 0)), 1: c.?(0: Result@(1, 0), 1: Result@(1, 1)))); }; Result@ r0 = Add1(x.0, y.0, 0); Result@ r1 = Add1(x.1, y.1, r0.carry); Bit2@(r1.sum, r0.sum); };
In this case we define a two bit Add
function based on a single bit
Add1
function that takes carry in and produces a carry out. We define a
local type Result@
to bundle a single bit sum and carry out.
The definition of Add1
in the previous section involves creating a
function value and assigning it to the variable Add1
. This will happen
every time the Add
function is called. Because Add1
doesn't depend
on the inputs x
and y
to the Add
function, we can pull it out of
the body of the Add
function to avoid some extra allocations. You could
pull the definition of Result@
and Add1
out into the global scope,
or you could keep its visibility restricted to just the definition of the
Add
function as follows:
(Bit2@, Bit2@) { Bit2@; } Add = { @ Result@ = *(Bit@ carry, Bit@ sum); (Bit@, Bit@) { Result@; } Add1 = (Bit@ a, Bit@ b, Bit@ c) { a.?( 0: b.?( 0: c.?(0: Result@(0, 0), 1: Result@(0, 1)), 1: c.?(0: Result@(0, 1), 1: Result@(1, 0))), 1: b.?( 0: c.?(0: Result@(0, 1), 1: Result@(1, 0)), 1: c.?(0: Result@(1, 0), 1: Result@(1, 1)))); }; (Bit2@ x, Bit2@ y) { Result@ r0 = Add1(x.0, y.0, 0); Result@ r1 = Add1(x.1, y.1, r0.carry); Bit2@(r1.sum, r0.sum); }; };
This way the Add1
function is only allocated once when the Add
function is defined, but it is still only visible to the Add
function.
This is a common way to pull out parts of a function that don't depend on
the function arguments to avoid repeatedly executing the same code every
time a function is called.
Fble supports recursive functions, via recursive variable declarations, as
we saw in the tutorial on Variables
. For example:
(Int@) { Int@; } Factorial = (Int@ n) { Eq(n, 0).?(true: 1, false: Mul(n, Factorial(Sub(n, 1)))); };
In fble, function arguments are evaluated strictly, meaning the arguments to the function are computed before passing them to the function. When using recursive functions, this could result in deeply nested chains of function calls that could take up a lot of space on the stack.
Fble does not arbitrarily restrict the size of stack. You can allocate as much on the stack as you could on the heap. There is no need to rewrite your code to do allocations on the heap instead of the stack as is the case for languages like Java or C.
Fble has special support for tail recursion. If the recursive call is the last thing in the function, the runtime will reuse the stack for the recursive call instead of adding another call frame on the stack. This makes it possible to write recursive functions that use constant space.
Fble does not have a while loop construct. Instead you should use recursive functions for loops.
Mutually recursive functions are naturally supported via mutually recursive
variable declarations as discussed in the tutorial on
Variables
.
Functions are values in fble, just like struct values and union values. Function values can be passed around just like struct values and union values, including as arguments or results of other functions. In other words, fble supports higher order functions.
For example, recall the bitwise And4
function we wrote earlier:
(Bit4@, Bit4@) { Bit4@; } And4 = (Bit4@ a, Bit4@ b) { Bit4@(And(a.3, b.3), And(a.2, b.2), And(a.1, b.1), And(a.0, b.0)); };
We could write bitwise Or4
and Xor4
functions that would look very
similar, assuming we have single bit Or
and Xor
functions defined:
(Bit4@, Bit4@) { Bit4@; } Or4 = (Bit4@ a, Bit4@ b) { Bit4@(Or(a.3, b.3), Or(a.2, b.2), Or(a.1, b.1), Or(a.0, b.0)); }; (Bit4@, Bit4@) { Bit4@; } Xor4 = (Bit4@ a, Bit4@ b) { Bit4@(Xor(a.3, b.3), Xor(a.2, b.2), Xor(a.1, b.1), Xor(a.0, b.0)); };
Rather than duplicate the logic for binary bitwise functions, we could factor it out into a function that takes the single bit operation as an argument:
((Bit@, Bit@) { Bit@; }, Bit4@, Bit4@) { Bit4@; } BinaryBitwise = ((Bit@, Bit@) { Bit@; } op, Bit4@ a, Bit4@ b) { Bit4@(op(a.3, b.3), op(a.2, b.2), op(a.1, b.1), op(a.0, b.0)); };
We've added an argument called op
with function type
(Bit@, Bit@) { Bit@; }
representing the single bit operation. We can
reuse this higher order function for the definitions of And4
, Or4
,
and Xor4
:
(Bit4@, Bit4@) { Bit4@; } And4 = (Bit4@ a, Bit4@ b) { BinaryBitwise(And, a, b); }; (Bit4@, Bit4@) { Bit4@; } Or4 = (Bit4@ a, Bit4@ b) { BinaryBitwise(Or, a, b); }; (Bit4@, Bit4@) { Bit4@; } Xor4 = (Bit4@ a, Bit4@ b) { BinaryBitwise(Xor, a, b); };
We've seen an example of passing functions as arguments to other functions.
It's also possible to return functions from functions. That's arguably a
more natural way to write our BinaryBitwise
helper function. Instead of
it taking three arguments, the operation and both operands, we can change it
to take a single argument which is the operation and return a function that
performs the corresponding operation on a Bit4@
. Here's how that would
look:
((Bit@, Bit@) { Bit@; }) { (Bit4@, Bit4@) { Bit4@; }; } BinaryBitwise = ((Bit@, Bit@) { Bit@; } op) { (Bit4@ a, Bit4@ b) { Bit4@(op(a.3, b.3), op(a.2, b.2), op(a.1, b.1), op(a.0, b.0)); }; };
Now we can very conveniently define our And4
, Or4
, and Xor4
functions:
(Bit4@, Bit4@) { Bit4@; } And4 = BinaryBitwise(And); (Bit4@, Bit4@) { Bit4@; } Or4 = BinaryBitwise(Or); (Bit4@, Bit4@) { Bit4@; } Xor4 = BinaryBitwise(Xor);
What we saw in the previous section is an example of currying functions, where we replace a function of multiple arguments with a function that returns a function.
Internally fble treats every multi-argument function as a single argument
function that returns another function. The two definitions we gave for
BinaryBitwise
produce exactly the same function value from the runtime's
point of view. That means you can define BinaryBitwise
using whichever
of the following is your favorite:
# 3 argument function version of BinaryBitwise ((Bit@, Bit@) { Bit@; }, Bit4@, Bit4@) { Bit4@; } BinaryBitwise = ((Bit@, Bit@) { Bit@; } op, Bit4@ a, Bit4@ b) { Bit4@(op(a.3, b.3), op(a.2, b.2), op(a.1, b.1), op(a.0, b.0)); }; # Function returning function version of BinaryBitwise ((Bit@, Bit@) { Bit@; }) { (Bit4@, Bit4@) { Bit4@; }; } BinaryBitwise = ((Bit@, Bit@) { Bit@; } op) { (Bit4@ a, Bit4@ b) { Bit4@(op(a.3, b.3), op(a.2, b.2), op(a.1, b.1), op(a.0, b.0)); }; };
Both version of BinaryBitwise have the same function type and can be used interchangeably.
Because a multi-argument function is treated the same as a function that
returns a function, you can call a function without supplying all its
arguments. Using the BinaryBitwise
example:
((Bit@, Bit@) { Bit@; }, Bit4@, Bit4@) { Bit4@; } BinaryBitwise = ((Bit@, Bit@) { Bit@; } op, Bit4@ a, Bit4@ b) { Bit4@(op(a.3, b.3), op(a.2, b.2), op(a.1, b.1), op(a.0, b.0)); }; (Bit4@, Bit4@) { Bit4@; } And4 = BinaryBitwise(And); (Bit4@, Bit4@) { Bit4@; } Or4 = BinaryBitwise(Or); (Bit4@, Bit4@) { Bit4@; } Xor4 = BinaryBitwise(Xor);
The BinaryBitwise
function takes three arguments, but here we call it
with only one. The result is a function that takes the rest of the
arguments and returns the result of calling BinaryBitwise once all the
arguments are supplied.
We could implement a bitwise Not4 function by supplying one more argument:
(Bit4@) { Bit4@; } Not4 = BinaryBitwise(Xor, Bit4@(1, 1, 1, 1));
One thing to keep in mind is that in some cases partial application of a function requires allocating an intermediate function value, which may impact performance depending on how often it is done.
The flip side of partial function application is that, in the case of a function that returns a function, you can supply more arguments than the function is declared with.
In this case we define BinaryBitwise
using the one argument version,
but call it with three arguments:
((Bit@, Bit@) { Bit@; }) { (Bit4@, Bit4@) { Bit4@; }; } BinaryBitwise = ((Bit@, Bit@) { Bit@; } op) { (Bit4@ a, Bit4@ b) { Bit4@(op(a.3, b.3), op(a.2, b.2), op(a.1, b.1), op(a.0, b.0)); }; }; (Bit4@, Bit4@) { Bit4@; } And4 = (Bit4@ a, Bit4@ b) { BinaryBitwise(And, a, b); };
Some languages have a distinct notion of an anonymous function, sometimes called a lambda. In fble, every function value is defined using an anonymous function. No special syntax or construct is needed.
For example, instead of passing And
to our BinaryBitwise
function,
we could have passed an anonymous implementation of the And
function:
(Bit4@, Bit4@) { Bit4@; } And4 = BinaryBitwise( (Bit@ a, Bit@ b) { a.?(0: 0, 1: b); } );
We know that types are values. In theory that means we could pass type values as arguments to functions and return type values as results from functions if we want. In practice, there's no real use for this.
If you know the type of a type value, you know the value of that type value. You have to specify the type of all function arguments and the return type of the function. That means you know the value of the type value when defining the function, so there's no point in taking it as an argument to a function.
For example, maybe we want to define a function that takes a type value and
returns it as is. The first thing we need to know to define that function is
what the type of the type value is. Let's say it's the type of the type
value Int@
. Then we could write:
(@<Int@>) { @<Int@>; } F = (@<Int@> x@) { x@; }; @ AnotherInt@ = F(Int@);
But you may as well just write:
@ AnotherInt@ = Int@;
There is support for polymorphism in fble, which allows you to write
functions parameterized on types, but it is a different mechanism from
passing type values to function. See the tutorial on
Polymorphism
for more about polymorphism in fble.
You now know everything there is to know about functions in fble. To
reinforce this, read over section 5 of
the fble language specification
. Everything there should be familiar to you
now.
Head over to the Lists
tutorial to learn all about lists
in fble.