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 ofTrue
andFalse
.None
is the “unit type”, and is returned by functions likeshow
that do something but produce no meaningful value.{A}
is the type of a block evaluating to a value of typeA
. For example,{1 + 1}
has type{Number}
.A -> B
is the type of a function accepting a single input of typeA
and producing a value of typeB
. 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 whatValue
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 tof 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
theengine
. Then,refill
thecoolant
. Finally,rotate
thetires
. Perform this maintenance oncar
, resulting inupgraded-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 ado
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 aControl-Flow
from aState
, we just want them to provide a state and letrepeat
figure out what to do with it. Traits in Wipple let you make split the implementation ofrepeat
apart so it works on different types. Within the implementation ofrepeat
, we’ll call the function defined byRepeat-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 basicallyMaybe
, just with a more specific meaning.Maybe
is kind of likeBoolean
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
:
- Call
Repeat-Predicate
on the current state. - If the predicate returns
Continue next
(wherenext
is the function, ie.next :: Body -> State
), evaluate the body and callnext
with the result to get the new state. - 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 forTimes
.
Show answer
Body
andResult
are bothNone
, 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:
Element | Example | Screenshot |
---|---|---|
Color | color [Color "#3b82f6"] | |
Dropdown | show [Dropdown ("A" , "B" , "C") "A"] |
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:
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"))
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 usesSource
to addrepeat
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
- annotate-expression →
(
expression::
type)
Annotate an expression with a type.
- apply-expression →
(
expression.
expression)
Function application using the
.
operator. - as-expression →
(
expressionas
type)
Convert a value of one type to a value of a different type.
- assignment →
(
pattern:
expression)
Assign a value to a pattern.
- attribute → (attribute |
(
(
name)
:
attribute-value)
)An attribute.
- attribute-value → (name | number | text)
An attribute value.
- binary-operator-expression → (
(
expressionto
expression)
|(
expressionby
expression)
|(
expression^
expression)
|(
expression*
expression)
|(
expression/
expression)
|(
expression%
expression)
|(
expression+
expression)
|(
expression-
expression)
|(
expression<
expression)
|(
expression<=
expression)
|(
expression>
expression)
|(
expression>=
expression)
|(
expression=
expression)
|(
expression/=
expression)
|(
expressionand
expression)
|(
expressionor
expression)
)An expression involving a binary operator.
- block-expression →
{
statement ...}
A block expression.
- block-type → (
{
type ...}
)A type whose value is computed from a block expression.
- call-expression →
(
expression ...)
Call a function with at least one input.
- collection-expression →
(
expression,
...)
A collection.
- constant-declaration →
(
(
name)
::
((
type-function=>
type)
| type))
A constant declaration.
- declared-type → (name |
(
name type ...)
)A declared type, optionally with parameters.
- destructure-pattern →
{
(
(
name)
:
pattern)
...}
A pattern that matches the fields of a structure.
- do-expression →
(
do
expression)
Call an intrinsic function provided by the runtime.
- equal-type →
(
type=
type)
Use two types in the place of one.
- expression → (annotate-expression | name-expression | number-expression | text-expression | apply-expression | binary-operator-expression | as-expression | is-expression | when-expression | intrinsic-expression | tuple-expression | collection-expression | structure-expression | block-expression | function-expression | do-expression | call-expression)
An expression.
- function-expression →
(
((
pattern ...)
| function-inputs)->
expression)
A function expression.
- function-type →
(
((
type ...)
| function-inputs)->
type)
A function type.
- instance →
(
name type ...)
An instance.
- instance-declaration → (
(
((
type-function=>
(
default
instance
instance)
)
|(
default
instance
instance)
):
expression)
| ((
type-function=>
(
default
instance
instance)
)
|(
default
instance
instance)
))A default instance declaration.
- instance-declaration → (
(
((
type-function=>
(
instance
instance)
)
|(
instance
instance)
):
expression)
| ((
type-function=>
(
instance
instance)
)
|(
instance
instance)
))An instance declaration.
- intrinsic-expression →
(
intrinsic
text expression ...)
Call an intrinsic function provided by the runtime.
- intrinsic-type →
intrinsic
An intrinsic type provided by the runtime.
- is-expression →
(
expressionis
pattern)
Check if a value matches a pattern.
- message-type → (text |
(
text type ...)
)A type-level piece of text used to generate compiler errors.
- mutate-pattern →
(
name!
)
A pattern that changes the value of an existing variable.
- name-expression → name
A name.
- number-expression → number
A number.
- number-pattern → number
A pattern that matches a number.
- or-pattern →
(
patternor
pattern)
A pattern that matches either one of its subpatterns.
- pattern → (wildcard-pattern | number-pattern | text-pattern | destructure-pattern | tuple-pattern | or-pattern | mutate-pattern | variant-pattern | annotate-pattern)
A pattern.
- placeholder-type → _
An inferred type.
- statement → (syntax-declaration | type-declaration | trait-declaration | instance-declaration | instance-declaration | constant-declaration | assignment | statement)
A statement.
- structure-expression →
{
(
(
name)
:
expression)
...}
A structure.
- syntax-declaration →
(
intrinsic
text)
A syntax declaration.
- text-expression → text
A piece of text.
- text-pattern → text
A pattern that matches a piece of text.
- top-level → (
{
}
|{
statement ...}
)A file or code box.
- trait-declaration →
(
(
name)
:
((
trait
)
|(
trait
type)
|(
type-function=>
((
trait
)
|(
trait
type)
))
))
A trait declaration.
- tuple-expression →
(
expression;
...)
A tuple.
- tuple-pattern →
(
pattern;
...)
A pattern that matches a tuple.
- tuple-type →
(
type;
...)
A tuple type.
- type → (placeholder-type | function-type | tuple-type | block-type | intrinsic-type | message-type | equal-type | declared-type)
A type.
- type-declaration →
(
(
name)
:
((
type
)
|(
type
type-representation)
|(
type-function=>
(
type
)
)
|(
type-function=>
(
type
type-representation)
)
))
A type declaration.
- type-function → (
(
type-parameter ...)
|(
((
_)
|(
type-parameter ...)
)where
(
instance ...)
)
)Provides generic type parameters and bounds to a declaration.
- type-parameter → (name |
(
infer
name)
|(
((
name)
|(
infer
name)
):
type)
)A type parameter.
- type-representation → (
{
((
(
name)
::
type)
|(
name type ...)
) ...}
| type)A set of fields or variants in a type.
- variant-pattern → (name |
(
name pattern ...)
)A pattern that matches a variant or binds to a variable.
- when-arm →
(
pattern->
expression)
An arm in a
when
expression. - when-expression →
(
when
expression{
when-arm ...}
)
Match a value against a set of patterns.
- wildcard-pattern → _
A pattern that matches any value.
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:
Name | Type | Value | Used By |
---|---|---|---|
add | trait | Add | The + operator |
and | trait | And | The and operator |
as | trait | As | The as operator |
boolean | type | Boolean | The runtime |
build-collection | constant | Build-Collection | The , operator |
by | trait | By | The by operator |
divide | trait | Divide | The / operator |
equal | trait | Equal | The = operator |
error | trait | Error | Custom error messages |
false | constant | False | The runtime |
greater-than-or-equal | trait | Greater-Than-Or-Equal | The >= operator |
greater-than | trait | Greater-Than | The > operator |
hasher | type | Hasher | The runtime |
initial-collection | constant | initial-collection | The , operator |
is-equal-to | constant | Is-Equal-To | The runtime |
is-greater-than | constant | Is-Greater-Than | The runtime |
is-less-than | constant | Is-Less-Than | The runtime |
less-than-or-equal | trait | Less-Than-Or-Equal | The <= operator |
less-than | trait | Less-Than | The < operator |
list | type | List | The runtime |
maybe | type | Maybe | The runtime |
multiply | trait | Multiply | The * operator |
none | constant | None | The runtime |
not-equal | trait | Not-Equal | The /= operator |
number | type | Number | Type of number literals |
or | trait | Or | The or operator |
ordering | type | Ordering | The runtime |
power | trait | Power | The ^ operator |
remainder | trait | Remainder | The % operator |
show | constant | Describe | _ in text literals |
some | constant | Some | The runtime |
subtract | trait | Subtract | The - operator |
task-group | type | Task-Group | The runtime |
text | type | Text | Type of text literals |
to | trait | To | The to operator |
true | constant | True | The 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
- Input: Wipple reads your code and stores it in a string.
- 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.
- 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 inx :: A -> B
, but it represents a function expression inx : A -> B
. Similarly,_
is a valid type or pattern, but not a valid expression.
- For example, the
- 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.
- Typechecking: Wipple assigns every a type to every expression in the program and resolves instances for traits.
- 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.
- Intermediate representation (IR) generation: The tree of expressions is converted into a list of instructions that operate on a stack of memory.
- Library generation: The IR for each item in the program is stored in a library file.
- 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 isfo
.- Scan
o
;o
is a character and we have a name, so the name isfoo
.- 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.
-
The source code string,
s
, is passed to thetokenize
function. -
Wipple calls
logos::Lexer::new(s).spanned()
, which returns anIterator
that produces aResult<RawToken, ()>
and aRange<usize>
. TheResult
is eitherOk(RawToken)
(a valid token) orErr(())
(an invalid token — the default error type in Logos is()
; Wipple currently does not have custom error handling for invalid tokens). TheRange
is called a “span”, aka. the start and end positions in the source code where the token is located. -
Wipple
map
s over each of these results, converting theRawToken
s intoToken
s, which are categorized so that operations on similar kinds of tokens (eg. operators or keywords) can be shared. AnyErr(())
values are converted toDiagnostic::InvalidToken
. Both theOk
andErr
variants are put in aWithInfo
structure containing aLocation
value that wraps the span. -
Wipple reports diagnostics for the
Err
values. -
Wipple calls
to_logical_lines
on theOk
values, which replaces multiple line breaks with single line breaks, and removes line breaks between lines involving operators.For example:
Code Equivalent to Behavior a b
a b
Single line a
b
a
b
Line break a
b
a
b
Multiple line breaks are reduced
into a single line breaka
+ b
a + b
Operator joins lines -
Wipple takes the processed list of tokens returned by
to_logical_lines
and passes it toTokenTree::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, aTokenTree::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 callsTokenTree::from_top_level
and extracts the first statement in the output. This will eventually be useful for tests, since you can pass a string like1 + 2
and get back the operator expression directly instead of it being wrapped in a block. -
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. hasAssociativity::Left
), so3 - 2 - 1
is equivalent to(3 - 2) - 1
(0
) and not3 - (2 - 1)
(2
).->
, on the other hand, is right-associative, soNumber -> Number -> Number
is equivalent toNumber -> (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:
- Type inference: Assigning every expression a type
- Unification: Checking that every expression’s type is valid in the surrounding context
- Bounds checking: Finding implementations for constant and trait expressions
- 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:
- 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.
- If either type is a type variable, add a substitution between the variable and the other type.
- 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):
- Apply both types; since we don’t have a substitution for
{0}
or{1}
, there’s nothing to do here. - Are both types function types? Yes; continue.
- Unify the input types of both sides:
- Found a variable,
{1}
; add the substitution1 => Number
.
- Found a variable,
- Unify the output types of both sides:
- Found a variable,
{0}
; add the substitution0 => Text
.
- Found a variable,
- 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
:
- Apply both types; we have a substitution for
{1}
, so the left type becomesNumber -> Text
. - Are both types function types? Yes; continue.
- Unify the input types of both sides:
- Unify
Number
andText
; the two types aren’t equal, so stop and produce an error.
- Unify
One more example — function calling. For function calling, we split the work into two steps:
- Determine the type of the function expression, and unify this type with the type
{0} -> {1}
, where{0}
and{1}
are fresh type variables. - Determine the type of the input expression, and unify this type with
{0}
. - 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)
andA 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 markedinfer
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, ifpixels :: Number -> Pixels
, then5 pixels
is equivalent topixels 5
and both expressions have typePixels
. -
Custom error messages: The typechecker has special knowledge of the
Error
andMismatch
traits to produce custom error messages. See Custom error messages for more information.