Overview

Welcome to the Wipple documentation! This book contains a tour of the Wipple programming language designed for people with an existing programming background. If you’re a beginner looking to learn to code with Wipple, the Wipple Playground contains 80+ lessons and guides.

In addition to the tour, this book includes a language reference, guides for advanced users, and documentation for the Wipple compiler.

Hello, world!

To display text on the screen, use show:

show "Hello, world!"
Hello, world!

Text is written inside double quotes ("...").

show accepts numbers, too:

show (1 + 2)
3

You need parentheses there because operators like + have precedence over whitespace, so show 1 + 2 is interpreted as (show 1) + 2. Rather than displaying 1 on the screen and adding the result to 2, we want to add 1 to 2 and display the result. You can use parentheses like this almost anywhere!

Variables

In Wipple, variables are defined using a colon (:):

name : "Wilson"
show name

sum : 1 + 2
show sum
Wilson
3

You can create multiple variables with the same name. When you refer to a name in your code, the most recent variable is chosen:

n : 1
show n

n : 2
show n
1
2

Each declaration is its own variable with its own value; they don’t need to be the same type:

n : 1
show n

n : "n"
show n
1
n

The right-hand side of the : is evaluated before bringing the variable into scope:

n : 1
n : n + 1
show n
2

A variable can only be accessed after it’s defined, not before:

n : n + 1
n : 1
show n
example:1:5: error: can't find `n`

Sometimes, you need to change the value of an existing variable. You can do this by putting an exclamation mark (!) after the variable name:

n : 0
n! : n + 1
show n

Now, any code that refers to n will observe the updated value.

Blocks and control flow

A block is a piece of code surrounded in braces ({...}). Blocks let you store code to run it later. To run the code in a block, use do:

greeting : {show "Hello, world!"}
do greeting
Hello, world!

Without do, the block will do nothing:

greeting : {show "Hello, world!"}
-- nothing happens

You can call do on a block multiple times:

greeting : {show "Hello, world!"}
do greeting
do greeting
Hello, world!
Hello, world!

You can write multiple lines of code in a block; the value of the block is the value of the last line:

sum : {
    show "calculating 1 + 2..."
    1 + 2
}

show (do sum)
calculating 1 + 2...
3

You don’t have to store a block in a variable before calling do:

sum : do {1 + 2} -- equivalent to `sum : 1 + 2`

Blocks are useful for logic and control flow. For example, if accepts a condition and two blocks. If the condition is True, the first block will be evaluated, and if it’s False, the second block will be evaluated.

secret : 5
guess : 3
if (guess = secret) {show "You win!"} {show "Try again"}
Try again

repeat accepts a number of times and runs the provided block that number of times:

repeat (3 times) {
    show "Hello, world!"
}
Hello, world!
Hello, world!
Hello, world!

It’s important to remember that blocks are values, just like text and numbers are — they can be stored in variables and passed to functions. repeat is just a function that accepts a block as input. You can build your types of control flow easily, and we’ll do just that in the next chapter!

Functions

Functions are written with an arrow (->). The inputs go on the left side of the arrow, and the output goes on the right:

add : a b -> a + b
sum : add 1 2
show sum
3

Functions are also just values, so they can be assigned to variables as shown above, or they can be used inline:

sum : (a b -> a + b) 1 2
show sum
3

If you want to have multiple statements in a function, you can use a do block:

debug-sum : a b -> do { -- don't forget `do`!
    show "called `debug-sum`"
    a + b
}

show (debug-sum 1 2)
called `debug-sum`
3

Let’s build a function that takes a block and runs it twice:

twice : block -> do {
    do block
    do block
}

twice {
    show "Hello, world!"
}
Hello, world!
Hello, world!

We just defined our own control flow!

Finally, you can use text values as functions — if you put underscore (_) placeholders in the text and provide values afterward, you can do string interpolation:

greet : name -> "Greetings, _!" name
show (greet "everyone")
Greetings, everyone!

Constants and types

Wipple has a powerful type system that can catch bugs in your program. By default, it works invisibly — all the code we’ve written so far has been fully typechecked! — but sometimes it’s helpful to provide type annotations. Type annotations serve as a form of documentation, describing the kinds of values your code works with and produces.

To add a type annotation to a variable, use a double colon (::):

add :: Number Number -> Number
add : a b -> a + b

show (add 1 2)
3

Adding a type annotation also changes how the variable is represented — rather than evaluating its value immediately, the variable is “lifted” out of the list of statements and is accessible anywhere as a constant. That means you don’t have to worry about the order in which constants are defined.

show (add 1 2)

-- Works, even though `add` is declared after the call to `show`
add :: Number Number -> Number
add : a b -> a + b
3

Because of this order independence, constants are “lazy”: they are evaluated whenever they are used, not when they are declared. You can think of the constant’s value as being wrapped in a block. In practice, this isn’t a problem because most constants produce functions that need to be called anyway.

Constants can also be generic — see Type functions and traits for more information.

Let’s look at some of the types that can be used in a type annotation:

  • Number is the type of numbers.
  • Text is the type of text.
  • Boolean is the type of True and False.
  • None is the “unit type”, and is returned by functions like show that do something but produce no meaningful value.
  • {A} is the type of a block evaluating to a value of type A. For example, {1 + 1} has type {Number}.
  • A -> B is the type of a function accepting a single input of type A and producing a value of type B. Likewise, A B C -> D is the type of a function accepting three inputs.

You can also make your own types! We’ll discuss structure types in this chapter, and enumeration types in the next chapter.

To define a structure type, pass a block of fields to type:

Sport : type {
    name :: Text
    players :: Number
}

Any block containing only variables is assumed to be a structure value:

Sport : type {
    name :: Text
    players :: Number
}
basketball :: Sport
basketball : {
    name : "Basketball"
    players : 5
}

When you define the Sport type, Wipple also generates a function Sport that accepts the block and returns it as-is. This is useful because it allows Wipple to infer the type of the structure without needing a type annotation:

Sport : type {
    name :: Text
    players :: Number
}
basketball : Sport {
    name : "Basketball"
    players : 5
}

To get the values out of a structure, you can put a block on the left-hand side of the colon (:), listing the field(s)’ names and the corresponding variable names.

Sport : type {
    name :: Text
    players :: Number
}

basketball : Sport {
    name : "Basketball"
    players : 5
}
{name : sport-name} : basketball
show sport-name
Basketball

Finally, you might also see the double colon (::) used to annotate the type of an expression. For example, you can write:

show (42 :: Number)
42

Usually this is unnecessary, but in some cases, Wipple’s type inference algorithm needs help. You’ll see an example of type annotations being used for this purpose in Type functions and traits.

Patterns

Wipple uses pattern matching to express control flow. For example, let’s say we want to generate a report card:

Grade : type {
    A
    B
    C
    D
    F
}

report-card :: Grade -> Text
report-card : grade -> when grade {
    A -> "top of the class"
    B -> "good work"
    C -> "need to study"
    D or F -> "didn't pass"
}

show (report-card A)
top of the class

First, we define our patterns using type. Rather than providing fields, we list the variants, and Wipple will create an enumeration for us. Then, we use when to return a different value for each variant. You can use or to match multiple variants at once.

In fact, in Wipple, if is just a regular function that matches on Boolean. We can create our own easily:

My-Boolean : type {
    My-True
    My-False
}

my-if : bool then else -> when bool {
    My-True -> do then
    My-False -> do else
}

show (my-if My-True {123} {456})
123

In addition to enumerations like these, you can store data alongside each pattern, allowing you to express values that are tied to a condition — in other words, the value is “wrapped” in a pattern, and you need to “unwrap” the value by checking for the condition using when. This may sound a bit confusing if you’ve used other languages without this feature, so let’s look at an example:

Maybe-Number : type {
    Some-Number Number
    No-Number
}

Here, we create a Maybe-Number value with two patterns. The first pattern contains a Number, and the second pattern contains nothing. Now, we can use pattern matching to “unwrap” the Maybe-Number:

Maybe-Number : type {
    Some-Number Number
    No-Number
}
describe-maybe-number : maybe -> when maybe {
    Some-Number n -> "we have a number: _" n
    No-Number -> "we don't have a number"
}

show (describe-maybe-number (Some-Number 42))
show (describe-maybe-number No-Number)
we have a number: 42
we don't have a number

Why is this useful? It means we can represent errors in our program! Let’s go back to our report card example, and allow the user to specify a grade as input:

Grade : type {
    A
    B
    C
    D
    F
}

report-card :: Grade -> Text
report-card : grade -> when grade {
    A -> "top of the class"
    B -> "good work"
    C -> "need to study"
    D or F -> "didn't pass"
}
Maybe-Grade : type {
    Valid-Grade Grade
    Invalid-Grade
}

parse-grade :: Text -> Maybe-Grade
parse-grade : text -> when text {
    "A" -> Valid-Grade A
    "B" -> Valid-Grade B
    "C" -> Valid-Grade C
    "D" -> Valid-Grade D
    "F" -> Valid-Grade F
    _ -> Invalid-Grade
}

repeat forever {
    grade : parse-grade (prompt "Enter your grade")

    when grade {
        Valid-Grade g -> show (report-card g)
        Invalid-Grade -> show "invalid grade"
    }
}
Enter your grade: A
top of the class
Enter your grade: B
good work
Enter your grade: Z
invalid grade
...

Wipple’s type system will check for us that we handle the error — watch what happens if we pass our Maybe-Grade to report-card directly:

Grade : type {
    A
    B
    C
    D
    F
}

report-card :: Grade -> Text
report-card : grade -> when grade {
    A -> "top of the class"
    B -> "good work"
    C -> "need to study"
    D or F -> "didn't pass"
}

Maybe-Grade : type {
    Valid-Grade Grade
    Invalid-Grade
}

parse-grade :: Text -> Maybe-Grade
parse-grade : text -> when text {
    "A" -> Valid-Grade A
    "B" -> Valid-Grade B
    "C" -> Valid-Grade C
    "D" -> Valid-Grade D
    "F" -> Valid-Grade F
    _ -> Invalid-Grade
}
grade : parse-grade (prompt "Enter your grade")
show (report-card grade)
example:32:19: error: expected `Grade` here, but found `Maybe-Grade`

If you’ve used a language with exceptions, Wipple’s pattern matching is kind of like try...catch, but you are forced to handle every error explicitly. This can seem cumbersome at first, but it makes bugs much easier to track down. And don’t worry, you don’t have to define your own Maybe type every time — Wipple has one built in that works for any type! We’ll learn how to use it in the next chapter.

Type functions and traits

In the same way functions create new values from any given input, type functions create new types from any given input type — in other words, types can be “generic”. That means we can create a Maybe that works for any value!

To make a type function, you use the “fat arrow” (=>), where the input types go on the left and the output type goes on the right:

My-Maybe : Value => type {
    My-Some Value
    My-None
}

describe-maybe-number :: (My-Maybe Number) -> Text
describe-maybe-number : maybe -> when maybe {
    My-Some number -> "we have a number: _" number
    My-None -> "we don't have a number"
}

describe-maybe-text :: (My-Maybe Text) -> Text
describe-maybe-text : maybe -> when maybe {
    My-Some text -> "we have text: _" text
    My-None -> "we don't have text"
}

show (describe-maybe-number (My-Some 42))
show (describe-maybe-text (My-Some "Hello, world!"))
we have a number: 42
we have text: Hello, world!

The arrow can also be used to make constants generic, so we only need one describe-maybe!

My-Maybe : Value => type {
    My-Some Value
    My-None
}
describe-maybe :: Value => (My-Maybe Value) -> Text
describe-maybe : maybe -> when maybe {
    My-Some value -> "we have a value: _" value
    My-None -> "we don't have a value"
}
example:7:22: error: cannot describe a `Value` value

Hmm, we run into some trouble here. The problem is that in Wipple, not all values can be converted into text! For example, let’s define a Sport type:

Sport : type {
    name :: Text
    players :: Number
}

basketball : Sport {
    name : "Basketball"
    players : 5
}

How should we display a My-Some basketball? Should we show the name first and then the number of players, or the other way around? What if we want to display an emoji instead? Wipple doesn’t assume any particular format in which to display your custom types. What we need to do is tell Wipple how to convert Sport into text.

If you’ve used other languages, you might be familiar with the concept of an “interface”; for example, in Java, the Comparable interface defines a compareTo function so you can use things like Arrays.sort.

Wipple has something similar called traits — a trait is a container for a value that changes depending on its type. We can define a trait like so:

My-Describe : Value => trait (Value -> Text)

My-Describe accepts a Value type, and produces a trait that stores a function to convert the Value into Text. Now, we can use where to say that describe-maybe is only available when My-Describe is implemented for Value:

My-Describe : Value => trait (Value -> Text)

My-Maybe : Value => type {
    My-Some Value
    My-None
}
describe-maybe :: Value where (My-Describe Value) => (My-Maybe Value) -> Text
describe-maybe : maybe -> when maybe {
    My-Some value -> "we have a value: _" (My-Describe value)
    My-None -> "we don't have a value"
}

Because My-Describe contains a function, we can call it, passing in our value. Now we have a Text value we can use to fill in the placeholder!

Under the hood, _ placeholders wrap the provided value in a call to the built-in Describe trait automatically. Describe is defined in the same way as our My-Describe trait, so from now on, we’ll use Describe instead:

My-Maybe : Value => type {
    My-Some Value
    My-None
}
describe-maybe :: Value where (Describe Value) => (My-Maybe Value) -> Text
describe-maybe : maybe -> when maybe {
    My-Some value -> "we have a value: _" value -- equivalent to `(Describe value)`
    My-None -> "we don't have a value"
}

Now, if we provide a piece of text or a number, our code works!

My-Maybe : Value => type {
    My-Some Value
    My-None
}
describe-maybe :: Value where (Describe Value) => (My-Maybe Value) -> Text
describe-maybe : maybe -> when maybe {
    My-Some value -> "we have a value: _" value
    My-None -> "we don't have a value"
}
show (describe-maybe (My-Some 42))
show (describe-maybe (My-Some "Hello, world!"))
we have a value: 42
we have a value: Hello, world!

Note: We run into trouble if we provide a My-None. That’s because Wipple can’t infer for us what Value is supposed to be. We can fix this with a type annotation:

My-Maybe : Value => type {
    My-Some Value
    My-None
}
describe-maybe :: Value where (Describe Value) => (My-Maybe Value) -> Text
describe-maybe : maybe -> when maybe {
    My-Some value -> "we have a value: _" value
    My-None -> "we don't have a value"
}
show (describe-maybe (My-None :: My-Maybe Number))
we don't have a value

It’s rare that you’ll have to do this, though.

So what about our Sport type from earlier? We can implement Describe for Sport using instance:

Sport : type {
    name :: Text
    players :: Number
}

basketball : Sport {
    name : "Basketball"
    players : 5
}
My-Maybe : Value => type {
    My-Some Value
    My-None
}
describe-maybe :: Value where (Describe Value) => (My-Maybe Value) -> Text
describe-maybe : maybe -> when maybe {
    My-Some value -> "we have a value: _" value
    My-None -> "we don't have a value"
}
instance (Describe Sport) : sport -> do {
    {
        name : name
        players : players
    } : sport

    "_ is played with _ people on each team" name players
}

show (describe-maybe (My-Some basketball))
we have a value: Basketball is played with 5 people on each team

instance can be used in place of a variable name — whenever Wipple sees an assignment to instance, it registers the right-hand side with the provided trait. Then, when we use describe-maybe, Value becomes Sport, and Wipple looks up the corresponding instance and makes it available inside describe-maybe.

You can also use where to make instances conditionally available — let’s replace describe-maybe with a custom implementation of Describe for our My-Maybe type!

My-Maybe : Value => type {
    My-Some Value
    My-None
}
Value where (Describe Value) => instance (Describe (My-Maybe Value)) : maybe -> when maybe {
    My-Some value -> "we have a value: _" value
    My-None -> "we don't have a value"
}

show actually uses Describe, so now we can pass both Sport and My-Maybe values to show, and everything just works!

My-Maybe : Value => type {
    My-Some Value
    My-None
}

Value where (Describe Value) => instance (Describe (My-Maybe Value)) : maybe -> when maybe {
    My-Some value -> "we have a value: _" value
    My-None -> "we don't have a value"
}
Sport : type {
    name :: Text
    players :: Number
}

instance (Describe Sport) : {
    name : name
    players : players
} -> "_ is played with _ people on each team" name players

basketball : Sport {
    name : "Basketball"
    players : 5
}
show basketball
show (My-Some "Hello, world!")
show (My-Some basketball)
Basketball is played with 5 people on each team
we have a value: Hello, world!
we have a value: Basketball is played with 5 people on each team

Now that you know how type functions and traits work, you don’t need to define your own My-Maybe type. Just use the built-in Maybe type, along with Some and None — it’s implemented in exactly the same way you saw here!

Let’s put everything together and refactor our report card program to use traits. We’ll take advantage of the Read trait, which contains a function accepting Text and producing a Maybe Value. prompt uses Read to validate the user’s input for us, so we don’t need to use repeat either!

Grade : type {
    A
    B
    C
    D
    F
}

report-card :: Grade -> Text
report-card : grade -> when grade {
    A -> "top of the class"
    B -> "good work"
    C -> "need to study"
    D or F -> "didn't pass"
}

-- Read : Value => trait (Text -> Maybe Value)
instance (Read Grade) : text -> when text {
    "A" -> Some A
    "B" -> Some B
    "C" -> Some C
    "D" -> Some D
    "F" -> Some F
    _ -> None
}

grade : prompt "Enter your grade"
show (report-card grade)
Enter your grade: Z
invalid input, please try again
Enter your grade: 42
invalid input, please try again
Enter your grade: A
top of the class

Modeling data

Wipple isn’t an object-oriented language, but you can work with data in a similar way using structures and functions. Let’s make a program that manages a bank account to explain how!

We’ll start by defining our Bank-Account structure to hold a balance:

Bank-Account : type {
    balance :: Number
}

Next, we’ll define deposit:

Bank-Account : type {
    balance :: Number
}
deposit :: Bank-Account Number -> Bank-Account
deposit : {balance : balance} amount -> {balance : balance + amount}

For convenience, we’ll implement Describe as well:

Bank-Account : type {
    balance :: Number
}
instance (Describe Bank-Account) : {balance : balance} -> "$_" balance

Let’s try it out!

Bank-Account : type {
    balance :: Number
}

instance (Describe Bank-Account) : {balance : balance} -> "$_" balance

deposit :: Bank-Account Number -> Bank-Account
deposit : {balance : balance} amount -> {balance : balance + amount}
my-account : Bank-Account {balance : 500}
show my-account

my-account : deposit my-account 100
show my-account
$500
$600

In Wipple, you generally create functions that return new values, rather than modifying the original. Doing it this way makes it easier to debug your code, because after you make a change, you still have the old value to compare against. In the example above, we don’t care about the old bank account after we make our deposit, so we just overwrite the my-account variable.

There’s an even better way to write the bank account example: rather than making deposit accept both the bank account and the amount at once, we can split the function into two. Let’s try it:

Bank-Account : type {
    balance :: Number
}
deposit :: Number -> (Bank-Account -> Bank-Account)
deposit : amount -> ({balance : balance} -> {balance : balance + amount})

Here, we create a function that accepts the amount to deposit, and returns another function that does the actual work of updating the account. If this seems strange, keep reading!

We actually don’t need the parentheses around the second function, either — the arrow reads from right to left.

Bank-Account : type {
    balance :: Number
}
deposit :: Number -> Bank-Account -> Bank-Account
deposit : amount -> {balance : balance} -> {balance : balance + amount}

Now, we can split the deposit operation into two steps:

Bank-Account : type {
    balance :: Number
}

instance (Describe Bank-Account) : {balance : balance} -> "$_" balance

deposit :: Number -> Bank-Account -> Bank-Account
deposit : amount -> {balance : balance} -> {balance : balance + amount}
payday : deposit 100

my-account : Bank-Account {balance : 500}
show my-account
show (payday my-account)
$500
$600

First, we call deposit 100. This returns another function that accepts a Bank-Account and produces a new Bank-Account with an additional $100. We give this function a name: payday. Finally, we call payday on my-account, and receive the updated account.

This pattern is called currying, and it’s useful because it lets you separate the action — what you’re trying to do — from the logic — how the action is executed. Notice that payday works independently of any specific bank account! The Wipple standard library uses currying in a lot of places, so it’s something you’ll get used to over time.

A good rule of thumb is to have the outer function accept the “data” inputs, and the inner function accept the “state” input. In object-oriented parlance, deposit(amount) is like a method on a BankAccount class.

One more thing: remember that functions can be used directly — you don’t need to give them a name. So if you have an action that’s difficult to name or is only used once, you can just call the inner function directly!

Bank-Account : type {
    balance :: Number
}

instance (Describe Bank-Account) : {balance : balance} -> "$_" balance

deposit :: Number -> Bank-Account -> Bank-Account
deposit : amount -> {balance : balance} -> {balance : balance + amount}
my-account : Bank-Account {balance : 500}
show ((deposit 100) my-account)
$600

And since (deposit 100) my-account returns another Bank-Account, we can call deposit 200 on that:

Bank-Account : type {
    balance :: Number
}

instance (Describe Bank-Account) : {balance : balance} -> "$_" balance

deposit :: Number -> Bank-Account -> Bank-Account
deposit : amount -> {balance : balance} -> {balance : balance + amount}
my-account : Bank-Account {balance : 500}
show ((deposit 200) ((deposit 100) my-account))
$800

This gets unwieldy quickly, though. Fortunately, Wipple has a dot operator (.) for chaining function calls — . calls the function on the right-hand side with the input on the left-hand side.

More formally:

  • a . f is equivalent to f a,
  • a . f b c is equivalent to (f b c) a,
  • a . f b . g c is equivalent to (g c) ((f b) a),
  • and so on.
Bank-Account : type {
    balance :: Number
}

instance (Describe Bank-Account) : {balance : balance} -> "$_" balance

deposit :: Number -> Bank-Account -> Bank-Account
deposit : amount -> {balance : balance} -> {balance : balance + amount}
my-account : Bank-Account {balance : 500}
show (my-account . deposit 100 . deposit 200)
$800

Although my-account . deposit 100 looks very similar to the myAccount.deposit(100) method call syntax in object-oriented languages, it’s important to remember that they work differently! The result of calling deposit 100 is just a function, which you should give a name to if it makes things more clear.

When you’re reading code that uses the dot operator, it’s helpful to mentally group each piece of code between the dots and think about each action being performed. Then, once you understand the sequence of actions, go back to the beginning and read the input, so you know what those actions are being performed on. For example:

upgraded-car : car . swap engine . refill coolant . rotate tires

“First, swap the engine. Then, refill the coolant. Finally, rotate the tires. Perform this maintenance on car, resulting in upgraded-car.”

In the next chapter, we’ll be using the dot operator a lot!

Collections and sequences

Often, you need to store multiple values in a list. You can do this with the comma operator (,), which creates a List value by default:

numbers : 1 , 2 , 3 , 4

To iterate over each item in a list, use the each function:

numbers : 1 , 2 , 3 , 4
numbers . each show
1
2
3
4

filter lets you keep only the items that satisfy a condition:

numbers : 1 , 2 , 3 , 4
even? : divisible-by? 2
numbers . filter even? . each show
2
4

transform lets you convert each item into a different item:

numbers : 1 , 2 , 3 , 4
double : n -> n * 2
numbers . transform double . each show
2
4
6
8

You can combine transform and filter to do more complicated list processing!

numbers : 1 , 2 , 3 , 4
even? : divisible-by? 2
double : n -> n * 2
numbers
  . filter even?
  . transform double
  . each show
4
8

Instead of each, you can use collect to store the final items back into a list, or another collection like Set or Dictionary if you provide a type annotation.

numbers : 1 , 2 , 3 , 4
even? : divisible-by? 2
double : n -> n * 2
doubled-evens :
  numbers
    . filter even?
    . transform double
    . collect

Wipple’s sequencing functions are “lazy”, meaning they work on one element at a time, and only once elements are requested. You can use next to request the next element in a sequence as a Maybe — if the sequence is finished, you’ll get None back.

numbers : 1 , 2 , 3 , 4
even? : divisible-by? 2
double : n -> n * 2
-- Without `collect`, we get a lazy sequence
sequence :
  numbers
    . filter even?
    . transform double

show (next sequence)
show (next sequence)
show (next sequence)
Some 4
Some 8
None

transform, filter, each, and collect are all implemented using next!

The laziness of sequences simplifies a lot of things — you only need to worry about one element at a time. Let’s explore this now by creating our own sequence:

count : 0
counter : Sequence {
  n : count
  count! : count + 1
  Some n
}

counter
  . take 10
  . each show
0
1
2
3
4
5
6
7
8
9

The Sequence function accepts a block that’s evaluated each time next is called. We start by initializing count to zero, and then incrementing it by one for each item. Remember that the block isn’t evaluated until next is called, so we don’t run into an infinite loop by continually incrementing count. There could be a long delay between each call to next, or we could take a million elements all at once! In the example above, we take just 10 elements, and then we’re done.

Tip: It’s good practice to hide the count variable in a do block so it can’t be changed accidentally outside the sequence:

counter : do {
  count : 0
  Sequence {
    n : count
    count! : count + 1
    Some n
  }
}

But wait, how can we pass a list to transform or each if we don’t call sequence first? Wipple actually has an As-Sequence trait that does this for us! List, Set, Stride, and many other types all implement As-Sequence, and all the sequence functions are of the form Collection Element where (As-Sequence Collection Element) => ....

Let’s look at Stride:

1 to 10 by 2 . each show
1
3
5
7
9

Whereas a range (min to max) is continuous, a stride (min to max by step) counts up in discrete steps. So it implements As-Sequence, and we can use it with all our sequence functions!

Sequence implements As-Sequence, too — it just returns itself. That way, you can chain calls to functions like transform and filter without needing to collect into a list after every step.

API design

Let’s continue working on our bank account example! We’ll start by adding an identifier to each account, so we can look up the owner.

Bank-Account : type {
    id :: Number
    balance :: Number
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "account #_: $_" id balance

Then we can define an open-account function to build a bank account with the provided identifier:

Bank-Account : type {
    id :: Number
    balance :: Number
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "account #_: $_" id balance
open-account :: Number -> Bank-Account
open-account : id -> {
    id : id
    balance : 0
}

Finally, we’ll make a new account!

Bank-Account : type {
    id :: Number
    balance :: Number
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "account #_: $_" id balance

open-account :: Number -> Bank-Account
open-account : id -> {
    id : id
    balance : 0
}
my-account : open-account 500
show my-account
account #500: $0

Hmm, it looks like we made a mistake — this code seems like it’s trying to open an account with $500, but instead it creates an account associated with the identifier 500. open-account takes a Number as input, but it’s not clear what that number actually represents.

Let’s make this API easier to understand and less error-prone by introducing a new type!

Account-ID : type Number

Rather than listing the fields (for a structure) or variants (for an enumeration), when you provide a single type, Wipple creates a wrapper for you:

Account-ID : type Number
-- Create an account ID wrapping 0
my-id : Account-ID 0

-- Unwrap the account ID to get the number back
Account-ID id-number : my-id

Let’s refactor Bank-Account to use our new Account-ID type!

Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id

Bank-Account : type {
    id :: Account-ID
    balance :: Number
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: $_" id balance

open-account :: Account-ID -> Bank-Account
open-account : id -> {
    id : id
    balance : 0
}

Now if we try to open a bank account with a plain number, we get an error:

Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id

Bank-Account : type {
    id :: Account-ID
    balance :: Number
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: $_" id balance

open-account :: Account-ID -> Bank-Account
open-account : id -> {
    id : id
    balance : 0
}
my-account : open-account 500
example:20:27: error: expected `Account-ID` here, but found a number

Great! Now it’s clear what kind of data open-account accepts. In general, when you’re designing APIs, try to create wrapper types around “plain” values like Number and Text to give the user of your API more information.

We can do the same thing for our balance, too. Another benefit of wrapper types is that you can customize how they’re displayed!

Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id
Balance : type Number

instance (Describe Balance) : (Balance dollars) -> "$_" dollars

Bank-Account : type {
    id :: Account-ID
    balance :: Balance
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: _" id balance

Next, let’s implement Add for Balance, and refactor deposit to use it:

Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id

Bank-Account : type {
    id :: Account-ID
    balance :: Balance
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: _" id balance

Balance : type Number

instance (Describe Balance) : (Balance dollars) -> "$_" dollars
instance (Add Balance Balance Balance) :
    (Balance current) (Balance amount) -> Balance (current + amount)

open-account :: Account-ID -> Bank-Account
open-account : id -> {
    id : id
    balance : Balance 0
}

deposit :: Balance -> Bank-Account -> Bank-Account
deposit : deposit -> {
    id : id
    balance : current
} -> {
    id : id
    balance : current + deposit
}

my-account : open-account (Account-ID 123)
show (my-account . deposit (Balance 50))

What about withdraw? Withdrawing is a bit trickier, since you can’t withdraw more than the account’s balance. Let’s use Maybe to represent this condition — if you have enough money in your account, you get back a Some value, and if you try to withdraw too much, you get back None:

Balance : type Number
instance (Subtract Balance Balance (Maybe Balance)) :
    (Balance current) (Balance amount) ->
        if (amount <= current) {Some (Balance (current - amount))} {None}

Now it’s up to you how to implement withdraw. In this example, we’ll revert back to the bank account as it was before attempting the withdrawal. This is where producing new values in Wipple, rather than mutating them in place, comes in handy!

Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id

Bank-Account : type {
    id :: Account-ID
    balance :: Balance
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: _" id balance

Balance : type Number

instance (Describe Balance) : (Balance dollars) -> "$_" dollars

instance (Subtract Balance Balance (Maybe Balance)) :
    (Balance current) (Balance amount) ->
        if (amount <= current) {Some (Balance (current - amount))} {None}

open-account :: Account-ID -> Bank-Account
open-account : id -> {
    id : id
    balance : Balance 0
}
withdraw :: Balance -> Bank-Account -> Bank-Account
withdraw : withdrawal -> {
    id : id
    balance : current
} -> when (current - withdrawal) {
    Some new -> {
        id : id
        balance : new -- use the new balance
    }
    None -> {
        id : id
        balance : current -- revert to the balance as it was before withdrawing
    }
}

my-account : open-account (Account-ID 123)
show (my-account . withdraw (Balance 50))
account #123: $0

Great, now our API is designed so it’s impossible to have a negative balance!

Here’s the full code for our bank account API, along with documentation comments:

-- An identifier for a bank account.
Account-ID : type Number

instance (Describe Account-ID) : (Account-ID id) -> "account #_" id

-- An amount of money stored in a bank account.
Balance : type Number

instance (Describe Balance) : (Balance dollars) -> "$_" dollars

instance (Add Balance Balance Balance) :
    (Balance current) (Balance amount) -> Balance (current + amount)

instance (Subtract Balance Balance (Maybe Balance)) :
    (Balance current) (Balance amount) ->
        if (amount <= current) {Some (Balance (current - amount))} {None}

-- A bank account.
Bank-Account : type {
    id :: Account-ID
    balance :: Balance
}

instance (Describe Bank-Account) : {
    id : id
    balance : balance
} -> "_: _" id balance

-- Open an account with the provided identifier.
open-account :: Account-ID -> Bank-Account
open-account : id -> {
    id : id
    balance : Balance 0
}

-- Deposit some money into a bank account.
deposit :: Balance -> Bank-Account -> Bank-Account
deposit : deposit -> {
    id : id
    balance : current
} -> {
    id : id
    balance : current + deposit
}

-- Attempt to withdraw some money from a bank account. If the account's balance
-- is too low, it will be left unchanged.
withdraw :: Balance -> Bank-Account -> Bank-Account
withdraw : withdrawal -> {
    id : id
    balance : current
} -> when (current - withdrawal) {
    Some new -> {
        id : id
        balance : new -- use the new balance
    }
    None -> {
        id : id
        balance : current -- revert to the balance as it was before withdrawing
    }
}

How repeat works

Before you read this article, you might want to read about type functions and traits.

The definition of repeat may seem really strange:

repeat :: State Body Result where (Repeat-Predicate State Body Result) => State {Body} -> Result

repeat is written this way to allow different methods for controlling the loop. repeat doesn’t just accept n times as its first input, it also accepts while {x}, forever, with-control-flow, and anything of your own creation! Let’s build our own implementation of repeat to learn how it works.

Start with the types

Before we write any executable code, let’s think about how having a user-specified method for controlling the loop — a predicate — influences what types we need. For simplicity, our initial version of repeat will always return None rather than a generic Result. Let’s start by thinking about the relationship the predicate establishes between its inputs and outputs.

We have two components in our relationship:

  • Some value that tells repeat whether to run its body again or stop.
  • Some way for the user to control the conditions for this value.

This maps to two types. The second, State, is initially given by the user, and the job of the predicate is to update this state over time.

The first component, the one that tells repeat whether to run again or stop, is actually one we define. We could use Boolean, but then it’s hard to tell whether True means “continue” and False means “stop”, or vice versa. Let’s make our own type specifically for repeat!

Control-Flow : type {
    Continue
    Stop
}

This won’t be enough to make repeat actually repeat, but it’s close enough for now. Let’s define our relationship in the simplest way possible, and we’ll extend it with more type parameters later!

-- A predicate in a `repeat` takes a state and tells `repeat` whether to
-- `Continue` or `Stop`.
Repeat-Predicate : State => trait (State -> Control-Flow)

A quick refresher on why we have this at all — we don’t want users of repeat to have to worry about how to get a Control-Flow from a State, we just want them to provide a state and let repeat figure out what to do with it. Traits in Wipple let you make split the implementation of repeat apart so it works on different types. Within the implementation of repeat, we’ll call the function defined by Repeat-Predicate on the state provided by the user.

Do you see the problem? The state never changes! Once repeat calls Repeat-Predicate with the initial state (the one the user provides, like 4 times), there’s no way to get a new state (like 3 times, 2 times, 1 times, and finally 0 times).

There are multiple ways we could do this — one is by attaching a State to the return value of the function:

Repeat-Predicate : State => trait (State -> (Control-Flow ; State))

But this is actually more strict than we’d like. What if Control-Flow is Stop? There may no longer be a valid state to return, since we’ve exhausted the loop! Let’s move State to the Continue variant of Control-Flow.

Control-Flow : State => type {
    Continue State
    Stop
}

Note that this version of Control-Flow is basically Maybe, just with a more specific meaning. Maybe is kind of like Boolean in this scenario — it works, but it’s not as clear.

Now, the predicate is only required to produce a new State when it actually wants the loop to continue.

OK, let’s update our trait to use our new version of Control-Flow!

-- A predicate in a `repeat` takes a state and tells `repeat` whether to
-- `Continue` with the next state or `Stop`.
Repeat-Predicate : State => trait (State -> Control-Flow State)

repeat :: State where (Repeat-Predicate State) => State {None} -> None

Writing the implementation

Now that we have types representing the possible interactions in our code, we can put them all together and implement repeat. We’ll write expressions that have the appropriate type at every step, working our way inward.

Here is the type of repeat from above, ignoring the bounds:

repeat :: State {None} -> None

So we need a function that takes a State and a {None}:

repeat : state body -> ...

On the right-hand side, we’ll call Repeat-Predicate on our state to get a Control-Flow:

repeat : state body -> when (Repeat-Predicate state) {
    Continue next -> ...
    Stop -> ...
}

The Stop arm is simple — just produce a unit value:

    Stop -> ()

On the Continue arm, we execute body, and then call repeat again with our next state:

    Continue next -> do {
        do body
        repeat next body
    }

You might be thinking, “wait, we aren’t actually looping — that’s just recursion!” Wipple will convert it to a traditional loop under the hood using something called tail call optimization. This is really how repeat is implemented in Wipple!

Here’s the full code:

repeat :: State where (Repeat-Predicate State) => State {None} -> None
repeat : state body -> when (Repeat-Predicate state) {
    Continue next -> do {
        do body
        repeat next body
    }
    Stop -> ()
}

Adding a predicate

Let’s now create a predicate we can use with our repeat function. n times sounds fun!

-- `Times` wraps `Number`
Times : type Number

-- Usage: `n times`
times :: Number -> Times
times : n -> Times n

-- Our predicate stops the loop when `n` is `0`
instance (Repeat-Predicate Times) : (Times n) ->
    if (n > 0) {Continue ((n - 1) times)} {Stop}

And now we can try out our implementation!

repeat (4 times) {
    show "Hello, world!"
}
Hello, world!
Hello, world!
Hello, world!
Hello, world!

Full code
-- All the definitions are prefixed with "my" here, so you can easily run this
-- in the Wipple Playground.

My-Control-Flow : State => type {
    My-Continue State
    My-Stop
}

My-Repeat-Predicate : State => trait (State -> My-Control-Flow State)

my-repeat :: State where (My-Repeat-Predicate State) => State {None} -> None
my-repeat : state body -> when (My-Repeat-Predicate state) {
    My-Continue next -> do {
        do body
        my-repeat next body
    }
    My-Stop -> ()
}

My-Times : type Number

my-times :: Number -> My-Times
my-times : n -> My-Times n

instance (My-Repeat-Predicate My-Times) : (My-Times n) ->
    if (n > 0) {My-Continue ((n - 1) my-times)} {My-Stop}

my-repeat (4 my-times) {
    show "Hello, world!"
}

Supporting more predicates

Let’s try to implement the with-control-flow predicate, which allows the user to provide a Control-Flow themselves in the body, and is more complicated in two ways:

  • Whether the loop continues or not depends on the result of the loop’s body.
  • When the loop exits, it can return something other than None.

We’ll need to change our definitions of Control-Flow and Repeat-Predicate to accommodate these requirements.

Control-Flow : Next Result => type {
    Continue Next
    Stop Result -- now `Stop` contains a value, too
}

Repeat-Predicate : State Body Result =>
    trait (State -> Control-Flow (Body -> State) Result)

Notice that the Continue variant now accepts a function to convert the result of the body into the next state. This function is provided by the repeat predicate, and called by the implementation of repeat after evaluating the body. Now we can break our task into three steps from the perspective of repeat:

  1. Call Repeat-Predicate on the current state.
  2. If the predicate returns Continue next (where next is the function, ie. next :: Body -> State), evaluate the body and call next with the result to get the new state.
  3. Call Repeat-Predicate with the new state, and so on.

Of course, if the predicate returns Stop result instead, we just return result rather than continuing. In fact, we have no choice but to stop, because we don’t have another state to pass back to Repeat-Predicate!

repeat :: State Body Result where (Repeat-Predicate State Body Result) => State {Body} -> Result
repeat : state body ->
    -- (1)
    when (Repeat-Predicate state) {
        Continue next -> do {
            -- (2)
            new-state : next (do body)

            -- (3)
            repeat new-state body
        }

        Stop result -> result
    }

This is the same as the built-in definition of repeat!

Now we can implement with-control-flow, which starts in the Continue state and updates the stored Control-Flow value based on the result of the body:

-- Store the previous `Control-Flow` returned by the body
With-Control-Flow : Result => type {
    current :: Control-Flow None Result
}

-- Start in the `Continue` state so the loop runs at least once
with-control-flow :: Result => With-Control-Flow Result
with-control-flow : {current : Continue ()}

-- The loop body must return a `Control-Flow None Result`
Result => instance (Repeat-Predicate (With-Control-Flow Result) (Control-Flow None Result) Result) :
    {current : current} -> when current {
        -- If we stored a `Continue`, the new state is the result of the body,
        -- which will influence whether the loop is run again the next time
        -- `Repeat-Predicate` is called
        Continue () -> Continue (new -> {current : new})

        -- If we stored a `Stop`, produce the stored result. The loop won't run
        -- again, but if it did, this would keep producing `result` because we
        -- never update the state again
        Stop result -> Stop result
    }

Let’s try it!

-- Loop a random number of times and eventually print "done"

result : repeat with-control-flow {
    if (random ()) {Continue ()} {Stop "done"}
}

show result
done

As an exercise, try updating the Repeat-Predicate instance for Times.

Show answer

Body and Result are both None, just like before. Wrap (n - 1) times in a function:

instance (Repeat-Predicate Times None None) : (Times n) ->
    if (n > 0) {Continue (() -> (n - 1) times)} {Stop ()}

Conclusion

Now that you’ve implemented repeat yourself, hopefully you have a better idea of why it has the type signature it does — repeat is a very flexible construct that abstracts over the looping behavior. Adding your own predicates is a good way to practice your understanding of traits in Wipple! If you want to learn more, check out repeat.wipple in the Wipple standard library on GitHub.

Interactive elements in the playground

If you use square brackets instead of parentheses, the Wipple Playground will render your code as an interactive element. The supported elements include:

ElementExampleScreenshot
Colorcolor [Color "#3b82f6"]color element
Dropdownshow [Dropdown ("A" , "B" , "C") "A"]dropdown element

You can also change how a declaration is highlighted using the Highlight trait and its related types Highlight-Category and Highlight-Icon. For example:

my-if :: Value => Boolean {Value} {Value} -> Value
my-if : if

instance (Highlight my-if (Highlight-Category "control-flow"))

Would be rendered as:

Screenshot of custom-highlighted declaration

You can add an icon, too — the Wipple Playground uses Google’s Material Symbols, so you have access to over 3000 different icons!

instance (Highlight my-if (Highlight-Category "control-flow" ; Highlight-Icon "question-mark"))

Screenshot of declaration with custom icon

Wipple has several highlight categories, including control-flow, io, and library-specific categories like turtle-motion. The full list of categories is at the bottom of theme.ts.

Custom error messages

Wipple has a few built-in types and traits that you can use to create custom error messages for your API.

The Error trait

Whenever Wipple resolves an instance of the Error trait, it generates an error message. So if you add a bound involving Error to a function or instance, you can create different messages depending on how your API is misused.

In this example, Error is used to mark a function as unavailable:

print :: Value where (Error "use `show` to display on the screen") => Value -> ()
print : unreachable

print "Hello, world!" -- error: use `show` to display on the screen

Error accepts a text value, which can also contain placeholders. The inputs to the placeholders are types, rather than values.

Value where (Error ("_ has no description" Value)) =>
    default instance (Describe Value) : unreachable

my-value : ()
Describe my-value -- error: unit has no description

If you want to display the source code of the input rather than its type, surround the placeholder with backticks (`_`):

Value where (Error ("`_` has no description" Value)) =>
    default instance (Describe Value) : unreachable

my-value : ()
Describe my-value -- error: `my-value` has no description

Error utilities

In addition to providing a message to Error, you may provide a tuple containing the message alongside these types:

  • The Error-Location type is used to change where the error appears in the source code:

    Value where (Error ("`_` has no description" Value ; Error-Location Value)) =>
        default instance (Describe Value) : unreachable
    
    my-value : ()
    Describe my-value -- error: `my-value` has no description
    --       ~~~~~~~~
    
  • The Error-Fix type is used to generate a fix. The first input is the fix message, and the second input is the replacement for the code highlighted by the error:

    Body where (Error ("missing `repeat` here" ; Error-Fix "add `repeat`" ("`repeat _`" Source))) =>
        instance (Mismatch Times ({Body} -> ()))
    
    (4 times) { -- error: missing `repeat` here
        show "Hello, world!"
    }
    
    -- After clicking "add `repeat`"...
    repeat (4 times) {
        show "Hello, world!"
    }
    
  • The Source type is always displayed as the source code for the expression that caused Wipple to resolve the bound. The above example uses Source to add repeat before (4 times).

The Mismatch type

Whenever two types mismatch, Wipple will attempt to resolve an instance of Mismatch. You can define your own instances and add Error bounds to generate custom error messages:

Box : Value => type Value

Value where (Error ("not a box" ; Error-Location Value ; Error-Fix "add `Box`" ("`(Box _)`" Value))) =>
    instance (Mismatch Value (Box Value))

(42 :: Box _) -- not a box

-- After clicking "add `Box`"...
((Box 42) :: Box _)

Syntax

Grammar

Type system

In Wipple, every expression must have a type known at compile-time. For most code, though, Wipple can infer the type.

Type annotations

To annotate the type of an expression explicitly, use the :: operator:

show ("Hello, world!" :: Text)

A type annotation at the top level is instead considered to be a constant definition, where the constant’s body is defined on the next line:

-- Create a new constant named `pi` with type `Number`
pi :: Number
pi : 3.14

If you actually want a type annotation rather than a constant definition, wrap the expression in parentheses:

-- Assert that the existing constant `pi` has type `Number`
(pi :: Number)

Builtin types

Literals

Number literals have the type defined by the number intrinsic. In the standard library, this is Number.

Text literals have the type defined by the text intrinsic. In the standard library, this is Text.

Functions

Function types are written A B C -> D, where A, B, and C are the inputs and D is the output.

Tuples

Tuple types are written A ; B ; C. Each element type may be different.

A tuple with a single element is written with a trailing semicolon (A ;). A single-element tuple is not equal to the element type on its own (ie. A ; is not equivalent to A).

Blocks

Block types are written {A}, where A is the type of the last statement in the block.

The empty block produces a value of type None, and so it has type {None}.

Intrinsics

The keyword intrinsic may be used in type position to refer to a runtime-provided type. All intrinsic types are equivalent to each other, but are not equivalent to any other type.

It’s not possible to create a value of type intrinsic in Wipple. The runtime always uses wrapper types like Number, Text, and List, instead of returning a value of type intrinsic.

Type-level text

Type-level text is written the same way as regular text, except the inputs are types. Type-level text is currently only used for producing custom error messages at compile time — it’s not possible to create a value with this type at runtime.

User-defined types

Wipple has four kinds of user-defined types: marker types, wrapper types, structure types, and enumeration types.

Marker types

Marker types contain no information and have a single value. They are defined with the type keyword:

Marker : type
instance (Describe Marker) : _ -> "marker"

show Marker -- marker

Wrapper types

Wrapper types contain a single value of the wrapped type, but you must explicitly convert between the wrapper and the wrapped value. They are defined with the type keyword, followed by the type to wrap:

User-ID : type Text
instance (Equal User-ID) : (User-ID a) (User-ID b) -> a = b

id : User-ID "abc"
id = "abc" -- error: expected `User-ID`, but found `Text`

Structure types

Structure types contain one or more fields. They are defined with the type keyword, followed by a block containing fields:

Sport : type {
    name :: Text
    players :: Number
}

instance (Describe Sport) : {
    name : name
    players : players
} -> "_ has _ players per team" name players

basketball : Sport {
    name : "Basketball"
    players : 5
}

show basketball -- Basketball has 5 players per team

Enumeration types

Enumeration types contain one or more variants. An enumeration value may only be one variant at a time. Enumeration types are defined with the type keyword, followed by a block containing variants:

Primary-Color : type {
    Red
    Green
    Blue
}

instance (Describe Primary-Color) : color -> when color {
    Red -> "red"
    Green -> "green"
    Blue -> "blue"
}

favorite-color : Blue
show favorite-color -- blue

Variants may also store values:

JSON : type {
    Null-Value
    String-Value Text
    Number-Value Number
    Array-Value (List JSON)
    Object-Value (Dictionary Text JSON)
}

Generics

Types can accept type parameters to make them generic:

Linked-List : Value => type {
    Nil
    Cons Value (Linked-List Value)
}

You can make constants generic with the same syntax:

join :: Value => (Linked-List Value) (Linked-List Value) -> Linked-List Value
join : ...

Traits and instances

Traits allow you to define functionality across a range of types. For example, the standard library’s Describe trait is used to convert values into Text. It’s used by show to display things on the screen.

Defining a trait

You can define a trait using the trait keyword, followed by the type of the value the trait represents:

Describe : Value => trait (Value -> Text)

Traits may have multiple type parameters:

Add : Left Right Sum => trait (Left Right -> Sum)

The value doesn’t have to be a function; for example, the Empty trait defines the “empty value” of a type, like 0 for Number and "" for Text:

Empty : Value => trait Value

Implementing a trait

You can define an instance for a trait using the instance keyword on the left side of the : assignment operator:

Person : type {name :: Text}

instance (Describe Person) : {name : name} -> "Hi, I'm _" name

A trait T is said to be “implemented” for a type(s) if there is an instance (T ...) corresponding to those type(s).

Note that there is no “primary type” that the trait is implemented on — an instance just represents a set of types corresponding to a trait implementation. In the following example, it’s more correct to say that Add is implemented for the three Numbers rather than on the Left or Right one.

Add : Left Right Sum => trait (Left Right -> Sum)
instance (Add Number Number Number) : ...

You can only define one instance for some set of types at a time:

instance (Describe Person) : ...
instance (Describe Person) : ... -- error

Using a trait

To use a trait, refer to it by name — Wipple will infer the types of the trait’s type parameters based on the surrounding context and select the instance that matches:

Foo : type
Bar : type

Trait : A => trait A
instance (Trait Foo) : Foo -- (1)
instance (Trait Bar) : Bar -- (2)

(Trait :: Foo) -- selects (1)
(Trait :: Bar) -- selects (2)

This works for functions and other complex types, too:

-- infers `Left` and `Right` as `Number`, so `instance (Add Number Number Number)`
-- is selected and the entire expression has type `Number` (aka. `Sum`)
Add 1 2

Inferred type parameters

When you mark a type parameter with infer in a trait, that type parameter is not considered when checking for overlapping instances. This constraint aids in type inference and can produce better error messages. For example, Sum in Add is marked infer:

Add : Left Right (infer Sum) => trait (Left Right -> Sum)

As a result, writing (Add 1 2 :: Text) produces expected `Number`, but found `Text`, rather than could not find instance `(Add Number Number Text)`.

Bounds

Bounds allow you to make a generic constant or instance available conditionally. For example, show requires that its input implement Describe:

show :: Value where (Describe Value) => Value -> ()
show : ...

Similarly, Maybe only implements Equal if its Value does:

Value where (Equal Value) => instance (Equal (Maybe Value)) : ...

Within a constant’s or instance’s body, the bounds are implied, so you can use instances that refer to the bounded type parameters. But when using the constant or instance from the outside, it’s up to the caller to ensure the bounds are satisfied.

Value where (Equal Value) => instance (Equal (Maybe Value)) : a b -> when (a ; b) {
    (Some a ; Some b) -> Equal a b -- resolves to `instance (Equal Value)`
    ...
}

Intrinsics

Wipple uses intrinsics to give the compiler special knowledge of certain types, traits and constants. For example, whenever the + operator is used, Wipple calls the function defined by the add intrinsic — in the standard library, this is the Add trait.

To define an intrinsic, use the intrinsic keyword, followed by the type of value (type, trait, or constant) and its name:

intrinsic "trait" "add" : Add

The compiler assumes that the associated type, trait, or constant has the correct shape and behavior; if it doesn’t, the compiler may produce invalid code, strange type errors, or crash.

Here’s a list of the intrinsics used in Wipple:

NameTypeValueUsed By
addtraitAddThe + operator
andtraitAndThe and operator
astraitAsThe as operator
booleantypeBooleanThe runtime
build-collectionconstantBuild-CollectionThe , operator
bytraitByThe by operator
dividetraitDivideThe / operator
equaltraitEqualThe = operator
errortraitErrorCustom error messages
falseconstantFalseThe runtime
greater-than-or-equaltraitGreater-Than-Or-EqualThe >= operator
greater-thantraitGreater-ThanThe > operator
hashertypeHasherThe runtime
initial-collectionconstantinitial-collectionThe , operator
is-equal-toconstantIs-Equal-ToThe runtime
is-greater-thanconstantIs-Greater-ThanThe runtime
is-less-thanconstantIs-Less-ThanThe runtime
less-than-or-equaltraitLess-Than-Or-EqualThe <= operator
less-thantraitLess-ThanThe < operator
listtypeListThe runtime
maybetypeMaybeThe runtime
multiplytraitMultiplyThe * operator
noneconstantNoneThe runtime
not-equaltraitNot-EqualThe /= operator
numbertypeNumberType of number literals
ortraitOrThe or operator
orderingtypeOrderingThe runtime
powertraitPowerThe ^ operator
remaindertraitRemainderThe % operator
showconstantDescribe_ in text literals
someconstantSomeThe runtime
subtracttraitSubtractThe - operator
task-grouptypeTask-GroupThe runtime
texttypeTextType of text literals
totraitToThe to operator
trueconstantTrueThe runtime

The runtime also exposes many functions that are called with intrinsic; see the runtime source code.

Project overview

When you write Wipple code, you’re essentially typing text into a text field. But computers don’t work with text, they operate using instructions on memory. That means the text you type needs to be transformed into a format the computer can work with more directly. The program responsible for these performing these transformations is called the Wipple compiler.

The Wipple compiler and related programs, including the interpreter that runs the compiled code and the functionality for displaying error messages, operate in several stages and are split across many folders in the Wipple repository. The majority of the project is written in the Rust programming language; if you aren’t familiar with Rust, please visit Learn Rust.

It’s good to know that the Wipple programming language is itself written in a different programming language. The Wipple compiler is essentially a program that takes text as input and tries to convert that text into computer instructions according to Wipple’s rules. This program is not Wipple itself!

Think of it like writing the user interface for a mobile app: the interface doesn’t know what order the user will navigate through the app, but the app must be able to handle all possible valid ways of interacting with the app, and disallow invalid interactions so that the app doesn’t end up in an incorrect state. The Wipple compiler has a similar goal — it must accept all “valid” programs and reject all “invalid” programs. The Rust code is what determines what Wipple code is valid.

Here’s an overview of how your code goes from text to a runnable program.

flowchart TD
    input["<strong>Input</strong><br><code>show (1 + 2)</code>"]
    lexing["<strong>Lexing</strong><br><code>'show' '(' '1' '+' '2' ')'</code>"]
    parsing["<strong>Parsing</strong><br><code>(call (name 'show') (operator '+' (number '1') (number '2')))</code>"]
    externalInterface["<strong>External Interface</strong><br><code>show :: Value -> None, Add : trait, ...</code>"]
    lowering["<strong>Lowering</strong><br><code>'constant show' ('trait Add' ('constructor Number' 1) ('constructor Number' 2))</code>"]
    typechecking["<strong>Typechecking</strong><br><code>(show :: Number -> None) ((instance Add Number Number Number) 1 2)</code>"]
    interfaceGeneration["<strong>Interface Generation</strong><br><code>top level</code>"]
    irGeneration["<strong>IR Generation</strong><br><code>push 1, push 2, constant 'add', call 2, constant 'show', call 1</code>"]
    libraryGeneration["<strong>Library Generation</strong><br><code>top level</code>"]
    externalLibrary["<strong>External Library</strong><br><code>show, instance Add Number Number Number</code>"]
    linking["<strong>Linking</strong><br><code>show, instance Add Number Number Number, top level</code>"]
    executable["<strong>Executable</strong>"]
    output["<strong>Output</strong><br><code>3</code>"]
    input --> lexing
    lexing --> parsing
    parsing --> lowering
    externalInterface --> lowering
    lowering --> typechecking
    typechecking --> interfaceGeneration
    interfaceGeneration --> lowering
    interfaceGeneration --> irGeneration
    irGeneration --> libraryGeneration
    externalLibrary --> linking
    libraryGeneration --> linking
    linking --> executable
    executable --> output
  1. Input: Wipple reads your code and stores it in a string.
  2. Lexing (tokenization): The string is split into a list of tokens, where each token represents a single piece of syntax, like a name, number, or symbol.
  3. Parsing: Wipple converts this list of tokens into a structured tree that it can traverse.
    • Concrete syntax tree (CST) construction: The tokens are split and grouped by punctuation and newlines.
    • Abstract syntax tree (AST) construction: “Concrete” syntax is transformed into “abstract” Wipple constructs based on context.
      • For example, the A -> B represents a function type in x :: A -> B, but it represents a function expression in x : A -> B. Similarly, _ is a valid type or pattern, but not a valid expression.
  4. Lowering: The structure of the program defines scope, and definitions are brought into scope and names resolved according to their position within the tree. At this point, names are represented using unique identifiers rather than strings.
  5. Typechecking: Wipple assigns every a type to every expression in the program and resolves instances for traits.
  6. Interface generation: The type information from all top-level items in the program is stored in an interface file that can be referenced by other files during lowering.
  7. Intermediate representation (IR) generation: The tree of expressions is converted into a list of instructions that operate on a stack of memory.
  8. Library generation: The IR for each item in the program is stored in a library file.
  9. Linking: Multiple library files are combined into a single executable.

From there, the executable can be run using the IR interpreter.

Lexing

Lexing, aka. tokenization, is the process of splitting the string of source code into a list of “tokens”. A good way to think of tokens is like they are atoms, the smallest meaningful unit of syntax.

This preprocessing step means the parser doesn’t have to iterate over the code character by character — without tokenization, there would be a lot of code to group characters together mixed with the parsing code. For example, without tokenization, the parser would have to do something like this:

Input: foo :: Number

  • Scan f; f is a character, so start a name.
  • Scan o; o is a character and we have a name, so the name is fo.
  • Scan o; o is a character and we have a name, so the name is foo.
  • Scan whitespace; stop the name.
  • Scan :; we either have : or ::.
  • Scan :; we have ::.
  • Scan whitespace; ignore.
  • Scan N; N is a character, so start a name.
  • etc.

But with lexing, the parser can already start with foo, ::, and Number split into separate groups.

Lexing is a bit more complicated than let tokens = code.split(" "), since not all tokens have whitespace between them — we want to split (x) into three tokens ((, x, and )), not one. Wipple uses the Logos library to define regular expressions that denote boundaries between tokens.

How Wipple tokenizes code

Open compiler/syntax/src/tokenize.rs to follow along.

  1. The source code string, s, is passed to the tokenize function.

  2. Wipple calls logos::Lexer::new(s).spanned(), which returns an Iterator that produces a Result<RawToken, ()> and a Range<usize>. The Result is either Ok(RawToken) (a valid token) or Err(()) (an invalid token — the default error type in Logos is (); Wipple currently does not have custom error handling for invalid tokens). The Range is called a “span”, aka. the start and end positions in the source code where the token is located.

  3. Wipple maps over each of these results, converting the RawTokens into Tokens, which are categorized so that operations on similar kinds of tokens (eg. operators or keywords) can be shared. Any Err(()) values are converted to Diagnostic::InvalidToken. Both the Ok and Err variants are put in a WithInfo structure containing a Location value that wraps the span.

  4. Wipple reports diagnostics for the Err values.

  5. Wipple calls to_logical_lines on the Ok values, which replaces multiple line breaks with single line breaks, and removes line breaks between lines involving operators.

    For example:

    CodeEquivalent toBehavior
    a ba bSingle line
    a
    b
    a
    b
    Line break
    a

    b
    a
    b
    Multiple line breaks are reduced
    into a single line break
    a
    + b
    a + bOperator joins lines
  6. Wipple takes the processed list of tokens returned by to_logical_lines and passes it to TokenTree::from_top_level. A “token tree” is a kind of concrete syntax tree that does grouping by parentheses and operators, but has no further syntactic structure. In this stage, comments are removed and the original code is no longer recoverable.

    TokenTree::from_top_level works by maintaining a stack of groups — when a left parentheses is encountered, for example, a TokenTree::List is pushed onto the stack, and when a right parentheses is encountered, this list is popped and appended to the group that’s now on the top. Non-grouping symbols are always appended to the group on the top of the stack. We easily detect mismatched parentheses/brackets and braces by checking if the top of the stack exists and is the expected group.

    There’s also a similar function, TokenTree::from_inline, that just calls TokenTree::from_top_level and extracts the first statement in the output. This will eventually be useful for tests, since you can pass a string like 1 + 2 and get back the operator expression directly instead of it being wrapped in a block.

  7. The resulting token tree is returned to the driver, and any diagnostics reported.

Operators

If you look through the lexer, you’ll see three kinds of operators: binary, non-associative, and variadic. Each one is processed a bit differently.

First, it’s important to know that every operator has a precedence, or priority — operators with a higher precedence group more tightly. For example, * has a higher precedence than +, so 1 + 2 * 3 is equivalent to 1 + (2 * 3) and not (1 + 2) * 3.

  • Binary operators accept two token trees, one on either side of the operator. Every binary operator has an associativity, which determines the grouping behavior when multiple operators of the same precedence are encountered.

    For example, - is left-associative (ie. has Associativity::Left), so 3 - 2 - 1 is equivalent to (3 - 2) - 1 (0) and not 3 - (2 - 1) (2).

    ->, on the other hand, is right-associative, so Number -> Number -> Number is equivalent to Number -> (Number -> Number) (which returns a function as output) and not (Number -> Number) -> Number (which accepts a function as input).

  • Non-associative operators also accept two token trees, one on either side, but have no associativity. If Wipple encounters multiple non-associative operators with the same precedence, it will produce an error. Non-associativity is essentially a way to require that an operator is used only once in an expression. For example, : is non-associative because you can only have one definition per line. (a : b : c doesn’t make sense in Wipple.)

  • Variadic operators accept zero or more token trees as input; the operator serves as a separator between each token tree. Wipple has two variadic operators: ; for tuples and , for collections. You may provide a trailing operator after the inputs (ie. 1 , 2 , 3 , is valid). To represent zero inputs, specify the operator on its own: ,. Normally, you wrap the operator in parentheses to avoid ambiguity: (,) is the typical syntax for an empty list.

Parsing

Lowering

Typechecking

The typechecker is responsible for determining that every expression in your program is used correctly. For example, adding a string to a number would raise an error in a typechecked program instead of producing invalid results. To resolve the error, you have to be explicit: do you want to convert the string to a number and add the two numbers, or convert the number into a string and concatenate the two strings?

Wipple’s typechecker works in four main steps:

  1. Type inference: Assigning every expression a type
  2. Unification: Checking that every expression’s type is valid in the surrounding context
  3. Bounds checking: Finding implementations for constant and trait expressions
  4. Finalization: Ensuring that every expression in the program has a concrete type

The typechecker works on one declaration (constant, instance, or top-level code) at a time; the implementation of one declaration cannot affect how the implementation of a different declaration is compiled.

Type inference

The basic idea behind type inference is that every expression resolves to some value at runtime, and we want to determine the “shape” of that value at compile time. This shape, or type, can be determined automatically based on how the value is used. To make our programs deterministic, there is a rule: the type of an expression must be preserved while it is being evaluated. Essentially, we should be able to “pause” the program at any point and see that even if an expression has been reduced, its type hasn’t changed:

f :: A -> B
g :: B -> C
h :: C -> D

-- The type of every expression in this program is preserved during evaluation.
(h (g (f (x :: A) :: B) :: C) :: D)
(h (g (f x :: B) :: C) :: D)
(h (g (f x) :: C) :: D)
(h (g (f x)) :: D)

With this rule of type preservation in mind, we can apply some constraints to the expressions. For example, if f :: A -> B, we know that f x :: B and x :: A. Likewise, if we know x :: A, then we at least know f :: A -> _ (where _ is unknown). If the result of f x is then assigned to a variable with type B, now we know the full type of f to be A -> B. By building a giant list of constraints and solving them, we can infer the types of most expressions automatically, without the need for explicit type annotations everywhere!

In Wipple, there is only one type of constraint used for type inference: the substitution. Essentially, we have a map between type variables (not to be confused with variables defined with :) and types that is stored in a context. We can:

  • Collect new substitutions by unifying two types together (see below)
  • Apply a type to substitute its type variables with the substitutions stored in the context
  • Finalize a type to assert that it doesn’t contain any type variables; ie. the type is fully known

So if we collect a giant list of substitutions from all the expressions in a declaration, apply all of these types, and then finalize them, we’ll know whether our declaration is well-typed or not! In Wipple, if a declaration is not fully typed (ie. there are type variables remaining even after applying), we produce an error. But in dynamically/gradually typed languages, you could replace all remaining type variables with Any instead.

Unification

Unification is basically equality checking with type variables thrown in. Here is the algorithm:

  1. Apply both types.
    • If we find a substitution between two variables, substitute the old variable and apply the new variable; that way, we can have multi-step substitutions.
  2. If either type is a type variable, add a substitution between the variable and the other type.
  3. Otherwise, check if the two types are equal, recursively unifying any sub-terms.

For example, if we want to unify a type Number -> {0} with a type {1} -> Text (where {n} represents a unique type variable):

  1. Apply both types; since we don’t have a substitution for {0} or {1}, there’s nothing to do here.
  2. Are both types function types? Yes; continue.
  3. Unify the input types of both sides:
    1. Found a variable, {1}; add the substitution 1 => Number.
  4. Unify the output types of both sides:
    1. Found a variable, {0}; add the substitution 0 => Text.
  5. Done.

Now we can apply both types, getting Number -> Text in both cases!

What if the types are incompatible? Let’s now try unifying {1} -> Text with Text -> Text:

  1. Apply both types; we have a substitution for {1}, so the left type becomes Number -> Text.
  2. Are both types function types? Yes; continue.
  3. Unify the input types of both sides:
    1. Unify Number and Text; the two types aren’t equal, so stop and produce an error.

One more example — function calling. For function calling, we split the work into two steps:

  1. Determine the type of the function expression, and unify this type with the type {0} -> {1}, where {0} and {1} are fresh type variables.
  2. Determine the type of the input expression, and unify this type with {0}.
  3. The type of the function call expression as a whole is {1}.

For expressions that instantiate a type or refer to a trait or constant, we copy the type from the declaration and replace all type parameters with new type variables. So if make-tuple :: A B => A -> B -> (A ; B), the expression make-tuple will have type {0} -> {1} -> ({0} ; {1}). This does not happen inside the body of make-tuple itself, where A and B are preserved so that make-tuple cannot construct values of type A or B or assume anything else about them.

And finally, expressions that resolved to an error during lowering (eg. undefined variables) are assigned the error type, which unifies with every other type. This is so that error expressions don’t produce even more errors during typechecking, confusing the user.

Bounds checking

After every expression has been assigned a type (which may or may not contain type variables), the typechecker attempts to find implementations for as many trait expressions and bounds as possible. This process is repeated until an implementation has been found for all expressions that need one, no progress is made (resulting in an error), or a predefined limit is reached (64 repetitions).

Remember that before this phase, we instantiate the types of constant and trait expressions to type variables representing concrete types. So during bounds checking, all we need to do is attempt to unify the types of these expressions with every possible implementation, and the first one that unifies is chosen. This is done by cloning the typechecker’s context (ie. set of substitutions) before unifying, and if unification fails, reverting the context to this snapshot. That way, if unification succeeds, the typechecker can incorporate any inferred types into future rounds of bounds checking.

In addition to unifying the types of the implementations, any bounds attached to the implementation’s signature are checked too. This uses the same logic as trait expressions — internally, a trait expression T : A B C => trait (A ; B ; C) is actually represented as a constant T :: A B C where (T A B C) => (A ; B ; C).

If there aren’t any implementations that satisfy the current type of the expression, the expression is left as-is and will be checked again in the next pass, when hopefully more types have been inferred. However, if the type unifies but the bounds don’t, an error is raised immediately.

Bounds may refer to themselves recursively, so to accommodate this, the typechecker maintains a stack of bounds it has already checked; if the stack already contains the current bound, that bound is assumed to be satisfied. This allows things like A where (Describe A) => instance (Describe (Maybe A)) where A is Maybe _.

Finalization

Finally, the typechecker does one last pass over all expressions to ensure they don’t contain any unresolved type variables. If one does, the could not determine what kind of value this code produces error is produced.

Other things the typechecker does

  • Exhaustiveness checking: The typechecker also performs exhaustiveness checking for variable assignments, function parameters and when expressions. The algorithm was adapted from this paper and this example to support tuple, destructuring and literal patterns in addition to enumeration (“constructor”) patterns.

  • Instance collision checking: Whenever a new instance is processed, the typechecker loops over all previous instances to see if they overlap. During this check, type parameters are treated like type variables and unify with everything (so that A B => instance (T A B) and A B => instance (T B A) overlap). No instance may unify with any other instance, even if the bounds are different.

  • Default types: If you assign a default type to a type parameter, it will be used when a better type can’t be inferred. During bounds checking, if no progress is made, the typechecker will traverse the syntax tree until it finds an expression with a default type. If one is found, it will be substituted and bounds checking will continue.

  • Inferred parameters: You can prefix a type parameter with infer to change the order of bounds checking. Usually, the type checker determines the types of type parameters from the type of the expression provided at the use site, and then checks to make sure any bounds are satisfied. infer reverses this process — it determines the type of the type parameter marked infer from any bounds, and then checks to make sure this type matches the type at the use site. infer doesn’t change the behavior of valid code, but it can produce better error messages for invalid code.

  • Units for numbers: As a special case, if a number appears in function position with an input of type Number -> _, Wipple will swap the expressions so that the input is called with the number. This allows you to write a unit after a number; for example, if pixels :: Number -> Pixels, then 5 pixels is equivalent to pixels 5 and both expressions have type Pixels.

  • Custom error messages: The typechecker has special knowledge of the Error and Mismatch traits to produce custom error messages. See Custom error messages for more information.

IR generation

The driver