1
Fork 0
mirror of https://github.com/RGBCube/Site synced 2025-07-30 12:37:50 +00:00
Site/site/blog/2025-06-20-intro-cab.md
2025-07-12 00:54:10 +03:00

6.2 KiB

title unlisted
An introduction to the Cab expression language true

Here is a small Cab snippet, taken from the standard library:

@Any = (@_), {[(1)]}

@symbol = {[(2)]} @name & String => {[(3)]} {
  {[(4)]} @`\(magic.name)` = name,
},

@nominate = @name & String => {
  @`\(magic.name)` = name,
  @`\(magic.value)` = value,
},

        {[(5)]}
@None = symbol "None",

@Some = nominate "Some",

@Option = {
  {[(6)]}
  @`\(magic.call)` = @value & Any => None | Some value,

  @unwrapOr = @default & Any => @option & Option (TypeOf default) =>
    if Some @value = option then
      value
    else
      default,

  @or = @alternative & Option Any => @option & TypeOf alternative =>
    if Some Any = option then
      option
    else
      alternative,
},

It may look confusing at first, but the way Cab works is refreshingly simple & at the end of this blog post, you'll understand how "typing", "pattern matching", and other things that don't actually exist in Cab work.

Let's start from the top:

@Any = (@_), {[(1)]}

Cab doesn't have "declarations", or "pattern matching". This is a literal comparison operation. Exactly the same as the == operator in most languages.

But then, how do we even declare anything in the local scope? How do we address values by name, instead of inlining them all?

The way Cab answers this question is interesting (and original, I have not seen anything like it before): Bindings as values!

In Cab, you can create a binding value with the @<identifier-here> syntax. So here, @Any is a binding value.

And the way you use bindings (aka, binds) in Cab is simple: You compare them using the comparison operator, = or !=.

A bind is equal to any value, literally anything! So, @foo = 123 is always true.

So, a bind is like a wildcard value? Then how does it actually "bind" anything to the local scope?

Simple: Binds, when compared with a value, bind that value to the local scope.

That means, when we evaluate this expression, the scope will have foo set to 123:

@foo = 123

You might have spotted something that hasn't been covered if you were reading carefully:

Why the parenthesis around the (@_) bind, then?

In order to prevent things from going out of control, Cab limits when binds can bind the value they are compared to to their local scope.

The rule that governs this is: A bind, when compared to a value, will bind that value to the scope the bind was declared in if the comparison operation is within that scope.

So, we don't actually get _ bound to @Any because the = is outside the scope (aka parenthesis) of @_, and thus we don't litter in our standard library.


If you are even more keen, you must have noticed ,, which is the "same" operator. It evaluates both operands at the same time, letting us do neat stuff like:

@countinc = count + 1,
@count = 123,

Or even:

@a = b,
@b = a,

This is similar to "toplevel" declarations in programming languages where you can reference a declaration before it is declared in the source text. Basically makes it evaluate in the order that it needs to, instead of top to down.

Trailing , is a noop operator, <expr>, and <expr> are exactly the same. Trailing commas are nice!

@symbol = {[(2)]} @name & String => {[(3)]} {
  {[(4)]} @`\(magic.name)` = name,
},

I've already explained how comparisons & binds work in Cab, so I'll skip the @symbol = part.

The way lambdas work in Cab is as follows: <value> => <body>.

The => is an infix operator, yet again. And the <value> can be any expression.

When a lambda is called, the <value> is compared with the argument in a new scope. If they are not "equal", aka when the comparison evaluates to false, an exception is thrown.

But when it is true, the <body> is evaluated and returned.

Okay, as the way lambdas work in Cab is out of the way, let's see what this expression is actually doing:

In this lambda, @name & String is the <value>, and { @`\(magic.name)` = name, } is the <body>.

The value

Let's start with the <value>, we know what @name is, it's a bind and when compared with the argument, it will set name in the local scope to it.

What is the & String part?

Answer: & is an infix operator, that takes two values and produces a value that is all of the given of the values, at the same time. It's called the all operator.

What is String? It's a value that is equal to all strings. No, not exactly a "type"! This is why Cab doesn't exactly have "typing", as everything is a value.

This makes @name & String a value that is equal to any string, and when compared to a string value, will bind it to the scope & have the comparison expression evaluate to true.

The body

Okay, so assuming the lambda was called properly, we should have a value named name in our local scope.

We will now evaluate this expression:

{
  @`\(magic.name)` = name,
}

Let's break it down:

{} is a special type of "parenthesis", it is identical to (), but unlike (), when it evaluates the inner expression, it doesn't return the expression itself. It returns its scope.

So, { @foo = 123 } will evaluate to an object of type Attributes, with foo set to 123.

Let's focus on the inner expression: { @`\(magic.name)` = name, }, specifically the @`\(magic.name)` part, as that is new.

It is a bind, but the identifier it has next to it isn't a "literal" identifier. It's interpolated.

\(...) is how you do string/identifier/path/etc interpolation in Cab, and it takes a single expression within that has to evaluate to a string. And the way you do "quoted" identifiers in Cab is by using `.

And the value within the bind is magic.name. magic is a builtin value, which contains strings that are used by the runtime for magical stuff.

The . operator is the "with scope" operator. The way magic . name is evaluated is by setting the scope of name to magic, and evaluating name, which is a reference. It's not an "attribute access" operator, because we can do stuff like: magic.[name, value].

Here, we effectively get the name value within magic and use it for the string interpolation.