Functional programming with lambda.r

[This article was first published on Cartesian Faith » R, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

After a four month simmer on various back burners and package conflicts, I’m pleased to announce that the successor to futile.paradigm is officially available on CRAN. The new package is lambda.r (source on github), which hopefully conveys the purpose of the library better than its whimsical predecessor. In some ways this new version deserves a more serious name as the package has matured quite a bit not to mention is part and parcel of a book I’m writing on computational systems and functional programming.

So what exactly is lambda.r? Put simply, lambda.r gives you functional programming in the R language. While R has many functional features built into the language, application development in R is a decidedly object-oriented affair. I won’t go into all the reasons why it’s better to write computational systems in a functional paradigm since that is covered in depth in my forthcoming book “Computational Finance and the Lambda Calculus”. However, here are the salient points:

  • Conceptual consistency with mathematics resulting in less translation error between model and system (see my slides from R/Finance 2011)
  • Modular and encapsulated architecture that makes growth of a system easier to manage (not to mention easier to accommodate disparate computing needs — think parallel alternatives of the same processing pipeline)
  • Efficiency in application development since wiring is trivial

Features

The fundamental goal of lambda.r is to provide a solid architectural foundation that remains intact through the prototyping and development phases of a model or application. One half is accomplished with a functional syntax that builds in modularity and encapsulation into every function. The other half is through the incremental adoption of constraints in the system. This article will focus primarily on the features, and in a separate article I will outline how to best leverage these features as a system matures.

Pattern Matching

Functional languages often use pattern matching to define functions in multiple parts. This syntax is reminiscent of sequences or functions with initial values in addition to multi-part definitions. Removing control flow from function definitions makes functions easier to understand and reduces the translation error from math to code.

Fibonacci Sequence

For example, the ubiquitous Fibonacci sequence is defined as

F_{n} = F_{n-1} + F_{n-2} , where F_{1} = F_{2} = 1

In standard R, one way to define this is with an if/else control block or function [1].

fib <- function(n)
{
  ifelse(n < 2, 1, fib(n - 1) + fib(n - 2))
}

Using lambda.r, pattern matching defines the function in three clauses. The behavior of the function is free of clutter as each clause is self-contained and self-explanatory.

fib(0) %as% 1
fib(1) %as% 1
fib(n) %as% { fib(n-1) + fib(n-2) }

Heaviside Step Function

When represented as a piecewise constant function, the Heaviside step function is defined in three parts. [2]

Using pattern matching in lambda-r, the function can be defined almost verbatim.

h.step(n) %when% { n < 0 } %as% 0
h.step(0) %as% 0.5
h.step(n) %as% 1

In languages that don’t support pattern matching, again if/else control structures are used to implement these sorts of functions, which can get complicated as more cases are added to a function. A good example of this is the ‘optim’ function in R, which embeds a number of cases within the function definition.

Guard Statements

The last example sneaks in a guard statement along with pattern matching. Guards provide a rich vocabulary to control when a specific function clause is executed. Each guard statement is a logical expression. Multiple expressions can be present in a guard block, so that the function clause only executes when all the expressions evaluate to TRUE. Using the Fibonacci example above, we can add an argument check to only allow integers.

fib(0) %as% 1
fib(1) %as% 1
fib(n) %when% {
  is.integer(n)
  n > 1
} %as% { fib(n-1) + fib(n-2) }

If none of the clauses are satisfied, lambda.r will complain telling you that it couldn’t find a matching function clause.

> fib(2)
Error in UseFunction("fib", ...) : No valid function for 'fib(2)'
> fib(as.integer(2))
[1] 2

Note: If you are running the examples as you are reading along, then you need to either seal() the functions or rm() the current definition prior to redefining the function. The reason is that function clauses are additive. You can add as many clauses as you want, and they will be evaluated in the order they were declared. Since lambda.r has no way of knowing when you are done defining your function you must explicitly tell it via the seal() function.

Types

Custom types can be defined in lambda.r. These types can be used in type constraints to provide type safety and distinguish one function clause from another. All types must be defined using PascalCase.

Type Constructors

A type constructor is simply a function that creates a type. The name of the function is the name of the type. The return value will automatically be typed while also preserving existing type information. This means that you can create type hierarchies as needed.

Point(x,y) %as% list(x=x,y=y)
Polar(r,theta) %as% list(r=r,theta=theta)

In this example we use a list as the underlying data structure. To create an instance of this type simply call the constructor.

point.1 <- Point(2,3)
point.2 <- Point(5,7)

Under the hood lambda.r leverages the S3 class mechanism, which means that lambda.r types are compatible with S3 classes.

Type Constraints

Types by themselves aren’t all that interesting. Once we define the types, they can be used as constraints on a function.

distance(a,b) %::% Point : Point : numeric
distance(a,b) %as% { ((a$x - b$x)^2 + (a$y - b$y)^2)^.5 }

distance(a,b) %::% Polar : Polar : numeric
distance(a,b) %as%
{
  (a$r^2 + b$r^2 - 2 * a$r * b$r * cos(a$theta - b$theta))^.5
}

As shown above each function clause can have its own constraint. Since type constraints are greedy, a declared constraint will apply to every successive function clause until a new type constraint is encountered.

> distance(point.1, point.2)
[1] 5

Attributes

Types are great for adding structure and safety to an application. However types can have diminishing returns as more types are introduced. In general lambda.r advocates using existing data structures where possible to minimize type clutter. Of course if data.frames and matrices are used for most operations, how do you differentiate function clauses? The answer of course are attributes, which come standard with R. Attributes can be considered meta-data that is orthogonal to the core data structure. They are preserved during operations, so can be carried through a process. Lambda.r makes working with attributes so easy that they should become second nature fairly quickly.

With lambda.r you can access attributes via the ‘@’ symbol. Define them in a type constructor as shown below.

Temperature(x, system='metric', units='celsius') %as%
{
  x@system <- system
  x@units <- units
  x
}

Function clauses can now be defined based on the value of an attribute.

freezing(x) %::% Temperature : logical
freezing(x) %when% {
  x@system == 'metric'
  x@units == 'celsius'
} %as% {
  if (x < 0) { TRUE }
  else { FALSE }
}

freezing(x) %when% {
  x@system == 'metric'
  x@units == 'kelvin'
} %as% {
  if (x < 273) { TRUE }
  else { FALSE }
}

It is trivial then to check whether a given temperature is freezing, based on the units. This approach can be extended to objects like covariance matrices to preserve information that is normally lost in the creation of the matrix (e.g. number of observations).

ctemp <- Temperature(20)
freezing(ctemp)

Note that the Temperature type extends the type of ‘x’, so it is also a numeric value. This means that you can add a scalar to a Temperature object, and everything behaves as you would expect.

> ctemp1 <- ctemp - 21
> freezing(ctemp1)
[1] TRUE

The combination of types and attributes are two essential tools in the lambda.r toolkit. In this section I’ve also illustrated how S3 classes can naturally be mixed and matched with lambda.r classes.

Introspection

The goal of lambda.r is to provide rich functionality with a simple and intuitive syntax. To accomplish this there is a lot of wiring behind the scenes. While most of the implementation can safely be ignored, there are times when it is necessary to look under the hood for troubleshooting purposes. Lambda.r provides a number of tools to make debugging and introspection as simple as possible.

The default output of a lambda.r function or type gives a summary view of the function clauses associated with this function. This is an abridged view to prevent long code listings from obscuring the high level summary. Any type constraints and guard statements are included in this display as well as default values.

> freezing
<function>
[[1]]
freezing(x) %::% Temperature:logical 
freezing(x) %when% {
  x@system == "metric"
  x@units == "celsius"
} %as% ...
[[2]]
freezing(x) %::% Temperature:logical 
freezing(x) %when% {
  x@system == "metric"
  x@units == "kelvin"
} %as% ...

Index values prefix each function clause. Use this key when looking up the definition of an explicit function clause with the ‘describe’ function.

> describe(freezing,2)
function(x) { if ( x < 273 ) {
TRUE
}
else {
FALSE
} }
<environment: 0x7f8cfcd7de60>

Debugging

Since lambda.r implements its own dispatching function (UseFunction), you cannot use the standard ‘debug’ function to debug a function clause. Instead use the supplied ‘debug.lr’ and ‘undebug.lr’. These functions will allow you to step through any of the function clauses within a lambda.r function.

Examples

All examples are in the source package as unit tests. Below are some highlights to give you an idea of how to use the package.

Prices and Returns

This example shows how to use attributes to limit the scope of a function for specific types of data. Note that the definition of Prices makes no restriction on series, so this definition is compatible with a vector or data.frame as the underlying data structure.

Prices(series, asset.class='equity', periodicity='daily') %as%
{
  [email protected] <- asset.class
  series@periodicity <- periodicity
  series
}

returns(x) %when% {
  [email protected] == "equity"
  x@periodicity == "daily"
} %as% {
  x[2:length(x)] / x[1:(length(x) - 1)] - 1
}

Taylor Approximation

This is a simple numerical implementation of a Taylor approximation.

fac(1) %as% 1
fac(n) %when% { n > 0 } %as% { n * fac(n - 1) }

d(f, 1, h=10^-9) %as% function(x) { (f(x + h) - f(x - h)) / (2*h) }
d(f, 2, h=10^-9) %as% function(x) { (f(x + h) - 2*f(x) + f(x - h)) / h^2 }

taylor(f, a, step=2) %as% taylor(f, a, step, 1, function(x) f(a))
taylor(f, a, 0, k, g) %as% g
taylor(f, a, step, k, g) %as% {
  df <- d(f,k)
  g1 <- function(x) { g(x) + df(a) * (x - a)^k / fac(k) }
  taylor(f, a, step-1, k+1, g1)
}

Use the following definitions like so:

> f <- taylor(sin, pi)
> v <- f(3.1)

References

[1] Julia, I Love You

[2] Heaviside Step Function


To leave a comment for the author, please follow the link and comment on their blog: Cartesian Faith » R.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)