Funk: Method Dispatch as Pattern Matching
Back in 2008, I wanted to see if I could make a useful language with the simplicity of Smalltalk. The result was Funk; a minimal, dynamically typed, hybrid functional and object oriented programming language.
Lambda functions are reminiscent of a Ruby blocks: {|x y| x + y}
is a function with two arguments x
and y
, and which returns the result of x + y
. Functions are curried, so the above expression expands to {|x| {|y| x + y}}
.
Pattern matching is available in function arguments. For example, the Fibonacci function could be written as:
:fib {
|0| 0
|1| 1
|n| fib(n - 1) + fib(n - 2)
}
The :fib
syntax defines a variable called fib
and assigns it the value of the right hand side expression {...}
. Recursion is implicit, and variables defined in the same scope can be mutually recursive. You can update variables with @
, eg. @x 42
updates x
so that it now contains the value 42
.
Object oriented programming
In Smalltalk, objects communicate by sending and receiving messages. The crucial distinction from other object oriented languages is that messages are values that can be manipulated (via doesNotUnderstand: message
), whereas methods in eg. Java are second-class citizens.
In lambda calculus, the closest thing we have to sending messages is to apply a function to an argument. A straightforward encoding of array.Size
is thus to call array
with the string "Size"
, eg. array("Size")
. This is exactly what Funk does; array.Size
is syntactic sugar for array("Size")
. If arguments follow, eg. array.Insert(5, "hello")
, it's desugared into array("Insert")(5)("hello")
.
For receiving messages, Funk uses its pattern matching facility. A pattern in funk is either a wildcard _
, a variable x
, an integer 7
or a string "Size"
. In funk, quotes can be omitted from strings that start with an uppercase letter and only contain alphanumeric characters, eg. Size == "Size"
.
That's really everything there is to Funk.
Example
In Java, we could define a 2D point like this:
public class Point {
private float x;
private float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
public Point add(Point p) {
return new Point(x + p.getX(), y + p.getY());
}
}
(Please don't get hung up on getters vs. no getters or on whether or not points can be added - it's not important for the example).
In Funk, the equivalent definition is this:
:newPoint {|x y| {
|X| x
|Y| y
|Add p| newPoint(x + p.X, y + p.Y)
}}
newPoint
is a function of x
and y
. When it's invoked, it returns an object, just like in Java. However, and object in Funk is just an ordinary function. Method dispatch is implemented by pattern matching:
|X| x
: When called with"X"
, return the value ofx
.|Y| y
: When called with"Y"
, return the value ofy
.|Add p| ...
: When called with"Add"
, return a function{|p| ...}
. When the returned function is called, return the value of...
.
:p1 newPoint(2, 3)
:p2 newPoint(4, 5)
:p3 p1.Add(p2)
Now p3.X == 6
and p3.Y == 8
. Recall that p1.Add(p2)
desugars into p1("Add")(p2)
.
No such method?
We could add a catch-all case to print to the screen whenever we receive a message that's not supported:
:newPoint {|x y| {
|X| x
|Y| y
|Add p| newPoint(x + p.X, y + p.Y)
|m| print("Sorry, I don't understand '" + m + "'.")
}}
Inheritance and delegation
Such a case can also be used to implement inheritance, by delegating unknown messages to the super object:
:newPoint3d {|x y z|
:point2d newPoint(x, y)
{
|Z| z
|Add p| newPoint3d(x + p.X, y + p.Y, z + p.Z)
|m| point2d(m)
}
}
So, newPoint3d
is a function of three arguments x y z
. Internally, it creates a 2D point using newPoint(x, y)
, which is going to be used like the super object in Java. It then returns the 3D point object.
Since point3d.Z
is a new method, we'll need to add a case for it here. Just like point2d.X
and point2d.Y
, it simply returns the corresponding value.
Add p
will have to be overridden, since it must take z
into account in addition to x
and y
. That's done simply by adding a case for it.
For all other messages (eg. method names) m
, delegate to the "super" object by simply calling it with the message: point2d(m)
.
Conclusion
Funk is a minimal language; it's lambda calculus with pattern matching and a few basic conveniences. There's no built-in knowledge of methods, overriding, inheritance, delegation or even objects. Nevertheless, all of these concepts are readily supported.