Skip to content
Open Saka

Cardinality and contracts

A cardinality describes how many occurrences a Bag may hold. Repeats count: {1, 2, 2} has three occurrences, even though it has only two different Values.

count gives the number of occurrences in a Bag:

count({1, 2, 2}) // {3}
count(f64:{}) // {0}

A cardinality can allow one count, such as 3, or a range, such as 2..5, which includes both ends. 2..<5 leaves out its end, so it allows two to four. * allows any number, including zero.

A contract pairs a Set with a cardinality: which Values a Bag may hold, and how many. After a name, a contract is written after a colon, or with a short marker for a common cardinality:

Contract Short marker Occurrences
x: f64'1 x! f64 Exactly one
x: f64'? x? f64 Zero or one
x: f64'+ x+ f64 One or more
x: f64'* x* f64 Any number
x: f64'2..5 — Two to five
x: f64'2..<5 — Two to four

In a function’s parameter list, x! f64 takes one Value at a time, while x: f64'1 takes exactly one: see One Value at a time.

def names a computation. SakaQL works out its contract from its expression, or checks a contract you write:

def one! f64 = 1
def maybe? str = "red"
def flags* bool = {true, false}
def few: f64'0..3 = {1, 2}
def wrong! f64 = {1, 2} // error SK4301

wrong promises exactly one number but computes two.

The Set must fit too:

def wrong_kind! str = 1 // error SK4301

A written contract is what other computations see. flags* bool allows any number of Booleans, even though its expression currently gives two.

count gives an int, and its Set is the cardinality of the Bag it counts. few holds zero to three numbers, so its count is an integer from 0 through 3:

def few_count! int:{0..3} = count(few)
def few_count_wrong! int:{1..3} = count(few) // error SK4301

A count can meet an f64 in arithmetic, as count(few) * 0.5 does. To give one an f64 contract or compare it with an f64, convert it with f64::from:

def few_count_number! f64 = f64::from(count(few))

qset names a contract you can reuse:

qset Rating = f64:{1, 2, 3, 4, 5}'?
def rating: Rating = 4
rating // {4}

Rating allows zero or one number from 1 to 5. An empty Bag means there is no rating.

as gives an expression a broader contract without changing its Values:

{1, 2} as f64'* // {1, 2}

expect checks an expression’s Values and its count when it runs. Its target is a contract, and either part may be _ to keep the expression’s own. Here _ keeps the Set, and '1 asks for exactly one occurrence:

"pen" >> {"pen" -> 2, "book" -> 5} expect _'1 // {2}

A missing key gives zero occurrences, so this check fails:

"pencil" >> {"pen" -> 2, "book" -> 5} expect _'1 // error: expected exactly one occurrence

expect keeps every occurrence. It checks the count rather than choosing one Value from several.

Inside a function, the count can be a formula over the function’s parameters, as a result’s contract can (Cardinality formulas). The check uses the formula’s value at the arguments’ counts:

def twice(xs* int) = {xs, xs} expect _'(2 * xs)
twice({1, 2}) // {1'2, 2'2}

A Set in the target checks each Value too, and the result holds only the Values of both Sets. A function can narrow an int to a u8 this way:

def to_byte(x! int)! u8 = x expect u8'_
to_byte(200) // {200}
to_byte(300) // error: 300 is outside u8

The target’s Set must share a Value with the expression’s, so 300 expect u8'_ is an error before anything runs (SK4301). Where the target holds every Value, nothing is checked: 7 expect int'1 always succeeds. To keep a result within a width without failing, use wrap or saturate (Sets of results).

Next, use Defs to name computations and their steps.