Programs involve code and data. High level languages provide us with various abstraction capabilities that allow us to create our own higher-level code and data structures.
Generally code is made up of statements and expressions. The standard abstraction for extending the statements and expressions provided by a programming language is the programmer defined function. A function returning a value may be used as an expression. Functions may also be used as statements, where the return value is simply ignored. Thus the programmer may use the function abstraction to extend the set of expressions and statements available for programming.
But what if we want to define our own control statements? One measure of how 'high-level' a language is would be to ask if there are any abstractions for defining your own control statements.
Control statements control the execution of some other block of code. Providing some abstraction capability for control statements would allow me to define my own control statements which would extend the set of control statements available for programming.
For example, let's suppose that I develop some kind of fancy collection of arbitrary objects. I might want to provide a way of handing each of the objects to a block of code, one at a time. What I envision is something like:
for obj in my_collection { <block of code that does something with obj> }
Generally control statement abstractions revolve around the ability to both define inline anonymous nested functions (these become the blocks of code to control) along with the ability to pass functions as parameters to other functions (the controlling functions).
So what I might end up with would be something like:
my_collection.enumerate( fn(obj) { <block of code that does something with obj> })
Note that once I've got this enumerate function, I can also use it on normal top-level functions, by simply passing the function:
my_collection.enumerate(print)
MiniMe provides several different notions of 'function' and allows you to use them interchangebly. For example, you should be able to pass any kind of function to enumerate, above.
These are the kind of function supported by all programming languages. For example, global functions or stand-alone functions. These represent the cases where you directly refer to the function body in the call to the function. (This also includes nested functions).
Member functions are the standard object-oriented notion of function call. In this case, you call a name and at run-time the name is looked up in the object's class to get the function to execute. So there is an indirection between the name called and the function body executed. Each time the call is done, it is possible to execute different function bodies.
Standard functions and member functions only get the ball once and then they're done.
Caller Function ------ -------- | +------>+ | / | call ---+ | + <---+ | | \ | | \ | | +--- return | V
Coroutines are two functions that are passing the ball back and forth to each other many times.
This is done by having the coroutine use a 'call' to get back to the caller, rather than a 'return'. The parameters in the call are passed back to the caller as return parameters and the coroutine is left waiting for more data. This is supplied by the caller the next time it does a call, which resumes the coroutine at the point it left off rather than restarting it.
Caller Coroutine ------ --------- | +------>+ | / | call ---+ | + <---+ | | \ | | \ | | +---- call | +------->+ call ---+ | + <---+ | | \ | | +----- call | (etc)
Note that, to the caller, the coroutine looks like a normal function. Thus where a function is expected as a parameter, a coroutine may be passed instead.
Coroutines may be chained together, much like a synchronous version of unix pipes. Here is an example of a caller calling two coroutines that have beene chained together.
Caller Coroutine A Coroutine B ------ ----------- ----------- | +------>+ +---->+ | / | / | call ---+ | / | +----->+ | / | | | | / call -----+ | | | / +---->+ | | | call ---+ / | | | | +------->+ / | | | call ---+ | / | | | +-->+ | / call --+ | | | | | / | | | | | call ---+ | | | | | | | | | (etc) | | | | | | | +--------------------------------------------+ | | | +--------------------------------------------------+
Note that the coroutine chain still looks like a normal function to the caller. Thus, we could also pass a coroutine chain where a function is expected as a parameter.
A composed function combines two functions into one through functional composition. The functional composition operator is the letter 'o'. For example, functions f and g would be composed by writing 'f o g'. When the composed function is called, say with parameter x, the result is f(g(x)). Note that the functions are written in the reverse order of execution (g is run first, than f)!
Of course, function composition can go on to any length: f o g o h o i.
The compose operator can only take a member function as the second function (e.g, normal_fun o member_fun).
A composed function may be used wherever a function is expected.
You may have noticed that combining normal functions through composition is much like chaining coroutines. You can use function composition to add normal functions to coroutine chains by specifying the coroutine chain in the first position and the normal function in the second position: coroutine_fun o normal_fun. Another coroutine can then be added to this by treating this as a coroutine chain.
Thus you may combine normal functions and coroutines any order to form a higher-level function that may then be used wherever a function is expected.
Curried functions are formed by specifying some of the arguments for some other function and leaving the other arguments open. For example, we may have a function, f, that takes 3 arguments and want to pass it to a function, g, that's expecting a function taking 2 arguments. To do this, we can pre-specify one of the 3 arguments for f, say the first one, as always having the value "mom". The result of the curry on f is a function that takes 2 arguments, which we can then pass to g. When g calls the curried function it passed 2 arguments, say "dad" and "dog". The curried function adds "mom" and calls f with "mom", "dad", and "dog".
Any kind of function may be curried. This works for member functions as well as composed functions and coroutines. You may also compose curried functions and curry individual coroutines within a coroutine chain (in addition to the entire chain itself).
A curried function may be used wherever a function is expected.