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)]}
@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,
},
@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.