FREAKV3 docs freak 0.14.2 (Maverick)

Tutorial 3: Modelling data#

> Shapes, nested shapes, methods, and both of V3's collection surfaces — including the restriction that catches everyone. About 25 minutes.

V3 gives you one user-defined type (shape) and two ways to hold a sequence. This tutorial builds a squadron roster and shows where each tool fits.

Step 1 — A shape with behaviour#

FREAK
shape Member {
    callsign: word
    sorties: int
}

impl Member {
    task enlist(callsign: word) -> Member {
        give back Member { callsign: callsign, sorties: 0 }
    }

    task veteran(self) -> bool {
        give back self.sorties >= 20
    }
}

enlist has no self, so it is an associated method: Member::enlist("V-1"). veteran takes self, so it is an instance method: m.veteran().

Constructor-style associated methods are the idiomatic way to build a shape with defaults, since fields have no default values.

Step 2 — Nesting#

Shapes hold shapes, and access chains:

FREAK
shape Airframe {
    model: word
    thrust: int
}

shape Member {
    callsign: word
    sorties: int
    frame: Airframe
}

Construct inline:

FREAK
pilot m: Member = Member {
    callsign: "Valkyrie-1",
    sorties: 0,
    frame: Airframe { model: "Takemikazuchi", thrust: 880 }
}

say word_from_int(m.frame.thrust)
say "{m.callsign} flies a {m.frame.model}"

Dotted paths work inside interpolation, including {self.frame.thrust} from within a method.

Note

Shapes are handles into runtime storage, not values. Passing one to a task and mutating a field is visible to the caller, and assigning one binding to another aliases the same storage. To get an independent copy, construct a new shape.

Step 3 — Lists#

List<T> is the typed sequence. The element is a scalar or a shape name:

FREAK
pilot names: List<word> = ["Valkyrie-1", "Valkyrie-2", "Valkyrie-3"]
say word_from_int(names.length())
say names[1]

Three rules to internalise:

  • **.length() is a method**, not a field. names.length reports "non-shape value has no fields".
  • **Indexed assignment needs pilot mut**, otherwise "indexed assignment requires a mutable list binding".
  • The literal infers the type. [1, 2, 3] is a List<int>; all elements must share one type.

List::filled(value, count) builds a pre-populated list, which is the way to size one up front:

FREAK
pilot mut tally: List<int> = List::filled(0, 3)
tally[0] = 24

Lists grow as of v0.14.2: .push(v), .pop(), .reserve(n), .capacity() and .clear(), with List::new() and List::with_capacity(n) as constructors. Mutating methods need pilot mut.

FREAK
pilot mut queue: List<int> = List::new()
queue.push(10)
queue.push(20)
say word_from_int(queue.pop())      -- 20

Still no insert, remove, sort or iterator.

Step 4 — The restriction#

This is the one that catches people:

FREAK
shape Roster {
    members: List<Member>     -- does not compile
}
type error: V3 owned shape fields do not yet support List values

A List<T> may be a binding, a task parameter and a return type — but not a shape field. So the "container shape holding a list of children" pattern is unavailable.

Work around it one of three ways:

  1. Keep the list beside the shape rather than inside it.
  2. Store a legacy int array handle in the field.
  3. Pass lists through tasks, which is allowed:
FREAK
task total_sorties(counts: List<int>) -> int {
    pilot mut sum: int = 0
    pilot mut i: int = 0
    repeat counts.length() times {
        sum += counts[i]
        i += 1
    }
    give back sum
}

Step 5 — The legacy handle#

Alongside List<T> there is an older untyped array: array_new() returns an int handle, and every element is a word.

FREAK
pilot h: int = array_new()
array_push(h, "alpha")
array_push(h, "bravo")
say word_from_int(array_len(h))
say array_get(h, 0)
array_release(h)

Now that List<T> grows too, it is worth keeping around for one reason: it is the only thing the std/algorithm.fk helpers accept — and the only collection you can store in a shape field.

FREAK
array_sort_int(h)
say array_join(h, ", ")
say word_from_int(array_max_int(h))

Those helpers are declared handle: int, so passing a List<word> fails:

type error: call to 'array_join' argument 1 expects int, got List<word>

The array_* builtins are more forgiving — array_len, array_get, array_set, array_push accept either. Builtins bridge; library tasks do not.

Careful

Two helpers are broken in the shipping compiler: array_sort_word() segfaults, and array_unique() returns an empty array. Use array_sort_int, and deduplicate by hand.

Step 6 — Choosing between them#

NeedUse
Typed elements, fixed sizeList<T>
Numbers without converting to wordList<int> / List<num>
Growing a collectionList<T> + .push()
Sorting, joining, searchingLegacy handle + std/algorithm.fk
A field inside a shapeLegacy handle — List is rejected
Returning a collection from a taskEither; or mutate a handle passed in

array_release(h) frees a legacy array. V3 has no garbage collector, so a long-running program that allocates in a loop must release.

The finished program#

examples/tut_roster.fk compilesruns
-- Tutorial 3, finished program.
--
-- Model a squadron with shapes, keep the members in a List, and compute
-- a summary. Shows the two collection surfaces working side by side and
-- the one restriction that catches people: a shape cannot hold a List.

shape Airframe {
    model: word
    thrust: int
}

shape Member {
    callsign: word
    sorties: int
    frame: Airframe
}

impl Member {
    task enlist(callsign: word, model: word, thrust: int) -> Member {
        give back Member {
            callsign: callsign,
            sorties: 0,
            frame: Airframe { model: model, thrust: thrust }
        }
    }

    task veteran(self) -> bool {
        give back self.sorties >= 20
    }

    task line(self) -> word {
        give back "{self.callsign}  {self.frame.model}  thrust {self.frame.thrust}  sorties {self.sorties}"
    }
}

-- A List may be a parameter and a return type. It may NOT be a field on
-- a shape: that reports
--   "V3 owned shape fields do not yet support List values"
task total_sorties(counts: List<int>) -> int {
    pilot mut sum: int = 0
    pilot mut i: int = 0
    repeat counts.length() times {
        sum += counts[i]
        i += 1
    }
    give back sum
}

task main() -> void {
    pilot mut ichi: Member = Member::enlist("Valkyrie-1", "Takemikazuchi", 880)
    pilot mut ni: Member = Member::enlist("Valkyrie-2", "Shiranui", 640)
    pilot mut san: Member = Member::enlist("Valkyrie-3", "Gekishin", 520)

    repeat 24 times { ichi.sorties += 1 }
    repeat 8 times { ni.sorties += 1 }
    repeat 31 times { san.sorties += 1 }

    say ichi.line()
    say ni.line()
    say san.line()

    -- A typed List holds the numbers we want to aggregate.
    pilot counts: List<int> = [ichi.sorties, ni.sorties, san.sorties]
    say "total sorties: " + word_from_int(total_sorties(counts))
    say "squadron size: " + word_from_int(counts.length())

    -- Names in a List<word>, indexed like any other list.
    pilot names: List<word> = [ichi.callsign, ni.callsign, san.callsign]
    pilot mut i: int = 0
    repeat names.length() times {
        say "  member " + word_from_int(i) + ": " + names[i]
        i += 1
    }

    -- Counting veterans needs the shapes themselves, so loop over them
    -- directly rather than trying to put Members in a List.
    pilot mut veterans: int = 0
    if ichi.veteran() { veterans += 1 }
    if ni.veteran() { veterans += 1 }
    if san.veteran() { veterans += 1 }
    say "veterans: " + word_from_int(veterans)

    -- The legacy handle is still the right tool when you need the
    -- std/algorithm.fk helpers, which are declared with `handle: int`.
    pilot ranking: int = array_new()
    array_push(ranking, word_from_int(ichi.sorties))
    array_push(ranking, word_from_int(ni.sorties))
    array_push(ranking, word_from_int(san.sorties))

    array_sort_int(ranking)
    say "sorted sorties: " + array_join(ranking, ", ")
    say "busiest pilot flew " + word_from_int(array_max_int(ranking))
    array_release(ranking)
}
Program output
Valkyrie-1  Takemikazuchi  thrust 880  sorties 24
Valkyrie-2  Shiranui  thrust 640  sorties 8
Valkyrie-3  Gekishin  thrust 520  sorties 31
total sorties: 63
squadron size: 3
  member 0: Valkyrie-1
  member 1: Valkyrie-2
  member 2: Valkyrie-3
veterans: 2
sorted sorties: 8, 24, 31
busiest pilot flew 31

Where to go next#

Every FREAK snippet on this site was compiled by freak 0.14.2 (Maverick), built from source with a verified self-host fixed point. 45/45 examples compile; regenerate with python tools/verify.py then python tools/build_docs.py.