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
, where
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% { series@asset.class <- asset.class series@periodicity <- periodicity series } returns(x) %when% { x@asset.class == "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)
Great work! I’ve been fascinated by the goings on of F# and Scala – thanks for bringing it to R.
LikeLike
Thanks for the kind words. Please let me know if you end up using it and what other features you are interested in. I have considered adding a check for side effects in function definitions but haven’t decided whether people would find that useful or not.
LikeLike
Man, that’s just brilliant!
I’ve started to learn R this year through some of coursera.org classes – and it was exactly this kind of thing I was missing coming from Haskell.
You, sir, are fucken awesome!
LikeLike
Thanks for the shout out. I’m a big believer in functional programming for analytical systems. The type declaration syntax is pulled from Haskell, while the guard syntax is inspired by Erlang. My hope is that the overall syntax and approach is consistent with R’s dynamic nature and idioms.
LikeLike
Pingback: Confident package releases in R with crant « Cartesian Faith
Pingback: R for Quants, Part I (B) « Cartesian Faith
I’m really looking forward to using this package! The type-system seems neater than S3 classes for a lot of the way I write code.
Is there a convenient way to define custom print methods for types? For example something analogous to print.point(p) -> cat(‘(‘, p$x, ‘,’, p$y, ‘/n’). Or is this achieved through the standard class systems manually?
LikeLike
Hey there. For custom print methods you can use a standard S3 generic as lambda.r is backwards compatible with S3.
print.Point <- function(p) sprintf("(%s,%s)\n", p$x, p$y)
LikeLike
Pingback: Infinite generators in R « Cartesian Faith
Hello,
I am new to R and am interested in learning about its functional programming capabilities. Your project looks to be interesting and pertinent to this. One thing is that Im not sure how to install lamda.r into R. I saw on github something about “Install from github using devtools: install_github(‘lambda.r’,’zatonovo’)”but Im not too familiar with github either. Could you please assist me in setting up lamda.r?
Thanks
Kenan
LikeLike
From CRAN, just run install.packages(‘lambda.r’) and follow the instructions regarding choice of mirror. If you use devtools, then you need to install that first, i.e. install.packages(‘devtools’).
Then from R,
You don’t need an account on github for this. Good luck!
LikeLiked by 1 person
I got the package to install but when I try the example for fib its not working:
this is the script:
fib(0) %as% 1
fib(1) %as% 1
fib(n) %as% { fib(n-1) + fib(n-2) }
I get this error:
Error: could not find function “%as%”
Execution halted
i also tried using the .libPaths function to get the package to work. that didnt work either.
pls help if you can.
LikeLike
You might want to brush up on your R a little. Try this first:
library(lambda.r)
Then the example will work.
LikeLike
Thanks. Im very green. youre right. : ]
LikeLike
Hi. Are you familiar with Haskell? A lot of the features you implement with lambda.r are similar to features from it. There is an example of how to create a binary search tree using haskell here: http://learnyouahaskell.com/making-our-own-types-and-typeclasses (its halfway down the page) Do you think it would be possible to implement a BST in a similar manner using R?
Thanks again.
LikeLike
I’m familiar with Haskell but not a big user of it. I did borrow some concepts from Haskell, although more come from Erlang. It’s probably possible to create a BST using a list in R and lambda.r syntax. I don’t know how fast it would be though.
LikeLike
thanks
LikeLike
even after using the seal function like seal(fib). it still errors out Error in UseFunction(fib, “fib”, …) : No valid function for ‘fib(4)’
LikeLike
this is confusing. for fib(1) works but fib(2) won’t and you need fib(as.integer(2)). this is lot more confusing than an if-else statement.
LikeLike
R help for is.integer says NOT to use is.integer to test if x is an integer. Instead it gives another function is.wholenumber to test if the number is indeed an integer.
LikeLike
The example you are referring to is meant to be illustrative. Keep in mind that it is purely for pedagogical purposes.
LikeLike
What version of R and lambda.r are you using? Here is mine:
> sessionInfo()
R version 3.0.2 (2013-09-25)
Platform: x86_64-apple-darwin12.5.0 (64-bit)
…
other attached packages:
[1] lambda.r_1.1.6
And the example:
> fib(0) %as% 1
> fib(1) %as% 1
> fib(n) %as% { fib(n-1) + fib(n-2) }
> fib(4)
[1] 5
LikeLike
Can you please also post the math of taylor approximation so i can clearly see the math to code translation.
LikeLike
Thank you for this great library.
It works in my computer but recursive function definitions take a lot of time to complete. For example, fib(100) takes more than 15 mins.
LikeLike
Thanks for the kind words. Yes, recursion is slow. At some point I’d like to implement tail recursion in R, but it’s non-trivial.
LikeLiked by 1 person
This is probably obvious to you, but here’s how I memoised
fib
when I was learning about the concept: https://gist.github.com/isomorphisms/cb65fec24ee4b13beccaLikeLike
So I wrote a naive version of fib, and it takes equally long (actually longer), so it’s less an issue with lambda.r and more about recursion in R.
LikeLiked by 1 person
One thing I tried from your tutorial (which may or may not make sense / tell you anything useful about user expectations):
as.integer( Integer(5) )
5L %isa% Integer
LikeLike
Just saw these comments. I don’t want to misinterpret what you are pointing out. Can you tell me what the issue is?
LikeLike
The way you used @, $, and attr in this package was brilliant!
LikeLike
Also love how much more descriptive
str(regress)
is thanstr(lm)
. (regress %as% lm
example in your docs)LikeLike
Thanks! I spent a lot of time working out a syntax that is intuitive and easy to use, so I’m glad someone appreciates it.
LikeLike