ddonche/goblin-lang
0.46.24
1
0
docs reference
[[actions]]

Actions


In Goblin, actions are the universal unit of callable logic. What other languages split into functions, methods, lambdas, and procedures, Goblin calls one thing: an action.

You never have to remember whether something is a function or a method. If it does something and you can call it, it's an action.


Declaring Actions

Actions are declared with either act or action — they are identical. Use whichever reads more naturally.

act greet(name)
    :say("Hello, {name}!")
xx

action greet(name)
    :say("Hello, {name}!")
xx
act greet(name)
    :say("Hello, {name}!")
xx

action greet(name)
    :say("Hello, {name}!")
xx

Both produce exactly the same result. Goblin docs use act as the convention for brevity.


Inline Actions

Short actions can be written on a single line using =>. Inline actions do not use a closing xx or end.

act double(x) => return x * 2

act is_even(x) => return x % 2 == 0

act greet(name) => :say("Hello, {name}!")
act double(x) => return x * 2

act is_even(x) => return x % 2 == 0

act greet(name) => :say("Hello, {name}!")


Multiline Actions

Multiline actions close with either xx (crossbones) or end. Goblin convention prefers xx.

act describe(item, qty)
    label | "{qty}x {item}"
    :say(label)
xx
act describe(item, qty)
    label | "{qty}x {item}"
    :say(label)
xx

Both closers are valid, but xx is the Goblin style.


Parameters

Parameters are declared in parentheses. Defaults are set with |.

act greet(name, greeting | "Hello")
    :say("{greeting}, {name}!")
xx

greet("Sheriff")            /// Hello, Sheriff!
greet("Sheriff", "Howdy")   /// Howdy, Sheriff!
act greet(name, greeting | "Hello")
    :say("{greeting}, {name}!")
xx

greet("Sheriff")            /// Hello, Sheriff!
greet("Sheriff", "Howdy")   /// Howdy, Sheriff!


Return Values

Actions return the value of a return statement. Without one, they return nil.

act add(a, b) => return a + b

result | add(3, 4)
:say(result)   /// 7
act add(a, b) => return a + b

result | add(3, 4)
:say(result)   /// 7

Multiple return values are supported:

act min_max(items)
    return :grab_first(items), :grab_last(items)
xx

lo, hi | min_max([3, 1, 4, 1, 5, 9])
act min_max(items)
    return :grab_first(items), :grab_last(items)
xx

lo, hi | min_max([3, 1, 4, 1, 5, 9])


Calling Actions

Actions are called by name with arguments in parentheses.

act square(n) => return n * n

square(5)    /// 25
act square(n) => return n * n

square(5)    /// 25

Method-style calls use .:

result | items.sort()
name   | text.upper()
result | items.sort()
name   | text.upper()


Intrinsic Actions

Goblin ships with a large set of built-in actions called intrinsics. By convention, intrinsics are always called with a : prefix:

:say("hello")
:len(items)
:json_parse(raw)
:run_cmd("goblin build.gbln")
:say("hello")
:len(items)
:json_parse(raw)
:run_cmd("goblin build.gbln")

The : prefix is optional — say("hello") works too — but using it is strongly recommended. It tells you immediately that this is a built-in you can look up in the docs, rather than something defined in your current file. You never have to grep your codebase wondering where say came from.

Tip
If you see :name(...), it's a Goblin built-in. If you see name(...), it's defined somewhere in your project.
Warning
Not every intrinsic may work with this; it is a known bug we are working on.

Invoking Actions Dynamically

Actions can be called by name at runtime using :invoke. This is useful when you want to pass behavior as data — for example, the predicate in collection operations like :grab_where.

:invoke accepts two forms:

act double(x) => return x * 2

/// pass args directly
:invoke("double", 5)        /// 10

/// or as an array
:invoke("double", [5])      /// 10
act double(x) => return x * 2

/// pass args directly
:invoke("double", 5)        /// 10

/// or as an array
:invoke("double", [5])      /// 10

Multiple arguments work the same way:

act add(a, b) => return a + b

:invoke("add", 3, 4)        /// 7
:invoke("add", [3, 4])      /// 7
act add(a, b) => return a + b

:invoke("add", 3, 4)        /// 7
:invoke("add", [3, 4])      /// 7

Namespaced actions use :::

:invoke("trailboss::rewrite_wiki_links", ctx)
:invoke("trailboss::rewrite_wiki_links", ctx)

This is how _where predicates work in collection operations — the action name is passed as a string and invoked on each element:

act is_big(x) => return x > 10

items | [3, 7, 12, 18, 2]
big | :grab_where(items, "is_big")   /// [12, 18]
act is_big(x) => return x > 10

items | [3, 7, 12, 18, 2]
big | :grab_where(items, "is_big")   /// [12, 18]


Summoning — Pipelines of Actions

:summon threads a value through a list of action names in sequence. Each action receives the output of the previous one. It is :invoke applied repeatedly down a list.

:summon(value, ["action_one", "action_two", "action_three"])
:summon(value, ["action_one", "action_two", "action_three"])

This is equivalent to:

value |= :invoke("action_one", value)
value |= :invoke("action_two", value)
value |= :invoke("action_three", value)
value |= :invoke("action_one", value)
value |= :invoke("action_two", value)
value |= :invoke("action_three", value)

In Sheriff, summon is how the entire build pipeline works. Each portal defines a schedule — an ordered list of action names — and summon threads the page context through all of them:

/// events.yall defines the schedule for this portal
schedule | cfg["schedule"]

/// thread ctx through every action in the schedule
ctx |= :summon(ctx, schedule)
/// events.yall defines the schedule for this portal
schedule | cfg["schedule"]

/// thread ctx through every action in the schedule
ctx |= :summon(ctx, schedule)

A schedule might look like:

schedule:
  - "frontier::strip_frontmatter"
  - "markdown_core::render"
  - "trailboss::rewrite_wiki_links"
  - "brindle::apply_layout"
  - "brindle::apply_tokens"
schedule:
  - "frontier::strip_frontmatter"
  - "markdown_core::render"
  - "trailboss::rewrite_wiki_links"
  - "brindle::apply_layout"
  - "brindle::apply_tokens"

Each action in the list receives ctx and returns a modified ctx. The final result is the fully built page.

Pre-build actions work the same way, but run once before any files are processed:

/// run a single pre-build action
ret | :summon(pre_ctx, [act_name])
/// run a single pre-build action
ret | :summon(pre_ctx, [act_name])

Tip
summon and invoke together give you a fully data-driven pipeline. The schedule lives in config — no code changes needed to add, remove, or reorder build steps.

Namespace Actions

Actions inside modules are called with the :: namespace operator:

import game/hero as h

h::create_hero()
h::show_hero()
import game/hero as h

h::create_hero()
h::show_hero()


Summary

Form Syntax Closes with
Inline act name(params) => expr nothing
Multiline act name(params) + body xx (preferred) or end
Keyword alias action same as above
Intrinsic :name(params) n/a — built-in
Dynamic :invoke("name", args) n/a — built-in
Pipeline :summon(value, ["a", "b"]) n/a — built-in

Goblin's single act keyword replaces the entire zoo of function, def, fn, method, proc, lambda, and sub that other languages accumulate over time. One word. Every callable thing.