Functions

When you want to bundle up a reusable chunk of code in Magpie, you'll usually use a method. But sometimes you want a chunk of code that you can pass around like a value. For that, you'll use a function. Functions are first-class objects that encapsulate an executable expression.

Creating Functions

Functions are defined using the fn keyword followed by the expression that forms the body of the function.

fn print("I'm a fn!")

This creates an anonymous function that prints "I'm a fn!" when called. The body of a function can be a single expression like above or can be a block.

fn
    print("First!")
    print("Second!")
end

Parameters

To make a function that takes an argument, put a pattern for it in parentheses after the fn keyword.

fn(name, age) print("Hi, " + name + ". You are " + age + " years old.")

Like with methods, any kind of pattern can be used here. Go crazy.

Implicit Parameters

When programming in a functional style, you often have lots of little functions that just call a method or do some trivial expression. Here's a line of code to pull the even numbers from a collection:

val evens = [1, 2, 3, 4, 5] where(fn(n) n % 2 == 0)

To make this a little more terse, Magpie supports implicit parameters. The above code can also be written:

val evens = [1, 2, 3, 4, 5] where(fn _ % 2 == 0)

Note that the parameter pattern is gone, and n in the body has been replaced with _.

The rule for implicit parameters is pretty simple. If a function has no parameter pattern, then a pattern will be created for it. Every _ that appears in the body of the function will be replaced with a unique variable for each occurrence. Then a pattern will be created that defines those variables in the order that they appear.

The "unique variable" and "order that they appear" parts are important here, since you can have multiple implicit parameters. When you do, each _ becomes its own parameter for the function.

fn (_ + _) / _

This creates a function with three separate implicit parameters. It's equivalent to:

fn(a, b, c) (a + b) / c

Implicit parameters can help code be more readable when the function body is small and the parameters are obvious from the surrounding context. But they can also render your code virtually unreadable (like the above example here) otherwise. Like all pointy instruments, wield it with care.

Calling Functions

Once you have a function, you call it by invoking the call method on it. The left-hand argument is the function, and the right-hand argument is the argument passed to the function.

var greeter = fn(who) print("Hi, " + who)
greeter call("Fred") // Hi, Fred

If a function doesn't take an argument, then there won't be a right-hand argument to call.

var sayHi = fn print("Hi!")
sayHi call

Like methods, the argument pattern for a function may include tests. If the argument passed to call doesn't match the function's pattern, it throws a NoMethodError.

var expectInt = fn(n is Int) n * 2
expectInt call(123) // OK
expectInt call("not int") // Throws NoMethodError.

If you pass too many arguments to a function, the extra ones will be ignored.

var takeOne = fn(n) print(n)
takeOne("first", "second") // Prints "first".

However, if you pass too few, it will throw a NoMethodError.

var takeTwo = fn(a, b) print(a + b)
takeOne("first") // Throws NoMethodError.

Returning Values

A function automatically returns the value that its body evaluates to. An explicit return is not required:

var name = fn "Fred"
print(name call) // Fred

If the body is a block, the result is the last expression in the block:

var sayHi = fn
    print("hi")
    "result"
end
sayHi call // Prints "hi" then returns "result".

If you want to return before reaching the end of the function body, you can use an explicit return expression.

var earlyReturn = fn(arg)
    if arg == "no!" then return "bailed"
    print("got here")
    "ok"
end

This will return "bailed" and print nothing if the argument is "no!". With any other argument, it will print "got here" and then return "ok".

A return expression with no expression following the keyword (in other words, a return on its own line) implicitly returns nothing.

Closures

As you would expect, functions are closures: they can access variables defined outside of their scope. They will hold onto closed-over variables even after leaving the scope where the function is defined:

def makeCounter()
    var i = 0
    fn i = i + 1
end

Here, the makeCounter method returns the function created on its second line. That function references a variable i declared outside of the function. Even after the function is returned from makeCounter, it is still able to access i.

var counter = makeCounter()
print(counter call // Prints "1".
print(counter call) // Prints "2".
print(counter call) // Prints "3".

Callables

The call method used to invoke functions is a regular multimethod with a built-in specialization for functions. This means you can define your own "callable" types, and specialize call to act on those. With that, you can use your own callable type where a function is expected and it will work seamlessly.