Comparing function handles across languages
I've worked with a couple of languages, but it was in JavaScript that I first noticed the pattern of passing functions around like any other value. I've been fascinated by this ever since (I think it's an amazing tool for code pluggability), and I finally got around to comparing how some common languages implement function handles.
"Function handles" is a term I use to refer to referencing callback functions that are defined outside the place they're used. Most of the languages I'll consider in this article allow you to write a callback function inline, as in defining it at the moment you're passing it to the other function. For instance, in JavaScript, this would be:
// Square each number
[1, 2, 3].map((x) => x * x) // [1, 4, 9]
What I'm more concerned with today is passing functions around. Assuming I want to use this square function in multiple places, I'd prefer to define it somewhere and pass it to any function that needs it. Let's see how different languages do that.
PHP: Callables
PHP uses the term "callable" to describe anything that can be created at one place and passed around and called somewhere else. In PHP, a callable can be represented in different ways:
Simple functions (user-defined or inbuilt) can be referred to via:
- the name of the function, as a string, or
- a variable containing the function (a closure).
For functions which belong to objects or classes (methods), we use:
- a tuple (array with two elements); the first element is the object or the name of the class, and the second is the method name, or
- the name of the function, as a string (only for static methods).
Here it is in action:
function square($n) {
return $n * $n;
}
// Same thing as square(5)
call_user_func_array('square', [5]);
// Passing the function to another
$numbers = [1, 2, 3];
$squares = array_map('square', $numbers); // [1, 4, 9]
// If square was a method on a Maths object
$maths = new Maths;
$squares = array_map([$maths, 'square'], $numbers);
// If square were a static method on the Maths class
$squares = array_map(['Maths', 'square'], $numbers);
// String syntax also supported for static methods
$squares = array_map('Maths::square', $numbers);
// As a variable
$square = fn($n) => $n * $n;
$squares = array_map($square, $numbers);
Bonus: PHP also supports invokable objects, objects that have an __invoke
method. They're objects, but you can call them like functions.
class NumberSquarer
{
public function __invoke($n)
{
return $n * $n;
}
}
$squarer = new NumberSquarer;
// You can invoke this object directly, like any function:
$squarer(2); // 4
// and pass it as a callable
$squares = array_map($squarer, $numbers);
Many PHP functions take callables, and you can write yours. But PHP's implementation of callables gives us a small problem: how do we determine if a provided string or array is a valid callable or not? Well, PHP provides some tools for that:
is_callable()
, which validates that a variable is in one of the above formats, and refers to an actual callable function- the
callable
pseudo-type, which can be used as a typehint when writing your functions
We can write some pretty cool code with these:
/**
* Map each value in an array to a new value
*/
function map(array $arr, callable $function) {
// $function is definitely a string/tuple/invokable object, etc
foreach ($arr as $item) {
// Doesn't matter what format $function is in
// We can call it with call_user_func_array
$result[] = call_user_func_array($function, [$item])
}
return $result;
}
/**
* Find the index of the first value in an array that matches a certain value or satisfies a condition
*/
function firstIndex(array $arr, $condition) {
if (is_callable($condition) {
// If a function was provided, return the first value for which the function returns true
foreach ($arr as $index => $item) {
if (call_user_func_array($condition, [$item])) {
return $index;
}
} else {
// Otherwise, return the first matching item
foreach ($arr as $index => $item) {
if ($item == $condition) {
return $index;
}
}
}
JavaScript: Functions are objects
In JavaScript, functions are objects (their prototype is Function
). So we can treat them like any other object or variable—pass them around, set properties, delete properties, etc.Sorta like PHP's invokable objects.
function square(n) {
return n * n;
}
let numbers = [1, 2, 3];
let squares = numbers.map(square); // [1, 4, 9]
// Static methods (assuming square is a method in class Maths)
let squares = numbers.map(Maths.square);
// Object methods
let maths = new Maths();
let squares = numbers.map(maths.square);
One important thing to keep in mind, though, when you pass an object method around this way, it doesn't keep its value of this
. So if you want it to use the same this
when it's called, you'll need to bind that first:
const math = {
times(a, b) {
return a * b;
},
square(n) {
// References `this`
return this.times(n, n);
}
}
let numbers = [1, 2, 3];
// This won't work, because math.square needs `this` to be set properly
let squares = numbers.map(math.square);
// This will work, since we've created a new function with `this` set to `math`.
let squares = numbers.map(math.square.bind(math));
Python & Go: First-class objects
Python's model is similar to JavaScript's: functions are first-class objects, and you can refer to them anywhere by name. You also don't need to do any binding to call an object method within its original context.
def square(n):
return n * n
numbers = (1, 2, 3)
squares = list(map(square, numbers)) # (1, 4, 9)
# Static methods (assuming square is a method in class Maths)
squares = list(map(Maths.square, numbers))
# Object methods
maths = Maths()
# This works even if maths.square references `self`
squares = list(map(maths.square, numbers))
You can also assign a lambda function to a variable and pass that:
square = lambda n: n * n
numbers = (1, 2, 3)
squares = list(map(square, numbers))
Bonus: Python also has callable objects (like PHP's invokable objects):
class NumberSquarer:
def __call__(self, n):
return n * n
squarer = NumberSquarer()
squares = list(map(squarer, numbers))
And there's also a callable()
function to check if an object (including functions) might be callable. Read more about Python's first-class functions here.
Go uses the same first-class object pattern as Python (Go has anonymous functions, which are akin to Python's lambdas).
Ruby: Three different mechanisms
In Ruby, the main units of code "pluggability" aren't functions, but blocks. Blocks are similar to inline functions: a couple of lines of code (with or without arguments) within braces (or a do...end construct). Most functions that perform an arbitrary operation on an object (like Array's map()
method) take a block. But blocks have a major limitation: they aren't objects and can't be assigned to variables or used at a different place from where they're written.
However, Ruby has lambdas, which can be passed around. If a function expects a lambda, it's easy:
def map(array, mapper)
result = []
array.each { |item| result.push(mapper.call(item)) }
return result
end
numbers = [1, 2, 3];
square = lambda { |n| n * n }
# This won't work because Array#map expects a block
numbers.map(square)
# This works because our map function specifically expects a lambda
squares = map(numbers, square) # [1, 4, 9]
For methods that expect blocks, we'll need to make use of a third concept, procs. Procs can be passed around like lambdas (lambdas are actually a type of proc), but what makes them really useful here is that they can be converted to blocks. In fact, we can (try to) convert anything to a block with the ampersand (&) operator. The operator will convert the object to a proc, then convert that to a block.
# Define a proc from a block
square = Proc.new { |n| n * n }
# Convert the proc to a block by using &
squares = numbers.map(&square)
# Using an existing function
def square(n)
n * n
end
squares = numbers.map(&method(:square))
In our above example, method(:square)
returns an instance of Method
. The &
then converts this object to a proc, then converts the proc to a block and passes that to the map method, and all is well.
It can get pretty confusing, I know. If it helps, here's a lovely article exploring the ampersand operator in more detail, and here's an intro to blocks, lambdas and procs in Ruby.
Java: Functional interfaces
Welp, we're entering into strongly-typed world now, so things are going to get more complicated. Fasten your seat belts.
Java only has methods (ie functions must belong to classes or objects). You can't reference a function without the containing object or class. In the past, you couldn't simply pass a method around. The process was::
- The thing that needs a callback (maybe you, maybe an external library) defines an interface with a single method (called a functional interface). This single method will contain the callback code they really want.
- You create an object that implements that interface and pass that object to the calling function.
- Internally, the calling function calls that method on the object you passed to it.
Luckily, Java 8 introduced method references, so it's much easier to pass a method around:
Here's how our squares example would work:
import java.util.Arrays;
import java.util.function.IntUnaryOperator;
class Maths
{
public static int square(int n) {
return n * n;
}
}
public class BecauseWeNeedAClass
{
public static void main(String []args) {
int[] numbers = {1, 2, 3};
int[] squares = Arrays.stream(numbers)
.map(Maths::square).toArray(); // [1, 4, 9]
// For an instance method
Maths maths = new Maths();
int[] squares = Arrays.stream(numbers)
.map(maths::square).toArray();
}
}
You can also assign a method reference or lambda expression to a variable and pass that round:
IntUnaryOperator square = Maths::square; // Method reference
IntUnaryOperator square = (n) -> n * n; // Lambda expression
int[] squares = Arrays.stream(numbers)
.map(square).toArray();
Internally, all this still uses functional interfaces. Lambda expressions and method references are syntactic sugar. We've actually passed an object implementing the functional interface IntUnaryOperator
, which extends Function
. This interface has one method, apply
, which contains our n * n
code. Under the hood, the map()
actually calls mapper.apply(item)
on each number. If you'd like to know more, check out this intro to functional interfaces and this guide to working with functional interfaces in Java 8.
C#: Delegates
For C#, callbacks are implemented via delegates. Delegates are similar to Java's functional interfaces. A function that wants a callback declares a delegate type, and the caller can pass in any function that satisfies the delegate's type signature: Let's see an example by writing our own map function:
using System.Collections.Generic;
// Our Map() function works on a list of items of type T.
// Our callback function, mapper, is a delegate of type `Func<T, T>`,
// meaning that it takes a value of type T and returns a result of the same type
List<T> Map<T>(List<T> items, Func<T, T> mapper)
{
var result = new List<T>();
foreach (T item in items)
{
result.Add(mapper(item));
}
return result;
}
var numbers = new List<int>(){1, 2, 3};
// We can pass in a lambda expression
var squares = Map(numbers, n => n * n); // [1, 4, 9]
// Or, using a regular function that satisfies Func<int, int>:
int Square(int n)
{
return n * n;
}
var squares = Map(numbers, Square);
Function handles in C# are like in JS, to an extent: we can reference a function by just writing its name, but what happens depends on the context. In the previous example, we passed Square
by name, and it was cast to a delegate to be used in Map()
.
We can assign any function to a variable and pass it to any requesting function, as long as we use the correct type. "The correct type" here is any type that satisfies (is a subtype of) the expected delegate type.
var numbers = new List<int>(){1, 2, 3};
// Assign a lambda expression to a variable
// ConvertAll<T> requires a delegate type of Converter<T, T>
Converter<int, int> square = n => n * n;
var squares = numbers.ConvertAll<int>(square); // [1, 4, 9]
// Use an existing function
// The function matches the type signature of Converter<T, T>,
// so we don't need to manually cast it.
int Square(int n)
{
return n * n;
}
var squares = numbers.ConvertAll<int>(Square);
// Or a static method:
var squares = numbers.ConvertAll<int>(Maths.Square);
// Or instance method
var maths = new Maths();
var squares = numbers.ConvertAll<int>(maths.Square);
C++: Function pointers
Historically, the direct equivalent to a function handle in C++ (and C) is a function pointer. Once you have a pointer to a function, you can pass it around as you wish.
// WARNING: it's C++, so expect the code to be a bit cryptic.
#include <vector>
#include <algorithm>
// Declare a function
int square(int n)
{
return n * n;
}
int main()
{
// Declare a pointer (we'll call it square_fn) to that function
int (* square_fn)(int) = □
// And pass it to a function that needs it:
std::vector<int> numbers{1, 2, 3};
std::vector<int> squares(numbers.size());
// Workaround; C++ doesn't have a map() function in its STL
std::transform(numbers.begin(), numbers.end(), squares.begin(), square_fn);
}
All function references in C++ end up as pointers to the function. So, if you were writing a function that wanted to accept a function pointer, you'd call the function as normal. The major addition would be writing the type declaration. Let's write a generic map() function to demonstrate:
template <typename T, typename R>
std::vector<R> map(std::vector<T> items, R (*mapper)(T))
{
std::vector<R> result;
for (auto item: items)
{
result.push_back(mapper(item)); // Use the function pointer like a normal function
}
return result;
}
int main()
{
int (* square_fn)(int) = □
std::vector<int> numbers{1, 2, 3};
// Pass in our `square` function pointer from earlier to our new map() function.
int (* square_fn)(int) = □
auto squares = map(numbers, square_fn);
}
We can also assign function pointers to static functions:
// If square is a static method on Maths
int (* square_fn)(int) = &Maths::square;
// We can omit the &, because function references are pointers
int (* square_fn)(int) = Maths::square;
We can assign pointers to member functions, but we'd need to change the type declaration in our receiving function (map()
).
auto maths = new Maths();
// This works, but square_fn is of type `int (Maths::*&)(int)`
// rather than `int (*)(int)` like our map() function wants, so the call fails
auto square_fn = &Maths::square;
// We can change map() to look like this:
std::vector<R> map(std::vector<T> items, std::function<R(T)> mapper)
{
// Same code as before
}
// This works.
auto square_fn = std::bind(&Maths::square, maths, std::placeholders::_1);
std::function
is a newer type, introduced in C++11, that allows us to accept a variety of functions and use them in the same way. The first parameter within the angled brackets is the return type of the function, and the second (within the parentheses) are the types of the function parameters. So a function like square
, that takes and returns an int will be std::function<int(int)>
.
With std::function
, our function now supports lambda expressions, and it still works with the old static method and simple functions:
auto square_fn = [](int n) -> int { return n * n; };
auto squares = map<int,int>(numbers, square_fn);
Fun fact: C++ actually has its own concept of callables, which can be written and used in a bunch of different ways. But that's beyond me!😅 You can check them out in this StackOverflow answer.
Closing thoughts
- I think my best approach is the JS/Python/Go way of functions as regular objects. It's pretty straightforward and has few surprises (minus some weird quirks with OOP in JS and Python).
- PHP's approach makes me chuckle at the "smarts", but I dislike that it's context-dependent and difficult to analyse statically. If I'm given a string, how do I know whether it's meant to be used as a function or treated as a normal string?
- I don't like Java's verbosity (are there folks who do?), but I truly love the method reference syntax that came in Java 8. Gets rid of a whole fuckton of boilerplate.🥰
- I hate that you have to know the exact expected type in Java and C# (strict typing🙄). In the Java example, I was using
Func<int,int>
at first (which is correct), but it refused to compile because it's a wider type thanIntUnaryOperator
. - It's kinda surprising how C and C++, languages you wouldn't ordinarily describe as "functional", have first-class function handles. Unfortunately, it's via pointers.
- I initially liked Ruby's blocks, but I think having blocks, procs and lambdas is a net negative. The differences are often confusing. Having procs and lambdas alone would be clearer.
I write about my software engineering learnings and experiments. Stay updated with Tentacle: tntcl.app/blog.shalvah.me.