Skip to content

Abstract types

tim-hardcastle edited this page Nov 14, 2024 · 29 revisions

In this page we will review the concept of "abstract types".

The essential point to remember is this. A concrete type is something a Pipefish value can have. An abstract type is a filter on what a variable can contain, or the parameter of a function, or the field of a struct.

So if for example x is a variable of type int?, this means that it can contain a value of type int or of type null. It cannot contain a value of type int?, because there are no values of type int?.

Nullable twins

As the previous example suggests, every one of the types we've met so far has an abstract "nullable twin", written <typename>?, which can contain either values of the type typename or the value NULL of type null: so for example int? contains either something of type int or it contains NULL. Of course this works for user-defined types too.

Note that you can't compare a non-null value with NULL, because you can't compare values of different types. So if you want to know if a nullable parameter/variable/field foo is equal to NULL, you can't write foo == NULL (because that would fail at runtime if foo wasn't NULL) but instead must use the conditional expression foo in null to test its type membership.

Other built-in abstract types

The abstract type any contains all types that aren't null or tuple. struct contains all structs, enum contains all enum values, and label contains all field labels of structs. Each of them has a nullable twin, e.g. any? contains everything except tuple.

We will meet a few more built-in abstract types on the next page.

Making your own abstract subtypes

You can define new abstract subtypes in the newtype section.

newtype

Number = int/float

While concrete types are nominal, abstract types are structural: an abstract type really is just a set of concrete types, and any two abstract types with the same underlying set are the same.

Abstract types can be constructed "on the fly", e.g. 42 in int/float is a valid expression. On the other hand abstract types must be defined in the newtype section before they can be used in function signatures, rather than you being able to define foo(x int/string): this will be changed in a future version but is not a high priority.

varchar

The abstract types above are supertypes of concrete types. varchar by contrast supplies us with a family of abstract subtypes of strings: varchar(42) is a type containing strings with 42 or fewer characters. This is an anomaly tacked on to the type system for the purposes of SQL interop. You can't make your own abstract subtypes because they shouldn't really exist.

Using abstract types

In functions and struct definitions, you can just use an abstract type as you would use a concrete one.

newtype

Penguin = struct(name string, numberOfChicks int?, favoriteFood string?)

def

nullToZero(i int?) : 
    i in null : 0
    else : i

To widen or narrow a variable type you put the name of the concrete type after the name of the variable when declaring it. Note that this syntax is just like function / struct declarations: the syntax is <name> <type>.

var

couldBeInt int? = 42
couldBeBool bool? = NULL
shortString varchar(10) = "walrus"

If you don't do that, then the variable is automatically given the concrete type of the thing assigned to it. So for example if above we had just written couldBeBool = NULL, then the variable couldBeBool could not in fact be bool: it would be given the type null and could never contain anything but NULL.

Clone this wiki locally