Functions

GitHub   Edit on GitHub

Functions are essential to any language. They allow us to reuse code and solve complex problems.


Defining Functions

Defining a named function is similar to naming any other value in Grain.

1
2
3
module Main

let add = (x, y) => x + y

A function can perform a series of actions. One thing to note about functions in Grain is that by default they return the result of the final expression in the function body, without needing an explicit return statement.

1
2
3
4
5
6
7
module Main

let logAndAdd = (x, y) => {
print(x)
print(y)
x + y
}

Calling Functions

Functions can be called with each argument passed either positionally or by name:

1
2
3
4
5
6
let add = (x, y) => x + y

// The following are equivalent
add(10, 20)
add(x=10, y=20)
add(y=20, x=10)

Functions as First Class Citizens

Since functions are just like any other values in Grain, they can be passed as arguments to other functions.

1
2
3
4
5
6
7
8
module Main

let doMath = (fn, x, y) => fn(x, y)

let multiply = (x, y) => x * y
let subtract = (x, y) => x - y

doMath(multiply, 4, 6) // 24

Furthermore, functions can return functions themselves!

1
2
3
4
5
6
7
8
9
10
module Main

let addTo = num1 => {
num2 => {
num1 + num2
}
}

let addTo5 = addTo(5)
print(addTo5(10)) // 15

Returning multiple Values

You can use tuples to return multiple values from functions.

1
2
3
4
5
6
7
8
9
module Main

let translateCoordinates = (x, y) => {
(x + 4, y + 4)
}

let (x, y) = translateCoordinates(1, 2)
print(x) // 5
print(y) // 6

Recursive Functions

We can define recursive functions using the rec keyword. Recursive functions are a key part of Grain, so remember to use let rec when necessary!

1
2
3
4
5
6
7
8
9
module Main

let rec fibonacci = (n) => {
if (n == 0 || n == 1) {
n
} else {
fibonacci(n - 1) + fibonacci(n - 2)
}
}

Early return

The return keyword can be used to explicitly cut the execution of a function short. Note that if return is used somewhere in a function, the remaining places where a value is returned must also use the return keyword

1
2
3
4
5
6
7
8
module Main

let isEven = n => {
if (n % 2 == 0) {
return true
}
return false
}

return can also be used without a value, in which case void is returned implicitly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module Main

let fizzBuzz = num => {
if (num % 15 == 0) {
print("FizzBuzz!")
return
}
if (num % 3 == 0) {
print("Fizz!")
return
}
if (num % 5 == 0) {
print("Buzz!")
return
}
return
}

fizzBuzz(9) // prints "Fizz!"

Infix Operators

Custom infix operators can be defined like regular functions, with the desired operator surrounded by parentheses.

1
2
3
4
5
module Main

let (*+*) = (a, b) => (a * a) + (b * b)

let value = 3 *+* 4 // 25

Default Arguments

Function parameters can be given a default value, and if the caller does not supply an argument value the default will be used. Note that if a parameter has a default value, the corresponding argument must be passed by name.

1
2
3
4
5
6
module Main

let addWithDefault = (x, y=0) => x + y

addWithDefault(10, y=5) // 15
addWithDefault(10) // 10

Parameters with default arguments can be placed anywhere in the parameter list. Furthermore, positional arguments supplied to the function when invoked will only be applied to required parameters.

1
2
3
4
5
6
7
8
module Main

let printWithDefaults = (first="First", middle, last="Last") => {
print(first ++ ", " ++ middle ++ ", and " ++ last)
}

printWithDefaults("Middle") // "First, Middle, and Last"
printWithDefaults(x="A", z="C", "B") // "A, B, and C"

Closures

Grain functions have access to values defined in their enclosing scope(s). In technical terms, Grain will automatically create a closure for you when a function uses a value defined outside of its parameter list.

1
2
3
4
5
6
7
8
9
10
11
12
module Main

let run = () => {
let mut toLog = "hello"
let log = () => {
print(toLog)
}

log() // hello
toLog = "world"
log() // world
}

The log function doesn’t define any bindings itself, but it has access to run‘s mutable binding toLog. When the log function is called, it utilizes the current value stored in toLog.

Furthermore, function closures will continue to “remember” values even when they’re used outside of their original scope. Here’s an example that makes a counter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module Main

let makeCounter = () => {
let mut count = 0
let increment = () => {
count += 1
print(count)
}
increment
}

let counter = makeCounter()
counter() // 1
counter() // 2
counter() // 3

The makeCounter function returns a counter function which will print sequential numbers when called.

This is a notification!