Messages
A message is a unique value whose type is itself.
gab\message
Messages values are a name or operator ending with a :.
+:
true:
<=!=>:
ok:? # => ok:
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
Messages responds to def:. This adds a new specialization of that message for a specific receiver type:
greet: .def (Strings.t, () => do
'Hello, $!'.sprintf(self).println
end)
'Alice'.greet # => Hello, Alice!
There are multiple ways to define messages, and it is commonplace to create your own.
def:
Defines a single specialization for a single type:
birthday: .def (Person, () => do
self.put(age: self.age + 1)
end)
If you omit the type, you create a general specialization. Think of this as a fallback implementation which will run if nothing more specific exists.
defcase:
Defines multiple specializations for one message at once, using a record. Each key-value pair in the record is used to create a new type-specialization.
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: none:
nil: is the value Gab binds to names that have no corresponding value. This may occur if, for example, 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 nil: and none: are just plain messages.