Skip to content

A shape describes the keys of a record, in order. It is the record’s type. All records with the same keys in the same order share exactly one shape.

a := { name: 'Joe' }
b := { name: 'Rich' }

a?          #  <gab\shape name:>
b?          #  <gab\shape name:>
(a?) == (b?)  #  true:

gab\shape

There are multiple ways to construct a shape value.

From a record, using the ? message:

Person := { name: '', age: 0 }?
#  <gab\shape name: age:>

Directly, using Shapes.make: with a list of keys, or the shape syntax:

Person := Shapes.make(name:, age:)
#  <gab\shape name: age:>
Person := \{ name: age: }
#  <gab\shape name: age:>

Both produce the same shape value.

Shapes as specialization targets

Shapes are most useful as receiver types in def:, defcase:, and defmodule:. All records with a matching shape will respond to the defined message:

Person := \{ name: age: }

birthday: .def (Person, () :: do
  'Happy Birthday, $!'.sprintf(self.name).println
  self.put(age: self.age + 1)
end)

bob := { name: 'bob', age: 44 }
bob := bob.birthday
#  Happy Birthday, bob!

bob.age   #  45

Shapes in dispatch

When gab resolves a message send, the shape is checked as the super type before the record’s base type (gab\record). This means shape-specialised messages take precedence over general record behaviour:

foo: .def (\{ x: }, 'shape case')
foo: .def (Records.t, 'record case')
foo: .def 'general case'

{ x: 1 }.foo          #  'shape case'   (shape wins over general)
{ y: 1 }.foo          #  'record case'
'something else!'.foo #  'general case'

See Messages — Dispatch Resolution Order for the full sequence.