Some functional programming tricks and gotchas

I took some quick notes comparing procedural and functional programming approaches after listening to Brian Lonsdorf's talk about Underscore.js. Currently, I'm not very enthusiastic about pure functional programming. I'm leaning towards the mainstream opinion which banishes it into the role of syntactic sugar in largely procedural and object-oriented libraries, given the huge amount of existing non-functional code out there.

In procedural programming you invoke functions with arguments. In functional programming you apply functions to arguments (or more generally, to some data structures).

In procedural programming there is a strict time and space separation between defining functions and using (invoking) them. In functional programming it's very common to not just pass around functions as arguments or return values, but also to define new functions by invoking other functions. Established patterns for function generation exist.

Perhaps the most strikingly confusing assumption when looking at FP code through eyes of a procedural programmer is that an expression f(something) represents a runtime invocation of f, which returns some data structure. This assumption, universally true in procedural world, does not hold in functional programming where such expressions commonly represent functions (and are possibly even subject to static optimizations by compilers). Unfortunately, no naming conventions seem to exist that would signify whether a function returns another function or data structure (which seems to me like a rather crucial distinction for aiding code comprehension). Tip: if you see the expression f(x) in functional programming code, try comprehending the whole (and not just f) as a function name.

In procedural programming it's usual that functions are uniquely named and static (they aren't created or destroyed at runtime). In functional programming anonymous temporary functions are common.

One common pattern found in functional programming is creating a new function based on another by fixing the first few arguments to constant values ("partial function application"). Argument lists are intentionally designed to allow this. The rule of thumb is to place the more variable arguments later in an argument list. More specifically, arguments that represent data to be processed should appear after such that represent functions to be applied to the data. This lets you specialize more general functions by replacing their function arguments with more concrete functions of your own.

A programming technique to support the argument fixing trick is known as "currying". Curry is an operation that can be (mechanically) applied to any function with multiple arguments. It returns a semantically equivalent function of the first argument, which in turn returns a function of the second argument, and so on. For example, currying a function of three arguments f(x,y,z) yields f', which you can then apply as follows: f'(x)(y)(z). You can now used this f' to easily create a function f(X,Y,z), with X and Y being fixed constants - simply by invoking f'(X)(Y).

Another common pattern for creating new functions from others is composition. You use composition if you want a function which applies some function to the result to another (a nested function invocation, in procedural terms). More generally, you can compose n functions (i.e. multiple levels of nested invocation). compose(f,g,h) will give you a new function which will produce the same result as f(g(h(...))) when applied.

The 'map' function, commonly found in functional programming languages as an idiom for processing a list of elements by applying some argument function, can be usefully generalized by allowing it to process not just lists but other also other objects known as "functors". To be supported as argument for map, such an object must provide an own implementation of 'map', which is then used in place of the default one to return a list of elements. Useful idioms that can be implemented this way include Maybe(x) and Either(x,y) of Haskell. The Maybe object applies the mapped function to its argument only if it is not null. The Either object applies to the mapped function to one of its arguments (allowing that one of them is not null).

No comments:

Post a Comment