-
Notifications
You must be signed in to change notification settings - Fork 5
Validation and parameterized types
Struct and clone types can have runtime validation attached to their type constructors.
newtype
Person = struct(name string, age int) :
that[name] != ""
that[age] >= 0
EvenNumber = clone int using +, -, * :
that mod 2 == 0
We can parameterize this validation. For example, let's make some math-style vectors and some operators to go with them.
newtype
Vec = clone{i int} list :
len(that) == i
def
(v Vec{i int}) + (w Vec{i int}) -> Vec{i} :
Vec{i} from a = [] for j::el = range v :
a + [el + w[j]]
(v Vec{i int}) ⋅ (w Vec{i int}) :
from a = 0 for j::el = range v :
a + el * w[j]
(v Vec{3}) × (w Vec{3}) -> Vec{3} :
Vec{3}[v[1]*w[2] - v[2]*w[1],
.. v[2]*w[0] - v[0]*w[2],
.. v[0]*w[1] - v[1]*w[0]]
Some notes on what's going on. The Vec{i int} types in the definition of + and ⋅ allow us to capture their type parameter under the identifieri, and, if it is used more than once, as in this case, to check that the parameters match.
Note that the presence of the return type ensures that the compiler recognizes that the output must be of the same concrete type as the inputs, so that for example it recognizes that each of the vector types fulfills the built-in Addable interface:
Addable = interface :
(x self) + (y self) -> self
While the parameters of the types in the call signature may be captured, the parameters in the return signature are computed. This example should clarify the distinction. Suppose that sometimes we wanted to concatenate values in the Vec type as though they were ordinary lists. Then we can write a return type like this:
concat (v Vec{i int}, w Vec{j int}) -> Vec{i + j} :
Vec{i + j}(list(v) + list(w))
(And indeed in the previous example of vector addition the return type was technically being computed, it's just that the computation was "evaluate i".)
The types we can use as parameters are bool, float, int, string, rune, type, and any enum type.
As mentioned, we can also parameterize struct types. In this particular example, we don't use the parameters for the runtime validation, but just to ensure that it treats different currencies as different types and doesn't try to add them together. An enum type is used as a parameter.
newtype
Currency = enum USD, EURO, GBP
Money = struct{c Currency}(large, small int):
0 <= that[large]
0 <= that[small]
that[small] < 100
def
(x Money{c Currency}) + (y Money{c Currency}) -> Money{c} :
x[small] + y[small] < 100 :
Money{c}(x[large] + y[large], x[small] + y[small])
else :
Money{c}(x[large] + y[large] + 1, x[small] + y[small] - 100)
We can of course implement "generics" merely as a special case where a parameterized type takes a type as a parameter. This has been done for you in the case of the built-in list, set, map, and pair types, with list and set taking one type parameter, and pair and map taking two. Here for example is the implementation of the generic list type:
newtype
list = clone{T type} list using +, slice :
from a = true for _::el = range that :
el in T :
continue
else :
break false
Note that as in this example we can overload built-in types such as list. We can also overload parameterized types, e.g. we could have the Vec{i int} constructor defined above and also have a Vec{i int, T type} constructor which both checks the length of the vector and typechecks its elements.
Each instance of a parameterized type, e.g. Vec{3}, Money{USA}, is a distinct concrete type, and concrete types cannot be declared at runtime. Instead, they must be produced at compile-time, and the compiler declares all the instances that you actually mention in the script. Hence, if you run the following script ...
newtype
Z = clone{i int} int :
0 <= that
that < i
make Z{9}, Z{12} // See note below.
Qux = struct(zort Z{7})
def
foo(z Z{5}) :
z
bar(i int) :
Z{3}(i)
... then the types Z{3}, Z{5}, Z{7}, Z{9}, and Z{12} will exist at runtime, but nothing else of the form Z{i}.
The make statement is for when you want to have a type (or as in this case, types) to use at runtime, but there's no other reason to mention it in the script: make mentions it, thus summoning it into existence, and has no other effect.
🧿 Pipefish is distributed under the MIT license. Please steal my code and ideas.