Messages
A message is an identifier ending in a colon. Its type is itself.
ok:? # => ok:
true:? # => true:
my_msg:? # => my_msg:
Messages are used as record keys, as sentinel/enum values, and as the mechanism for polymorphism. They are Gab’s implementation of booleans, nil, and result values — there are no built-in keywords for any of these.
Defining Specializations
A message responds to def: to add a new specialization — an implementation for a specific receiver type:
greet: .def (Strings.t, () => do
'Hello, $!'.sprintf(self).println
end)
'Alice'.greet # => Hello, Alice!
There are three definition messages, each suited to different situations.
def:
Defines a single specialization for a single type:
birthday: .def (Person, () => do
self.put(age: self.age + 1)
end)
defcase:
Defines multiple specializations for one message at once, using a record as a dispatch table:
describe: .defcase {
ok: result => 'Success: $'.sprintf(result).println
err: msg => 'Error: $'.sprintf(msg).println
nil: => 'Nothing here.'.println
}
Each key in the record is a receiver type; each value is the block to call when that type receives the message. Values alone (without a block) are also valid — they are returned directly.
defmodule:
Defines multiple messages for multiple receiver types at once:
[Point, Vector] .defmodule {
scale: (factor) => self.put(x: self.x * factor, y: self.y * factor)
zero: () => self.put(x: 0, y: 0)
}
Dispatch Resolution Order
When a message is sent to a value, Gab resolves the specialization in this order:
- Super type — if the value’s type (e.g. a
gab\shape) has a specialization, use it. - Type — use the specialization defined for the value’s
gab\type (e.g.gab\record). - Property — if the receiver is a record and the message name matches one of its keys, return that value. This is how field access works:
{ name: 'bob' }.namereturns'bob'without any explicitdef:. - General — use a specialization defined with no specific type.
y: .def 'general case'
z: .def (Shapes.make(x:), 'shape case')
{ x: 1 }.y # => 'general case' (general)
{ x: 1 }.z # => 'shape case' (super type — the shape <x:>)
{ x: 1 }.x # => 1 (property)
and:, or:, then:, else:
These messages are defined on true: and false: in the core library. Their semantics differ in one important way:
and: and or: accept values — the argument is always evaluated before the message is sent:
true: .and 2 # => 2
false: .and 2 # => false:
false: .or 2 # => 2
true: .or 2 # => true:
then: and else: accept blocks — only the appropriate branch is invoked:
true: .then () => 'yes'.println # => yes
true: .else () => 'no'.println # (block is never called)
nil: and none:
nil: is the value Gab binds to names that have no corresponding value — for example, if a binding list is longer than the tuple being destructured:
(a, b) = 1 # a => 1, b => nil:
none: is used by certain APIs to signal the absence of a result (as opposed to an error). Both are plain message values with no special runtime treatment.