functional ff
a small, highly extensible, functional language with strong rust interoperability and native actor based concurrency.
# arithmetic is exact-rational; `**` is power
1 + 2 * 3 # 7
2 ** 10 # 1024
1 / 3 + 1 / 3 # 2/3 — no floating-point drift
# bindings are immutable within a scope; rebinding shadows
greeting = "hello"
println(greeting :: ", world")
# numbers, strings, bools
42
"abc" :: "def" # "abcdef"
true && !false # true
# `()` is the unit value
()
# three collections — lists, objects, sets. Lists are heterogeneous.
[1, "two", true]
{"lang": "ff", "version": 0.1}
{1, 2, 2, 3} # {1, 2, 3} — dedup
# `.` indexes lists by position and atom-keyed objects by name
[10, 20, 30].0 # 10
{:x: {:y: 7}}.x.y # 7
# use the `Object` module to access object with arbitrary keys
# objects and sets are unordered
squares = {1: 1, 2: 4, 4: 16}
Object.get 2 squares # 4
Object.put 5 25 squares # {1: 1, 2: 4, 4: 16, 5: 25}
# you can also use cons to put to objects
[5, 25] :: squares # {1: 1, 2: 4, 4: 16, 5: 25}
Layouts are forgiving: commas and newlines are interchangeable inside [],
{}, and call args.
xs = [
1
2, 3
4,
]
functions are values. => builds a lambda; parameters can be comma- or
space-separated, with or without parens.
inc = x => x + 1
add = (x, y) => x + y
add3 = a b c => a + b + c
inc(5) # 6
inc 5 # 6 — juxtaposition is application
add(3)(4) # 7 — everything is curried
add3 1 2 3 # 6
# partial application falls out of currying
add5 = add(5)
add5(10) # 15
# as a convenience, we define pipe `(|>)`, left composition `(<<)` and right composition `(>>)` in the prelude
[1, 2, 3] |> map(x => x * x) |> reduce((a, b) => a + b, 0) # 14
inc = x => x + 1
double = x => 2 * x
double_then_inc = inc << double
double_then_inc 3 # 7
inc_then_double = inc >> double
inc_then_double 3 # 8
match dispatches on shape. The scrutinee is optional — a bare match is a
one-argument function, which is the idiomatic way to define case-analyzing
functions.
describe = match
[] -> "empty",
[x] -> "one",
[x, y] -> "two",
_ -> "many"
describe([1, 2, 3]) # "many"
Cons-patterns peel one element off any sequence — list, string, or set:
sum = match
h :: t -> h + sum(t),
_ -> 0
sum([1, 2, 3, 4]) # 10
Destructuring also works in plain assignments, with ..rest for the middle or
the tail:
[head, ..tail] = [1, 2, 3, 4] # head = 1, tail = [2, 3, 4]
[x, y] = [10, 20]
{"lang": lang} = {"lang": "ff", "version": 0.1}
…and in function parameters, so a function can destructure its arguments
directly. Any pattern that works on the LHS of = works as a parameter —
including list, cons, and object patterns:
first = [a, ..] => a
key = [k, _] :: _ => k # peel the first entry of a object
name = {:name: n} => n
add = ([a, b], c) => a + b + c # multi-param mix
A parameter pattern that doesn't match raises a runtime error at the call site,
so for partial patterns (e.g. cons on a possibly-empty list) prefer match
with a fallback arm.
Guards refine an arm:
sign = match
n if n < 0 -> "neg",
0 -> "zero",
_ -> "pos"
optional arguments and variadic functions are not supported at the language level. it is trivial to implement on your own via pattern matching on a list.
for example, the stdlib assert function takes an optional msg and is implemented as
assert = args => (
[val, msg] = match args
[val,msg] -> [val,msg],
val -> [val, "assert failed"]
if val != default(val) then val else panic msg
)
parentheses with multiple statements form a block. the value of the last expression is the value of the block; inner bindings don't leak.
area = (
w = 4
h = 5
w * h
) # area = 20
abs = n => if n < 0 then -n else n
[a..b], [a..=b], and [a..] are lazy ranges. Because :: doesn't force
its tail, list combinators stream:
match map(x => x * 2, [0..]) # infinite range
a :: b :: c :: _ -> [a, b, c] # [0, 2, 4]
[0..] |> filter(x => x % 2 == 0) |> take 5 # [0, 2, 4, 6, 8]
:name is an atom — a self-evaluating constant that compares equal only to
itself. Pairs well with lists and pattern matching for tagged-union style.
:ok # :ok
:ok == :ok # true
:ok == :error # false
safe_div = (a, b) =>
if b == 0 then [:error, "div0"] else [:ok, a / b]
handle = match
[:ok, v] -> v,
[:error, msg] -> -1
handle(safe_div(10, 2)) # 5
handle(safe_div(10, 0)) # -1
atoms work anywhere a value does — list/set elements, object keys, and patterns.
objects keyed by atoms read back with .name:
m = {:lang: "ff", :fullname: "functionalff"}
m.lang # "ff"
{:lang: lang} = m # lang = "ff"
functions that may fail should return length 2 lists of atom, value.
e.g., [:ok, 1], [:error, "msg"]
this allows the caller to handle the cases as they wish via pattern matching.
safe_div = (a, b) =>
if b == 0 then [:error, "div0"] else [:ok, a / b]
handle = match
[:ok, v] -> v,
[:error, msg] -> -1
handle(safe_div(10, 2)) # 5
handle(safe_div(10, 0)) # -1
any sequence of operator characters can be a user-defined infix; precedence is
OCaml-style, picked from the first character (*///% bind tighter than
+/-, which bind tighter than =/</>, etc.).
prefix ops start with ? or ~.
(<|>) = (x, y) => if x != default(x) then x else y
"" <|> "fallback" # "fallback"
(~?) = x => default(x)
~?[1, 2, 3] # []
Wrapping any operator in parens turns it into a normal value:
plus = (+)
[1, 2, 3] |> reduce(plus, 0) # 6
import "path.ff" returns an atom-keyed object containing whatever the file
marked export — a module is just an object. As a bare statement (not the
RHS of =), an import splats those names into the current scope.
# math.ff
square = x => x * x
cube = x => x * x * x
export square, cube
# main.ff
import "math.ff"
square(7) # 49
# or keep it namespaced
M = import "math.ff"
M.cube(3) # 27
ff has an actor based concurrency model inspired by erlang.
actors are sequential "processes" that communicate via message passing (see Communicating Sequential Processes).
an actor is just an object with atom-keyed callbacks: :init, :handle_request, :handle_notify.
Actor.request and Actor.notify are equivalent to erlang's call and cast respectively.
Each actor processes its mailbox strictly in order; concurrency comes from interleaving many actors.
handle_request = (msg, n) => match msg
:get -> [n, n] # [reply, new_state]
handle_notify = (msg, n) => match msg
[:add, x] -> n + x, # new_state only
:reset -> 0
counter = {
:init: () => 0
:handle_request: handle_request
:handle_notify: handle_notify
}
[:ok, pid] = Actor.spawn(counter)
Actor.notify(pid, [:add, 5]) # fire-and-forget
Actor.notify(pid, [:add, 7])
Actor.request(pid, :get) # [:ok, 12]
Primitives: Actor.spawn, Actor.notify, Actor.request, Actor.self,
Actor.alive, Actor.stop. notify returns () immediately; request blocks
the calling actor (or top-level code) until the target replies.
Actor.self(self_pid) from inside a handler is detected as :self_deadlock
and surfaces as [:error, :self_deadlock] rather than blocking forever;
calls to a dead or unknown pid return [:error, :no_proc]. A handler that
raises kills its actor and surfaces [:error, "handler crashed: ..."] to
the caller without aborting the script.
Actors run on a tokio multi-thread runtime: each spawn parks the actor on
its own blocking task, so independent actors progress in parallel on
different OS threads. Per-actor ordering is still strict (one handler at a
time, mailbox is FIFO via tokio::sync::mpsc). Synchronous call is
implemented via a tokio::sync::oneshot reply channel that the caller
blocks on until the target's handler returns.
use f2::interop::{FfResult, define_value, register};
use f2::interpreter::{Scope, eval_program};
use f2::parser::parse;
use f2::prelude;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct User { name: String, age: u32 }
let env = Scope::new();
prelude::install(&env);
// Push a Rust value into the script's env.
define_value(&env, "me", &User { name: "Ada".into(), age: 36 }).unwrap();
// Expose a Rust function — args and return value (de)serialize via serde.
register(&env, "greet", |u: User| format!("Hello, {}!", u.name));
// Fallible natives use FfResult so ff sees a tagged pair.
register(&env, "checked_div", |a: i64, b: i64| -> FfResult<i64> {
if b == 0 { FfResult::Err("divide by zero".into()) } else { FfResult::Ok(a / b) }
});
let prog = parse("greet(me)").unwrap();
assert_eq!(eval_program(&prog, &env).unwrap().to_string(), "\"Hello, Ada!\"");
let prog = parse("checked_div(10, 0)").unwrap();
assert_eq!(
eval_program(&prog, &env).unwrap().to_string(),
"[:error, \"divide by zero\"]"
);see the interop module docs for more information.
cargo install f2
f2 # REPL
f2 path.ff # run a file