All About Functions

Functions are self-contained sets of statements meant to accomplish a particular task. They accept zero or more parameters and optionally return a result. They can then be called by other Functions or invoked as part of Expressions. For example, one might write a function to return a value from the Fibonacci sequence:

def fib(n)
  n if n < 2 else fib(n-1) + fib(n-2) 
end

Functions are first-class elements of Fate, meaning they can be passed around and assigned to variables.

Guarded Functions

The definition of a function can also be 're-opened' to apply guards using where clauses. The order in which functions are defined determines the order in which the guard clauses are evaluated, where the most recently defined will be evaluated first. For example:

def fib(n)
  n
end

def fib(n) where n >= 2
  self(n-1) + self(n-2) 
end

In this case, if n was greater or equal to 2, the second variation of fib would be executed. Otherwise control would fall-through to the first. If the unguarded version of fib had been defined last, it would shadow the previous definition, thus short-circuiting its possible evaluation.

Re-opening a function applies only to the current scope (and any of its nested scopes). If you import a function from a module and then re-open it with a guard, the re-opened version will not be applied globally.

If a Function is defined with a guard, and it hasn't been re-opened, there is a chance that the guard will not be triggered. If this occurs, a run-time error will be raised.

Inline-Guards

Functions with a where clause such as the one above are particularly useful if the conditions are based on evaluations performed against multiple arguments of the Function. For simple checks against individual arguments, inline-guards provide a more concise method.

Inline-guards leverage Fate's pattern matching capability to create guards based on the arguments of a Function Definition. For example:

def printItem(type, name)
  { type, name } | "This is a %type named %name" | print
end

This function can be extended to deal with specific type values:

def printItem('developer', name)
  { name } | "Developers rock! Especially %name" | print
end

In this case, no local argument name is bound to the value. You can simply treat it as discarded. On the other hand, sometimes you're performing matching against complex values and you may need access to the entire value in the body of your function. To do this, you can alias it like so:

def renderPerson({type:'developer'} as person)
  person | '%name writes code'
end

def renderPerson({type:'banker'} as person)
  person | '%name steals money'
end

let me = {name:'Thom', type:'developer', age:42}
renderPerson(me)

Lambdas (-> or →)

A Lambda is a special type of Function. Specifically, it's one that can be included as part of an Expression. Unlike normal Function declarations, a Lambda only accepts named arguments and cannot include a guard. A Lambda is declared as follows:

lambda      : "(" arg_names? "->" statement+ ")"
            | lambda_args? "->" NL statement+
            | lambda_args? "->" statement

lambda_args : "(" arg_names? ")"
            | id

arg_names   : id ( "," id )*

Argument names are optional, meaning that a lambda can be kicked off just by using the arrow operator ->. Also, the parser will consume as many statements as it can, across multiple lines. Meaning it's your responsibility to contain a lambda using parentheses, when appropriate. For example:

# the parens keep the parser from grabbing the second assignment
let my_lambda = (x, y -> x + y)
let x = my_lambda(12, 10)

On the other hand, where one might normally include a lambda expression, this is not usually a problem. For example:

# the function call's parens will contain the lambda statement(s)
timeout(100, ->
  # go until we hit the closing paren
  do_stuff()
  do_more_stuff()
)

Note: You can also kick off a lambda using the arrow character . This character is pretty difficult to type directly into a keyboard, but is available via the Fate Packages for Visual Studio Code and Sublime Text in the form of snippets.

Function and Lambda Calls

Calling a Function or Lambda in Fate is like calling a function in JavaScript or similar languages. This means that if you provide too few arguments to a Function, they will be assumed to match the Nothing Pattern.

from pattern import Nothing

def func(arg1, arg2, arg3)
  print("arg3 not provided") if arg3 like Nothing
end

func(1, 2)  # third argument not provided

Variadic Arguments

The final argument of Functions or Lambdas can be declared as being variadic. Other languages refer to this as a 'rest parameter'. What this means is that all arguments from that point on will be aggregated into a single Array.

let sum = (vals*) -> reduce sum = 0 for val in vals select sum + val

sum(10, 20, 30) | io.print  # 60

In the case of Functions, the variadic argument can also accept a Pattern:

from pattern import NonEmptyString, ArrayOf, Number
let Numbers = ArrayOf(Number)

def concatSum(NonEmptyString as prefix, Numbers as vals*)
  reduce sum = 0
  for val in vals
    let sum = sum + val
  end
  { prefix, sum } | "%prefix is %sum"
end

concatSum("Sum", 10, 20, 30) | io.print  # Sum is 60

Call Binding (_)

Fate supports Function argument binding via wildcards (_). For Example:

from array import join
let j = join(_, ' -- ')
let a = ['joined','with','dashes']
"Result is %a|j"

Binding is also useful against functions when you want to pass them around for later invocation:

from layouts import mainLayout
from functions import printList
let printItems = printList(items, _)
mainLayout('A Title', printItems)

Now, when mainLayout invokes printItems(), the bound list in items will be rendered.

Piped Calls (|)

A piped call is an operator where the evaluated value of the left operand is passed as the sole argument to the right operand. The right operand must evaluate to a Function. These calls can be chained from left to right, where the result of each call is passed into the next right-hand operand.

from array import join
from string import title
let words = ['a', 'guide', 'to', 'fate']
words | join | title

Function Composition (o or ∘)

Functions in Fate can be composed using the o (or ) operator. What this means is that the Function operands will be chained such that the arguments will be used to invoke the left operand, and the result of that invocation will be passed into the right operand.

from io import print
let double = x -> x * 2,
    addOne = x -> x + 1

# the combined function will first double the input and then add one
let combined = double ∘ addOne

# same as let combined = x -> addOne(double(x))

10 | combined | print  # 21

Note that the composition operator was borrowed from Standard ML. But unlike SML, the execution of the functions is reversed, so it behaves more like Scala's andThen operator. This is in keeping with the left-to-right pipeline principle that Fate tends to leverage.

Function Combination (&&, ||)

Functions in Fate can be combined using the && and || operators. What this means is that the Function operands will be combined into a single Function whose arguments are passed through to the operand Functions. For example:

from io import print

let isGreaterThanOne = x -> x > 1,
    isLessThanTen = x -> x < 10,
    isInRange = isGreaterThanOne && isLessThanTen

5 | isInRange | print

Here, isInRange is a combined function. When invoked, its arguments ('5' in this case) will first be passed to isGreaterThanOne, and if the result of that Function is true, it will then be passed to isLessThanTen. The last result will always be the one returned.

Recursive Calls (self)

The current Function or Lambda invocation can be referred to using the self keyword. In most cases, this will also refer to the current Function or Lambda, but things change somewhat when you 're-open' a Function using guards. When that happens, self will refer to the guarded version of the Function that was initially invoked, no matter where control presently resides in the guard chain.

For example:

import io

def testFunc(val)
  # 'self' refers to the "Hello, World!" version of testFunc
  self("Hello, " + val + "!")
end

def testFunc("Hello, World!" as val)
  'success: ' + val
end

# Will fall through from the second to the first version of testFunc
testFunc("World") | io.print

What this means is that you can extend base Functions that perform recursion, while still leveraging your re-opened guard logic.

Note that with Functions defined using def you can also refer to the current Function using its name to perform recursion, but the behavior will be different. Specifically, it will only refer to the current version of the function, ignoring any subsequent guards.

results matching ""

    No results matching ""