polar

Functions

Functions live in the functions zone. This page shows how to write them, pass them around and make them work for many types.

Declaring a function

functions
  add(a: Int, b: Int) -> Int {
    a + b
  }

A function has a name, typed parameters, an optional return type after ->, and a body. The body's last expression is the result.

Parameters always need types. The return type is optional: leave it out and Polar works it out from the body.

double(n: Int) {
  n * 2
}

Note: Writing the return type does one more thing: it promises the function is pure, unless you list its effects too. We'll come back to this in Effects. For now, a good habit is to write return types on exported functions.

Calling functions

Call a function with parentheses. Functions from other modules are reached through the module name, like List.map. Functions can call themselves:

factorial(n: Int) -> Int {
  if n <= 1 { 1 } else { n * factorial(n - 1) }
}

Anonymous functions

function(…) { … } makes a function without a name. You'll mostly use these as arguments:

List.filter(tasks, function(t) { !t.done })

Their parameter types are usually inferred from where they're used, so you rarely write them. You can also store one in a let:

let shout = function(s) { String.uppercase(s) }

Functions are values

A named function can be passed anywhere a function is expected, without wrapping it:

[1, 2, 3] |> List.map(double) |> List.map(Int.to_string)

A function type is written function(Int) -> Int. Here's a function that takes another function:

apply_twice(f: function(Int) -> Int, x: Int) -> Int {
  f(f(x))
}

apply_twice(double, 5) is 20.

Generic functions

A lowercase name in a type, like a, is a type variable. It stands for "any type", so the function works for all of them. You don't declare type variables; you just use them.

types
  Pair<a> = { first: a, second: a }

functions
  swap(p: Pair<a>) -> Pair<a> {
    { first: p.second, second: p.first }
  }

swap works on a Pair<Int>, a Pair<String> and so on. The type checker makes sure both fields have the same type, and that the result does too.

Destructuring parameters

A parameter can be a pattern that pulls fields out of a record:

types
  Entry = { key: String, value: Int }

functions
  describe({ key: k, value: v }: Entry) -> String {
    "#{k}=#{v}"
  }

All together

module Funcs

uses
  Std.List

types
  Entry = { key: String, value: Int }

  Pair<a> = { first: a, second: a }

functions
  add(a: Int, b: Int) -> Int {
    a + b
  }

  double(n: Int) {
    n * 2
  }

  apply_twice(f: function(Int) -> Int, x: Int) -> Int {
    f(f(x))
  }

  describe({ key: k, value: v }: Entry) -> String {
    "#{k}=#{v}"
  }

  swap(p: Pair<a>) -> Pair<a> {
    { first: p.second, second: p.first }
  }

  factorial(n: Int) -> Int {
    if n <= 1 { 1 } else { n * factorial(n - 1) }
  }

  main() {
    let shout = function(s) { String.uppercase(s) }
    let p: Pair<String> = { first: "left", second: "right" }

    Log.info("#{add(2, 3)} #{double(21)} #{apply_twice(double, 5)}")
    Log.info(shout("quiet"))
    Log.info(List.join(List.map([1, 2, 3], double) |> List.map(Int.to_string), ", "))
    Log.info(describe({ key: "a", value: 1 }))
    Log.info(swap(p).first)
    Log.info("10! = #{factorial(10)}")
  }

exports
  main
$ polar run funcs.px
5 42 20
QUIET
2, 4, 6
a=1
right
10! = 3628800

Next: records, the main way to group data in Polar.