Welcome to Wipple!

Wipple is a programming language created by Wilson Gramer that’s natural to read, write and learn.

You can use this documentation to learn how to write Wipple code, how to express concepts from other programming languages in Wipple, and how to manage your own Wipple projects.

How to use this guide

Click the menu icon in the top left corner to see the table of contents. From there, you can jump to any page in the guide. If you want to read the guide in order, click the arrows at the bottom of each page.

The Wipple Playground

The Wipple Playground is a place to experiment with Wipple code in a Jupyter Notebook-like environment. The playground includes a beginner-friendly guide to learning Wipple as well!

Quick start for JavaScript developers

Welcome to Wipple! This guide goes over some basic JavaScript concepts and their equivalent in Wipple. When you finish this guide, you’ll have a foundational understanding of Wipple code that you can use to experiment on your own.

Hello, world

Wipple’s equivalent of console.log is show:

show "Hello, world!"

Notice that there’s no semicolons in Wipple code — just put each statement on its own line.

Comments, numbers and strings

You can write a comment using --. Wipple only has line comments:

-- This is a comment
this is executed -- this is not

Numbers are represented in base 10 instead of floating point, but they are written the same way:

42
3.14
-1

Strings are called “text” in Wipple, and must use double quotes:

"Hello, world!"
"line 1\nline 2"

You can use format to do string interpolation:

format "Hello, _!" "world" -- Hello, world!

Variables

In Wipple, you can declare variables using the : operator:

answer : 42
name : "Wipple"

Wipple uses static single assignment, which means that you can’t change the value of an existing variable after you create it. However, you can declare the same variable twice — the new variable shadows the old one:

x : 42
x : x + 1
show x -- 43

if statement

Wipple doesn’t have an if statement like in JavaScript. Instead, if works more like the ternary operator, and can be used anywhere an expression is needed. By convention, boolean variables end in a question mark.

password : "letmein123"
valid? : password = "password123!" -- use a single '=' to compare values
show (if valid? "Access granted" "Access denied") -- Access denied

Basic types

Wipple is a statically-typed language, which means that your code is verified at compile-time. Luckily, Wipple has type inference, so you usually don’t need to think about types at all! You can use :: to annotate the type of a value.

42 :: Number
"Hello" :: Text

If you mismatch the types, Wipple will emit an error:

42 :: Text -- mismatched types: expected `Text`, but found `Number`

Objects

Wipple calls objects “types”, which you can create using type:

Person : type {
    name :: Text
    age :: Number
}

You can create an instance of this object like so:

bob : Person {
    name : "Bob"
    age : 35
}

And you can use destructuring to get the inner values:

{ name age } : bob

Functions

Wipple’s functions work like JavaScript’s arrow functions. In fact, they both use the arrow notation!

increment : x -> x + 1
show (increment 42) -- 43

One big difference is that Wipple functions may only accept a single parameter. If you want multiple parameters, use multiple functions!

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

If that’s confusing, here’s the equivalent JavaScript code:

const add = (a) => (b) => a + b;
console.log(add(1)(2)); // 3

Methods

Wipple doesn’t allow you to add methods to an object (although you can store functions inside types like any other value). Instead, you can declare functions like this:

greet :: Person -> Text
greet : { name } -> format "Hello, _!" name

greet bob -- Hello, Bob!

Alternatively, you can use the . operator to chain function calls:

bob . greet -- Hello, Bob!

Inheritance

Wipple has neither classes nor inheritance. Instead, you can use traits! Traits are pretty advanced, but here’s a simple example in TypeScript and in Wipple:

TypeScript

// Greet is an interface that can be implemented with a function returning text
interface Greet {
    greet(): string;
}

// For any value implementing Greet, return a greeting
function greet<A extends Greet>(x: A): string {
    return `Hello, ${x.greet()}`;
}

class Person implements Greet {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    // Greet for Person values is defined as the person's name
    greet() {
        return this.name;
    }
}

class Earth implements Greet {
    constructor() {}

    // Greet for Earth values is defined as "world"
    greet() {
        return "world";
    }
}

greet(new Person("Bob")); // Hello, Bob!
greet(new Earth()); // Hello, world!

Wipple

-- Greet is a trait that can be defined with a function returning text
Greet : A => trait (A -> Text)

-- For any value where Greet is defined, return a greeting
greet :: A where (Greet A) => A -> Text
greet : x -> format "Hello, _!" (Greet x)


Person : type {
    name :: Text
}

-- Greet for Person values is defined as the person's name
instance (Greet Person) : { name } -> name


Earth : type

-- Greet for Earth values is defined as "world"
instance (Greet Earth) : just "world"


show (greet (Person { name : "Bob" })) -- Hello, Bob!
show (greet Earth) -- Hello, world!

Quick start for Python developers

Welcome to Wipple! This guide goes over some basic Python concepts and their equivalent in Wipple. When you finish this guide, you’ll have a foundational understanding of Wipple code that you can use to experiment on your own.

Hello, world

Wipple’s equivalent of print is show:

show "Hello, world!"

Comments, numbers and strings

You can write a comment using --, similar to Python’s #.

-- This is a comment
this is executed -- this is not

Numbers are represented in base 10 instead of floating point, but they are written the same way:

42
3.14
-1

Strings are called “text” in Wipple, and must use double quotes:

"Hello, world!"
"line 1\nline 2"

You can use format to do string interpolation, similar to Python’s % operator:

format "Hello, _!" "world" -- Hello, world!

Variables

In Wipple, you can declare variables using the : operator:

answer : 42
name : "Wipple"

Wipple uses lexical scoping instead of function scoping. Basically, that means a variable is only accessible within the block that created it. For example, this wouldn’t work:

if True {
    a : 1
} {
    a : 2
}

show a -- error: cannot find `a`

If you want to assign to a based on a condition, use the if on the right-hand side of the :, like so:

a : if True 1 2

if statement

Wipple’s if x a b is equivalent to Python’s x if a else b:

password : "letmein123"
valid : password = "password123!" -- use a single '=' to compare values
show (if valid "Access granted" "Access denied") -- Access denied

Note that Wipple doesn’t have an if statement. If you want to execute multiple statements inside an if, either refactor the code to use functions or use a block expression:

-- This is OK...
if (1 + 1 = 2) {
    show "Woohoo!"
} {
    show "Oh no"
}

-- But this is better...
result : if (1 + 1 = 2) "Woohoo!" "Oh no"
show result

Basic types

Wipple is a statically-typed language, which means that your code is verified at compile-time. Luckily, Wipple has type inference, so you usually don’t need to think about types at all! You can use :: to annotate the type of a value.

42 :: Number
"Hello" :: Text

If you mismatch the types, Wipple will emit an error:

42 :: Text -- mismatched types: expected `Text`, but found `Number`

Classes and objects

Wipple calls classes “types”, which you can create using type. If you’ve used type annotations in Python, this should look pretty familiar:

Person : type {
    name :: Text
    age :: Number
}

Instead of defining an __init__ function, in Wipple you write the name of the type followed by its fields:

bob : Person {
    name : "Bob"
    age : 35
}

And instead of bob.name and bob.age, you can use destructuring or the of operator:

-- Preferred way
{ name age } : bob

-- Alternative way
name : name of bob
age : age of bob

Functions

Wipple’s functions work like Python’s lambda expressions. The left-hand side of the arrow is the input, and the right-hand side is the output:

increment : x -> x + 1
show (increment 42) -- 43

One big difference is that Wipple functions may only accept a single parameter. If you want multiple parameters, use multiple functions!

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

If that’s confusing, here’s the equivalent Python code:

add = lambda a: lambda b: a + b
print(add(1)(2))  # 3

Methods

Wipple doesn’t allow you to add methods to an object (although you can store functions inside types like any other value). Instead, you can declare functions like this:

greet :: Person -> Text
greet : { name } -> format "Hello, _!" name

greet bob -- Hello, Bob!

Alternatively, you can use the . operator to chain function calls:

bob . greet -- Hello, Bob!

Magic methods

Since Wipple doesn’t have methods, a different approach is needed to implement “magic methods” like __eq__. Wipple solves this problem by using “traits”. Here is a simple example that implements a Greet trait for a Person:

Python

# For any value with the `greet()` method, return a greeting
def greet(x):
    return f"Hello, {x.greet()}"

class Person:
    def __init__(self, name: str):
        self.name = name

    # Greet for Person values is defined as the person's name
    def greet(self):
        return self.name

class Earth:
    # Greet for Earth values is defined as "world"
    def greet(self):
        return "world"

greet(Person("Bob"))  # Hello, Bob!
greet(Earth())  # Hello, world!

Wipple

-- Greet is a trait that can be defined with a function returning text
Greet : A => trait (A -> Text)

-- For any value where Greet is defined, return a greeting
greet :: A where (Greet A) => A -> Text
greet : x -> format "Hello, _!" (Greet x)


Person : type {
    name :: Text
}

-- Greet for Person values is defined as the person's name
instance (Greet Person) : { name } -> name


Earth : type

-- Greet for Earth values is defined as "world"
instance (Greet Earth) : just "world"


show (greet (Person { name : "Bob" })) -- Hello, Bob!
show (greet Earth) -- Hello, world!

One important difference is that the Python code assumes x has a greet() method — if it doesn’t, then the program will crash at runtime. Wipple, on the other hand, verifies that Greet is implemented for x at compile time. It takes some getting used to, but your code will have far fewer bugs!

In Wipple, the = operator is just shorthand for Equal left right, where Equal is a trait representing a function that accepts two values and returns a Boolean. So we can implement the Equal trait for our Person type to get equality checking for free, just like Python’s __eq__ method!

Python

class Person:
    def __init__(self, name: str):
        self.name = name

    def __eq__(self, other: Person):
        return self.name == other.name

Wipple

Person : type {
    name :: Text
}

instance (Equal Person) : p1 -> p2 ->
    name of p1 = name of p2
        and age of p1 = age of p2

Wipple also allows you to derive traits like Equal automatically!

Person : type {
    name :: Text
}

instance Equal Person -- auto-generates an implementation

List comprehensions

Another popular Python feature is list comprehensions:

def birthday(person):
    return Person(person.name, person.age + 1)

older_people = [birthday(person) for person in people]

Wipple supports this syntax using the | operator:

birthday :: Person -> Person
birthday : { name age } -> Person {
    name
    age : age + 1
}

older-people : people | birthday

Handling None

In Python, you represent the absence of a value using None. Wipple also has None, but Wipple helps ensure that you handle None throughout your program. It achieves this using a Maybe type, which holds Some x (for any x) or None. Imagine a Python program that fetches a user from a database:

class Database:
    def fetch_user(self, id):
        table = self.table("users")

        if table.contains(id):
            return table.get(id)
        else:
            return None

if __name__ == "__main__":
    database = ...

    bob = database.fetch_user(42)
    print(bob.name)

Uh oh, this program has a bug — we forgot to handle the case where fetch_user returns None! Now let’s write the same program in Wipple:

fetch-user :: Integer -> Database -> Maybe User
fetch-user : id -> database -> {
    table : database . table "users"

    if (table . contains? id)
        (Some (table . get id))
        None
}


database : ...

bob : fetch-user 42
show bob

Now we get the following error:

error: missing instance
   ┌─ playground:11:1
   │
11 │ show bob
   │ ^^^^ could not find instance `Show (Maybe User)`
   │
   ┌─ https://pkg.wipple.gramer.dev/std/output.wpl:16:17
   │
16 │ show :: A where (Show A) => A -> ()
   │                 -------- required by this bound here

As you can see, Wipple doesn’t know how to show a Maybe User. We have to handle the case where the user is None! In Wipple, we can accomplish handle the None case using when:

when (database . fetch-user 42) {
    Some bob -> show bob
    None -> show "error: no such user"
}

Great — now our code won’t crash and we can choose how to handle the error in an application-specific way!

Exceptions

Wipple doesn’t have exceptions. Instead, functions that can produce errors return the Result type. Similar to Maybe, Result stores either an OK x or an Error e. Let’s refactor our fetch-user example to return a Result instead of a Maybe:

Database-Error : type {
    message :: Text
}

instance (Show Database-Error) : { message } -> format "database error: _" message


fetch-user :: Integer -> Database -> Result User Database-Error
fetch-user : id -> database -> {
    table : database . table "users"

    if (table . contains? id)
        (OK (table . get id))
        (Error (Database-Error { message : format "no user with id _" id }))
}

And we can handle the error using when:

when (database . fetch-user 42) {
    OK bob -> show bob
    Error error -> show error
}

To propagate the error up, you can use end:

bob : when (database . fetch-user 42) {
    OK user -> user
    Error error -> end (Error error)
}

show bob

This pattern can be written more succinctly using try:

bob : try (database . fetch-user 42)
show bob

Quick start for Java developers

Welcome to Wipple! This guide goes over some basic Java concepts and their equivalent in Wipple. When you finish this guide, you’ll have a foundational understanding of Wipple code that you can use to experiment on your own.

Hello, world

Wipple’s equivalent of System.out.println is show:

show "Hello, world!"

Notice that there’s no semicolons in Wipple code — just put each statement on its own line.

Comments, numbers and strings

You can write a comment using --. Wipple only has line comments:

-- This is a comment
this is executed -- this is not

Numbers are represented in base 10 instead of floating point by default, but they are written the same way:

42
3.14
-1

Strings are called “text” in Wipple:

"Hello, world!"
"line 1\nline 2"

You can use format to do string interpolation. All of the _s will be replaced by the string representation of the provided values:

format "Hello, _!" "world" -- Hello, world!

Variables

In Wipple, you can declare variables using the : operator:

answer : 42
name : "Wipple"

Wipple has type inference, so you don’t need to write the type of the variable — Wipple will infer it automatically! If you really want to declare the type, you can do so using the :: operator:

answer : (42 :: Number)
name : ("Wipple" :: Text)

Alternatively, you can write the type on its own line just above the variable declaration:

answer :: Number
answer : 42

name :: Text
name : "Wipple"

Note: This syntax actually transforms the variable into a constant that’s lazily evaluated. It’s primarily intended for use in libraries and not in the bodies of functions, top-level code, or other places where the evaluation order matters. The separate-line syntax is required if you want to use generics or recursion.

All variables in Wipple are the equivalent of final — you can’t change their value after declaring them. If you need access to mutable state, you can do so using mutable. By convention, functions that change a mutable value end in !.

counter : mutable (0 :: Natural)

show (get counter) -- 0

increment! counter
show (get counter) -- 1

if statement

Wipple’s if x a b is equivalent to Java’s x ? a : b:

password : "letmein123"
valid : password = "password123!" -- use a single '=' to compare values
show (if valid "Access granted" "Access denied") -- Access denied

Note that Wipple doesn’t have an if statement. If you want to execute multiple statements inside an if, either refactor the code to use functions or use a block expression:

-- This is OK...
if (1 + 1 = 2) {
    show "Woohoo!"
} {
    show "Oh no"
}

-- But this is better...
result : if (1 + 1 = 2) "Woohoo!" "Oh no"
show result

Basic types

Wipple is a statically-typed language just like Java. However, Wipple has type inference, so you usually don’t need to think about types at all! As mentioned above, you can use :: to annotate the type of a value:

42 :: Number
"Hello" :: Text

If you mismatch the types, Wipple will emit an error:

42 :: Text -- mismatched types: expected `Text`, but found `Number`

Classes and objects

Wipple calls classes “types”, which you can create using type:

Person : type {
    name :: Text
    age :: Number
}

Instead of defining a constructor, in Wipple you instantiate a type by writing the name of the type followed by its fields:

bob : Person {
    name : "Bob"
    age : 35
}

And instead of bob.name and bob.age, you can use destructuring or the of operator:

-- Preferred way
{ name age } : bob

-- Alternative way
name : name of bob
age : age of bob

Functions

Wipple’s functions work like Java’s lambda expressions. In fact, they both use the arrow notation!

increment : x -> x + 1
show (increment 42) -- 43

One big difference is that Wipple functions may only accept a single parameter. If you want multiple parameters, use multiple functions!

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

If that’s confusing, here’s the equivalent Java code:

Function<Double, Function<Double, Double>> add = a -> b -> a + b;
System.out.println(add.apply(1.0).apply(2.0)); // 3.0

Methods

Wipple doesn’t allow you to add methods to an object (although you can store functions inside types like any other value). Instead, you can declare functions like this:

greet :: Person -> Text
greet : { name } -> format "Hello, _!" name

greet bob -- Hello, Bob!

Alternatively, you can use the . operator to chain function calls:

bob . greet -- Hello, Bob!

Interfaces

Wipple has something similar to interfaces called “traits”. For example, here is how you would define a Greet trait and implement it for Person and Earth:

Java

// Greet is an interface that can be implemented with a function returning text
interface Greet {
    String greet();
}

class Greeter {
    // For any value implementing Greet, return a greeting
    static <A extends Greet> String greet(A x) {
        return "Hello, " + x.greet() + "!";
    }
}

class Person implements Greet {
    String name;

    Person(String name) {
        this.name = name;
    }

    // Greet for Person values is defined as the person's name
    public String greet() {
        return this.name;
    }
}

class Earth implements Greet {
    // Greet for Earth values is defined as "world"
    public String greet() {
        return "world";
    }
}

class Main {
    public static void main(String[] args) {
        System.out.println(Greeter.greet(new Person("Bob"))); // Hello, Bob!
        System.out.println(Greeter.greet(new Earth())); // Hello, world!
    }
}

Wipple

-- Greet is a trait that can be defined with a function returning text
Greet : A => trait (A -> Text)

-- For any value where Greet is defined, return a greeting
greet :: A where (Greet A) => A -> Text
greet : x -> format "Hello, _!" (Greet x)


Person : type {
    name :: Text
}

-- Greet for Person values is defined as the person's name
instance (Greet Person) : { name } -> name


Earth : type

-- Greet for Earth values is defined as "world"
instance (Greet Earth) : just "world"


show (greet (Person { name : "Bob" })) -- Hello, Bob!
show (greet Earth) -- Hello, world!

Built-in Java methods like equals and toString are also implemented using traits in Wipple. For example:

Person : type {
    name :: Text
    age :: Number
}

instance (Equal Person) : p1 -> p2 ->
    name of p1 = name of p2
        and age of p1 = age of p2

instance (Show Person) : { name age } ->
    format "_ is _ years old" name age

And now we can show a Person value!

bob : Person {
    name : "Bob"
    age : 30
}

show bob -- Bob is 30 years old

Wipple also allows you to derive traits like Equal automatically!

instance Equal Person -- auto-generates an implementation

Inheritance

Wipple is not an object-oriented language and doesn’t support class inheritance. But you can achieve the same functionality using composition and traits! For example, let’s write some Java code for a GUI application:

class Button extends View {
    String title;

    Button(String title) {
        this.title = title;
    }

    void draw(Window window) {
        window.drawText(this.title);
    }
}

class RedButton extends Button {
    @Override
    void draw(Window window) {
        window.setColor(Color.RED);
        super.draw(window);
    }
}

class BlueButton extends Button {
    @Override
    void draw(Window window) {
        window.setColor(Color.BLUE);
        super.draw(window);
    }
}

Let’s refactor this code in Wipple to use composition instead of inheritance:

Button : type {
    title :: Text
    color :: Color
}

instance (View Button) : window -> { title color } ->
    window
        . set-color color
        . draw-text title


Red-Button : type {
    title :: Text
}

instance (View Red-Button) : window -> { title } -> {
    button : Button {
        title
        color : Red
    }

    button . View window
}


Blue-Button : type {
    title :: Text
}

instance (View Blue-Button) : window -> { title } -> {
    button : Button {
        title
        color : Blue
    }

    button . View window
}

Instead of calling super, you can delegate to the View implementation of Button inside the implementations of Red-Button and Blue-Button.

For this particular example, it makes more sense to create a constructor function instead of a whole new type for each color of button:

Button : type {
    title :: Text
    color :: Color
}

instance (View Button) : window -> { title color } ->
    window
        . set-color color
        . draw-text title


red-button :: Text -> Button
red-button : title -> Button {
    title
    color : Red
}


blue-button :: Text -> Button
blue-button : title -> Button {
    title
    color : Blue
}

Handling null

In Java, you represent the absence of a value using null. Wipple has something similar called None, but Wipple helps ensure that you handle None throughout your program. It achieves this using a Maybe type, which holds Some x (for any x) or None. Imagine a Java program that fetches a user from a database:

class Database {
    public User fetchUser(int id) {
        Table table = this.table("users");

        if (table.contains(id)) {
            return table.get(id);
        } else {
            return null;
        }
    }
}

class Main {
    public static void main(String[] args) {
        Database database = ...;
        User bob = database.fetchUser(42);
        System.out.println(bob.name);
    }
}

Uh oh, this program has a bug — we forgot to handle the case where fetchUser returns null! Now let’s write the same program in Wipple:

fetch-user :: Integer -> Database -> Maybe User
fetch-user : id -> database -> {
    table : database . table "users"

    if (table . contains? id)
        (Some (table . get id))
        None
}


database : ...

bob : database . fetch-user 42
show bob

Now we get the following error:

error: missing instance
   ┌─ playground:11:1
   │
11 │ show bob
   │ ^^^^ could not find instance `Show (Maybe User)`
   │
   ┌─ https://pkg.wipple.gramer.dev/std/output.wpl:16:17
   │
16 │ show :: A where (Show A) => A -> ()
   │                 -------- required by this bound here

As you can see, Wipple doesn’t know how to show a Maybe User. We have to handle the case where the user is None! In Wipple, we can accomplish handle the None case using when:

when (database . fetch-user 42) {
    Some bob -> show bob
    None -> show "error: no such user"
}

Great — now our code won’t crash and we can choose how to handle the error in an application-specific way!

Exceptions

Wipple doesn’t have exceptions. Instead, functions that can produce errors return the Result type. Similar to Maybe, Result stores either an OK x or an Error e. Let’s refactor our fetch-user example to return a Result instead of a Maybe:

Database-Error : type {
    message :: Text
}

instance (Show Database-Error) : { message } -> format "database error: _" message


fetch-user :: Integer -> Database -> Result User Database-Error
fetch-user : id -> database -> {
    table : database . table "users"

    if (table . contains? id)
        (OK (table . get id))
        (Error (Database-Error { message : format "no user with id _" id }))
}

And we can handle the error using when:

when (database . fetch-user 42) {
    OK bob -> show bob
    Error error -> show error
}

To propagate the error up, you can use end:

bob : when (database . fetch-user 42) {
    OK user -> user
    Error error -> end (Error error)
}

show bob

This pattern can be written more succinctly using try:

bob : try (database . fetch-user 42)
show bob

Quick start for Rust developers

Welcome to Wipple! This guide goes over some basic Rust concepts and their equivalent in Wipple. When you finish this guide, you’ll have a foundational understanding of Wipple code that you can use to experiment on your own.

Hello, world

Wipple’s equivalent of println! is show:

show "Hello, world!"

Notice that there’s no semicolons in Wipple code — just put each statement on its own line.

Comments, numbers and strings

You can write a comment using --. Wipple only has line comments:

-- This is a comment
this is executed -- this is not

Numbers are represented in base 10 instead of floating point by default, but they are written the same way:

42
3.14
-1

Strings are called “text” in Wipple:

"Hello, world!"
"line 1\nline 2"

Just like in Rust, you can use format to do string interpolation. _ is used as the delimiter instead of {}:

format "Hello, _!" "world" -- Hello, world!

Variables

In Wipple, you can declare variables using the : operator:

answer : 42
name : "Wipple"

Wipple has type inference, so you don’t need to write the type of the variable — Wipple will infer it automatically! If you really want to declare the type, you can do so using the :: operator:

answer : (42 :: Number)
name : ("Wipple" :: Text)

Alternatively, you can write the type on its own line just above the variable declaration:

answer :: Number
answer : 42

name :: Text
name : "Wipple"

Note: This syntax actually transforms the variable into a constant that’s lazily evaluated. It’s primarily intended for use in libraries and not in the bodies of functions, top-level code, or other places where the evaluation order matters. The separate-line syntax is required if you want to use generics or recursion.

Wipple doesn’t allow you to change variable’s value after declaring it. If you need access to mutable state, you can do so using mutable (which works like an Rc<RefCell<T>>). By convention, functions that change a mutable value end in !.

counter : mutable (0 :: Natural)

show (get counter) -- 0

increment! counter
show (get counter) -- 1

if statement

Wipple’s if x a b is equivalent to Rust’s if x { a } else { b }:

password : "letmein123"
valid : password = "password123!" -- use a single '=' to compare values
show (if valid "Access granted" "Access denied") -- Access denied

If you want to execute multiple statements inside an if, you can use a block expression:

-- This is OK...
if (1 + 1 = 2) {
    show "Woohoo!"
} {
    show "Oh no"
}

-- But this is better...
result : if (1 + 1 = 2) "Woohoo!" "Oh no"
show result

Basic types

Wipple has a very similar type system to Rust. You can use :: to annotate the type of a value:

42 :: Number
"Hello" :: Text

If you mismatch the types, Wipple will emit an error:

42 :: Text -- mismatched types: expected `Text`, but found `Number`

Structs

Wipple calls structs “types”, which you can create using type:

Person : type {
    name :: Text
    age :: Number
}

Just like in Rust, you instantiate a type by writing the name of the type followed by its fields:

bob : Person {
    name : "Bob"
    age : 35
}

And instead of bob.name and bob.age, you can use destructuring or the of operator:

-- Preferred way
{ name age } : bob

-- Alternative way
name : name of bob
age : age of bob

Functions

Wipple’s functions work like Rust’s closures. a -> b is equivalent to Rust’s |a| b:

increment : x -> x + 1
show (increment 42) -- 43

One big difference is that Wipple functions may only accept a single parameter. If you want multiple parameters, use multiple functions!

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

If that’s confusing, here’s the equivalent Rust code:

let add = |a| { move |b| { a + b } };
println!("{}", add(1.0)(2.0)); // 3

Methods

Wipple doesn’t allow you to impl methods for a type (although you can store functions inside types like any other value). Instead, you can declare functions like this:

greet :: Person -> Text
greet : { name } -> format "Hello, _!" name

greet bob -- Hello, Bob!

Alternatively, you can use the . operator to chain function calls:

bob . greet -- Hello, Bob!

Traits

Wipple’s traits are similar to Rust’s traits, but they are even more powerful. Instead of being limited to being implemented for a single Self type, Wipple traits can represent a relationship between multiple types at once. Let’s start with a simple example though — here is how you would define a Greet trait and implement it for Person and Earth:

Rust

// Greet is a trait that can be implemented with a function returning a string
trait Greet {
    fn greet(&self) -> &str;
}

// For any value implementing Greet, return a greeting
fn greet<A>(x: A) -> String
where
    A: Greet,
{
    format!("Hello, {}!", x.greet())
}

struct Person {
    name: String,
}

impl Person {
    fn new(name: impl ToString) -> Self {
        Person {
            name: name.to_string(),
        }
    }
}

// Greet for Person values is defined as the person's name
impl Greet for Person {
    fn greet(&self) -> &str {
        &self.name
    }
}

struct Earth;

    // Greet for Earth values is defined as "world"
impl Greet for Earth {
    fn greet(&self) -> &str {
        "world"
    }
}

fn main() {
    println!("{}", greet(Person::new("Bob"))); // Hello, Bob!
    println!("{}", greet(Earth)); // Hello, world!
}

Wipple

-- Greet is a trait that can be defined with a function returning text
Greet : A => trait (A -> Text)

-- For any value where Greet is defined, return a greeting
greet :: A where (Greet A) => A -> Text
greet : x -> format "Hello, _!" (Greet x)


Person : type {
    name :: Text
}

-- Greet for Person values is defined as the person's name
instance (Greet Person) : { name } -> name


Earth : type

-- Greet for Earth values is defined as "world"
instance (Greet Earth) : just "world"


show (greet (Person { name : "Bob" })) -- Hello, Bob!
show (greet Earth) -- Hello, world!

Wipple also allows you to derive implementations of traits like Equal — just omit the implementation and Wipple will generate it for you!

instance Equal Person -- auto-generates an implementation

Option<T> and Result<T, E>

Wipple’s equivalent of Option<T> is Maybe A, and Result<T, E> is Result Success Failure. Otherwise, they work in the same way!

  • Use when instead of match to do pattern matching.
  • Use end to exit the current block with a value.
  • Use try to exit the current block if the provided value is None, Error, or another type that can be converted into a Result.

Here’s an example:

Database-Error : type {
    message :: Text
}

instance (Show Database-Error) : { message } -> format "database error: _" message


fetch-user :: Integer -> Database -> Result User Database-Error
fetch-user : id -> database -> {
    table : database . table "users"

    if (table . contains? id)
        (OK (table . get id))
        (Error (Database-Error { message : format "no user with id _" id }))
}


bob : try (database . fetch-user 42)
show bob

Generics

Wipple has a powerful type system that lets you express relationships between values. Often, you’ll want to implement a function or instance that works for any input type — for example, implementing Equal for Maybe Value where Equal Value is implemented.

Wipple lets you express generics using type functions, which use the => operator. The left-hand side of the type function introduces type parameters into scope, and the right-hand side is a type depending on these parameters. For example, we can define Maybe as follows:

Maybe : Value => type {
    Some Value
    None
}

Type functions can also be used with traits, constants and instances:

Show : A => trait (A -> Text)

unwrap :: A => Maybe A -> A
unwrap : ...

A where (Show A) => instance (Show (Maybe A)) : ...

That where clause in the above example allows you to introduce bounds on the type parameters — that is, the type, trait, constant or instance may only be used if there are instances matching the trait with the provided parameters.

You can provide as many parameters and bounds as you want:

A B C where (T A) (U B) (V C) => ...

Unused type parameters

In a type declaration, you don’t need to actually use the parameters anywhere in the type. This is useful for creating “type states” that represent data at the type level:

Idle : type
Hovering : type

Drone : State => type

take-off :: Drone Idle -> Drone Hovering
take-off : just Drone

land :: Drone Hovering -> Drone Idle
land : just Drone


my-drone :: Drone Idle
my-drone : Drone

my-drone . take-off . land -- works!
my-drone . land -- cannot land because drone is already idle

Implicit type parameters

In constants, instance definitions, and where bounds, you can replace a type parameter with a type placeholder (_). There, _ indicates an implicit type parameter. For example, Element is only used once in the signature of count, so you can replace it with a type placeholder:

-- explicit type parameter
count :: Collection Element where (Iterate Collection Element) => Collection -> Natural

-- implicit type parameter
count :: Collection where (Iterate Collection _) => Collection -> Natural

Note that you cannot use implicit type parameters in type or trait definitions, because that would prevent you from calling their type functions.

Mutability

Wipple doesn’t allow you to reassign the value of a variable once you’ve declared it. Instead, you can shadow an existing variable by assigning to the same name:

x : 1 -- first assignment
x : "hello" -- second assignment

Notice how the types don’t need to be the same — this is because the two xs are distinct values.

Importantly, any code referring to the original x will continue to do so:

x : 1
show-x : () -> show x
x : 2
show-x () -- displays 1, not 2

However, there are circumstances where you actually need to change a variable’s value and have that change be shared across the program. To accommodate this, Wipple provides a Mutable type!

You can create a new Mutable value by using the mutable function:

-- mutable :: A => A -> Mutable A
x : mutable 1

To retrieve the value inside, use get:

-- get :: A => Mutable A -> A
show-x : () -> show (get x)

And use set! to change it:

-- set :: A => A -> Mutable A -> ()
x . set! 2

Now when you call show-x, you’ll get 2 instead of 1!

By convention, any function that changes the value of a Mutable input ends with !. There is no need to append ! to functions that only mutate internal state.

There are many useful functions for changing mutable values; here are just a few:

Function Type Description
swap! A => Mutable A -> Mutable A -> () Swaps the values of its inputs
add! Left Right where (Add Left Right Left) => Right -> Mutable Left -> () Adds a value to its input
increment! A where (Add A Number A) => A -> () Increments its input by 1

Specialization

Wipple doesn’t support overloading like many other languages. Instead, you can use a trait to describe how functions operating on the trait should behave for a certain type. For example, you can implement the count function for every iterable type:

count :: Collection where (Iterate Collection _) => Collection -> Natural
count : ... -- iterate over each item and increment a counter

And if you implement Iterate on, say, a List, you get count for free!

There’s one problem with this approach, and that’s performance. While count’s current implementation (iterating over each item and incrementing a counter) is indeed the most generic and flexible way to do it, it’s very inefficient for types that already know the number of elements they contain. Let’s say we have a type called Repeat that produces a value count times:

Repeat : A => type {
    value :: A
    count :: Natural
}

A => instance (Iterate (Repeat A) A) : ...

Because Repeat implements Iterate, count will work just fine. But we can eliminate a bunch of unnecessary work because Repeat already knows its count! To accomplish this, we can use specialization.

How does specialization work?

Specialization allows you to declare a new constant for a more specific type and tell Wipple to use it instead of the generic implementation. This is done using the [specialize] attribute:

[specialize]
count :: Repeat _ -> Natural
count : { count } -> count

Now Wipple will use the specialized implementation of count whenever it’s called with a Repeat value.

Rules and conventions

Because specialization is intended solely for performance, it’s supposed to be invisible to the end user of your library. Therefore, there are some restrictions on specialized constants:

  • They must have the same name as the generic constant.
  • They must have the same type as the generic constant and satisfy all of its bounds.
  • You can’t specialize a constant that is a specialization of another constant.

In addition, there are some conventions that Wipple can’t check, but you should follow:

  • The specialized constant should have the same behavior as the generic constant; it should be a “drop-in replacement”.
  • You should only specialize a constant you created, or your specialization should operate at least one type you created. This prevents conflicts between libraries.

Type-level programming

Wipple’s type system is so powerful, you can write programs that are executed entirely by the type checker! It turns out that values can correspond to types, and functions can correspond to traits. Let’s look at a simple example of “type arithmetic”:

Z : type
S : N => type

Here, we define Z to represent zero and S to represent the “successor” to a number. For example, one is represented by S Z, two is represented by S (S Z), and so on. These types are effectively the same as an enumeration at the value level:

N : type {
    Z
    S N
}

You might notice that at the type level, Wipple doesn’t restrict what type you provide to S — you could provide Text or any other type. At the type level, Wipple is effectively dynamically-typed!

Now that we have our data, we can write a function to add together two numbers. This is defined using a trait:

Add : A B Sum => trait

Notice that we don’t have a value after the trait definition. This prevents the trait from being used in a value position; that is, it can only be referenced as a bound in a where clause. Similarly, we omit the value when we declare an instance:

A => instance (Add A Z A)

This is our base case — any “value” A plus zero is equal to A. Our second instance is a bit more complicated:

A B Sum where (Add A B Sum) => instance (Add A (S B) (S Sum))

This definition states that if you can add A and B together to get a Sum, then A plus the successor of B is equal to the successor of Sum — a + (b + 1) = (a + b) + 1. The recursion terminates when B is zero and the base case is reached. Essentially, you do the addition by repeatedly adding one to the input!

Now let’s use our Add “function” — remember that we’re working at the type level, so to perform a computation, we need to use a type annotation:

result :: Sum where (Add (S (S Z)) (S (S (S Z))) Sum) => Sum
result : ...

And now we can print result’s type by raising a type error:

_ : _ -> result

The error tells us that Sum is S (S (S (S (S Z)))), proving that 2 + 3 = 5!

error: could not determine the type of this expression
   ┌─ playground:11:5
   │
11 │ _ : _ -> result
   │     ^^^^^^^^^^^
   │     │
   │     try annotating the type with `::`
   │     this has type `a -> S (S (S (S (S Z))))` for some unknown type `a`

Let’s try another example — determining if a number is odd or even! We’ll start by defining our data:

Z : type
S : N => type

Odd : type
Even : type

And now we’ll make our function, which accepts a number N and returns a kind Kind (Odd or Even):

Odd-Even : N Kind => trait

Our base case is that Zero is Even:

instance (Odd-Even Z Even)

And now we implement the recursion! If n + 1 is odd, then n is even, and vice versa:

N where (Odd-Even N Even) => instance (Odd-Even (S N) Odd)
N where (Odd-Even N Odd) => instance (Odd-Even (S N) Even)

Let’s try it out!

zero :: A where (Odd-Even Z A) => A
zero : ...
_ : _ -> zero

one :: A where (Odd-Even (S Z) A) => A
one : ...
_ : _ -> one

two :: A where (Odd-Even (S (S Z)) A) => A
two : ...
_ : _ -> two

Sure enough, zero is even, one is odd, and two is even:

error: could not determine the type of this expression
   ┌─ playground:14:5
   │
14 │ _ : _ -> zero
   │     ^^^^^^^^^
   │     │
   │     try annotating the type with `::`
   │     this has type `a -> Even` for some unknown type `a`

error: could not determine the type of this expression
   ┌─ playground:18:5
   │
18 │ _ : _ -> one
   │     ^^^^^^^^
   │     │
   │     try annotating the type with `::`
   │     this has type `a -> Odd` for some unknown type `a`

error: could not determine the type of this expression
   ┌─ playground:22:5
   │
22 │ _ : _ -> two
   │     ^^^^^^^^
   │     │
   │     try annotating the type with `::`
   │     this has type `a -> Even` for some unknown type `a`

A practical example: first and rest

At the value level, you can use the first and rest functions to obtain the first item in a list or the items after it. These functions return Maybe values, since the input list could be empty. But at the type level, we can ensure that the list is non-empty! To start, let’s create a new list type along with a nil constructor:

Z : type
S : N => type

List : A Count => type -- no need to store an `A` because we don't care about
                       -- the list's value

nil :: A => List A Z
nil : ...

And now we can implement first like so:

first :: A Count => List A (S Count) -> A
first : ...

Notice that first requires that the list’s Count be the successor of a number; this is effectively saying that Count must be greater than zero. rest is similar:

rest :: A Count => List A (S Count) -> List A Count
rest : ...

Here, we make sure to reduce the Count by one in the returned list.

And lastly, let’s implement cons:

cons :: A Count => A -> List A Count -> List A (S Count)
cons : ...

Now let’s test our program!

my-list : cons 1 (cons 2 (cons 3 nil))
first my-list -- works!
first (rest my-list) -- works!
first (rest (rest (rest my-list))) -- fails!

The last line fails with a type error:

error: mismatched types
   ┌─ playground:22:7
   │
22 │ first (rest (rest (rest my-list))) -- fails!
   │       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `List Number (S a)` for some unknown type `a`, but found `List Number Z`

Awesome, now we have compile-time bounds checking!

Syntax

Wipple has a minimal syntax with just a few constructs:

  • Comments are ignored.
  • Blocks represent a sequence of lists.
  • Lists represent a sequence of expressions.
  • Templates transform a list into an expression.
  • Operators and attributes change how lists are parsed.

All of Wipple’s “keywords” are implemented as templates, operators and attributes that produce expressions or special forms.

Comments

A comment begins with -- and continues until the end of the line. The contents of a comment are ignored. For example, writing x -- y is equivalent to writing x.

Blocks

A block begins with { and ends with }. The top level of a file is also implicitly a block.

Each line in a block is parsed into a list, so { (a b c) } is equivalent to { a b c }. If a line is indented using a tab character, then it becomes part of the previous line. For example:

-- This:
a b c
  d e f

-- is equivalent to:
a b c d e f

Lists and templates

A list begins with ( and ends with ). Each statement in a block is also implicitly a list.

If the list contains no operators, then it is evaluated in one of three ways:

  • If the list is empty, then it evaluates to itself.
  • If the list contains one expression, then the list is replaced by the expression. For example, (foo) is the same as foo and ((foo) (bar) (baz)) is the same as (foo bar baz).
  • If the list contains two or more expressions, then the first expression is called with the remaining expressions.

If the first item in a list is a template, then the template is expanded with the remaining items in the list at compile time. Otherwise, the list is evaluated at runtime. For example, consider a template duplicate that accepts an input x and evaluates to (x x) — writing duplicate a is equivalent to writing a a.

Defining a template

You can define a template using the ~> operator:

swap : a b ~> b a
swap x y -- equivalent to (y x)

Operators

Operators are a type of template that are written between one or more expressions on each side. For example, consider an operator o that is placed between two expressions x and yf and evaluates to y x — writing a o b is equivalent to writing b a.

Every operator has a “precedence”, where higher-precedence operators have priority over lower-precedence ones. For example, consider an operator a that has a higher precedence than an operator b — writing x a y b c is equivalent to writing x a (y b c).

Every precedence defines an “associativity”, indicating which direction the operators of that precedence should be parsed if there are more than one. For example, consider an operator o that is left-associative — writing x o y o b is equivalent to writing (x o y) o b. Operators do not need to have an associativity; in that case, writing more than one operator in the same list is an error.

Defining an operator

You can define an operator using the operator operator:

swap : dot operator (a b ~> b a)
x swap y -- equivalent to (y x)

Attributes

Attributes are an alternative way to use templates. An attribute begins with with [ and ends with ], and applies to the line below it. For example:

-- This:
[a x]
[b y]
z

-- Is equivalent to:
a x (b y z)

Some attributes may also be placed at the beginning of a file using [[ and ]]. For example, the no-std file attribute prevents automatically importing the standard library:

[[no-std]]

show "Hello, world!" -- error: cannot find `show`

There’s no special syntax to define an attribute; all templates may be used as attributes.

Atoms

Atoms allow you to fill a list with information. There are three kinds of atoms:

  • Names: x, foo, favorite-color, set!, +, :
  • Numbers: 42, -5, 3.14
  • Text: "", "Hello, world!", "line 1\nline2"

Variables and patterns

A variable is a way to give a name to a value. In Wipple, you can declare one or more variables using the : operator:

sum : 1 + 1

The left-hand side of the : is a pattern, and the right-hand side is an expression. The expression is evaluated and then matched according to the pattern, assigning to the new variables.

Patterns

A pattern is a way to describe the structure of a value and extract its parts into variables. There are several kinds of patterns:

  • A name pattern (eg. x) matches an entire value and assigns it to a variable name.
  • A variant pattern (eg. Some x, None) matches a variant of an enumeration and its associated values.
  • A destructuring pattern (eg. { x y z }) matches the fields of a structure. Providing just the name of a field (eg. { x }) is equivalent to matching the field as a variable (ie. { x : x }). It is not required to list all fields; missing fields are ignored.
  • A tuple pattern (eg. x , y , z) matches each element of a tuple.
  • A literal pattern (eg. 42, 3.14, "hi") matches a value if it is equal to the literal.
  • An or pattern (eg. x or y) attempts to match the first pattern and then the second pattern.
  • A where pattern (eg. x where y) matches the pattern only if the condition following the where is satisfied.
  • A wildcard pattern matches everything and binds no variables.
  • An instance pattern has to do with traits and are discussed in that section.

Patterns may be composed however you want; for example, you can match a Maybe (Number , Maybe Number) using the pattern Some (1 , Some x).

You can also use patterns when defining functions, eg. a , b , c -> a + b + c accepts a tuple and returns the sum of its elements.

Exhaustiveness and the when expression

Patterns on the left-hand side of a : must be exhaustive, meaning they match every possible value the right-hand side could contain. For example, the Some x in Some x : m where m :: Maybe Number is invalid because m could also be None. To match multiple patterns on a single value, you can use a when expression:

when m {
    Some x -> a
    None -> b
}

when evaluates m and then attempts to match each provided pattern in order. The first pattern that matches the input will have its associated body executed. when also checks for exhaustiveness, but does so by combining the structure of all the patterns provided into a single set of possible matches. In the above example, since a Maybe may only contain Some or None, the when expression is exhaustive. If you want to have a “default” branch that’s executed when none of the other patterns match, just add a wildcard pattern to the end:

show (when m {
    Some 42 -> "matched 'Some' with 42"
    None -> "matched 'None'"
    _ -> "matched something else"
})

Scope

All variables are scoped to the block, function, or when expression in which they are declared. Wipple uses lexical scope, not function scope, so the following code doesn’t work:

if True {
    x : 1
} {
    x : 2
}

show x -- error: cannot find `x`

The correct way is to “lift” the variable assignment to the block level:

x : if True 1 2
show x

The : syntax is equivalent to writing a when expression as follows:

-- This...
x : a
f x

-- is equivalent to...
when a {
    x -> f x
}

Since each new variable assignment effectively introduces its own scope, you can declare two different variables with the same name. The original variable is no longer accessible, but functions that refer to it will continue to do so instead of referring to the new variable:

x : 1
show-x : () -> show x -- refers to the above 'x'
x : 2 -- this creates a new variable named 'x' and does not change the original 'x'
show-x () -- displays "1", not "2"
show x -- displays "2"

A consequence of this is that the new variable does not need to have the same type as the original. If this “shadowing” of variable names is confusing, we can translate the : syntax into the when syntax to make the scoping explicit:

when 1 {
    x -> when (() -> show x) {
        show-x -> when 2 {
            x -> {
                show-x () -- displays "1", not "2"
                show x -- displays "2"
            }
        }
    }
}

Mutability

Wipple encourages structuring your code so that functions produce new values instead of mutating their inputs. But if you need to have mutability, Wipple offers the Mutable type:

x : mutable 1
show-x : () -> show (get x)
x . set! 2
show-x () -- displays "2"

The mutable function creates a new mutable value (of type Mutable Number), the get function retrieves the value, and the set! function mutates the value. By convention, functions that mutate Mutable values end in !.

Types and generics

A type is a way to identify what “kind” of value something is. For example, the expression "hello" has type Text, and 1 + 2 has type Number.

There are five main kinds of types in Wipple:

  • Marker types have a single value and contain no information.
  • Structure types represent a collection of values (“fields”), where each field has a name and stores a single value.
  • Enumeration types represent a fixed set of values (“variants”), where each variant has zero or more associated values.
  • Tuple types represent a fixed-size, heterogeneous collection of values.
  • Function types represent a function that accepts a value of one type and returns a value of another type.

Type annotations

You can use the :: operator to explicitly declare the type of a value. If Wipple determines that your type is incorrect, it will raise an error. For example, to explicitly declare that 42 has type Number:

42 :: Number

Note: If you want to give a variable x a type T, you can’t write x :: T at the statement level, as this defines a constant. To get around this, wrap the type annotation in parentheses: (x :: T).

Catalog of types

Markers

Marker types can be declared using the type template:

Marker : type

To refer to the value the marker represents, just write the name of the marker type. For example, if x : Marker, then x has type Marker.

Structures

Structure types can be declared using the type template followed by a block of type annotations:

Structure : type {
    x :: Number
    y :: Text
}

To create a new structure, write the structure’s name followed by a block of variable assignments:

s : Structure {
    x : 42
    y : "hello"
}

Enumerations

Enumeration types can be declared using the type template followed by a block of variants:

Grade : type {
    A
    B
    C
    D
    F
}

You can also add associated values to each variant:

Either : type {
    Left Number
    Right Text
}

To create a variant of the enumeration, write the enumeration’s name, followed by the variant’s name, followed by any associated values:

g : Grade A
e : Either Left 42

If you want to refer to the variants directly without having to write the enumeration’s name every time, you can use the use template:

use Grade
g : A

use Either
e : Left 42

Tuples

Tuples and tuple types can be declared using the , operator:

(1 , "a" , True) :: (Number , Text , Boolean)

The empty tuple () is also valid. Usually, () is used for a function that accepts and/or returns no meaningful value:

show 42 :: ()

Function types

Functions and function types can be declared using the -> operator:

(x -> x) :: (Number -> Number)

In Wipple, functions may only accept one value. To accept another value, make the function return another function and move your computation into that new function:

f : (x -> y -> x + y) :: (Number -> Number -> Number)
g : (f 1) :: (Number -> Number)
h : (g 2) :: Number

Generics

Wipple supports generics in the form of type functions, which accept one or more types and produce a new type as a result. For example, we can redefine Either from above to be more generic:

Either : A B => type {
    Left A
    Right B
}

To use such a type function, you call it by its name, providing the specific types as input:

Left 42 :: Either Number Text

Here, the annotation is required because Left only refers to A, meaning there’s no way for Wipple to automatically determine B.

Type placeholders

You can use _ to represent a placeholder, the type of which Wipple should determine automatically. For example, we know the type of A in the above example to be Number, so we can make the type annotation more concise using a placeholder:

Left 42 :: Either _ Text

In a type function, you can use _ to create an implicit type parameter:

left :: Left => Either Left _ -> Maybe Left
left : ...

right :: Right => Either _ Right -> Maybe Right
right : ...

Files and constants

It’s encouraged to split your program into files to make it easier to maintain. In Wipple, you can import the contents of another file with the use template. For example, consider a file named x.wpl that imports y.wpl:

-- x.wpl
use "y.wpl"

If you want to import only specific declarations in the file, you can assign the use to a destructuring pattern:

-- x.wpl
{ a b c } : use "y.wpl"

When importing files, the order you import them in doesn’t matter. This means that executable code at the top level is only allowed in the root file.

Note: Wipple imports types, traits, instances and constants from a file.

Constants

Constants are similar to variables, but can be imported by another file.

There are two steps to declaring a constant. The first is to write the constant’s name followed by a type annotation:

sum :: Number

And the second step is to give the constant a value:

sum : 1 + 2

Importantly, constants are lazily evaluated — where variables indicate a computation to be performed now, a constant indicates a computation to be performed when the constant is first referenced. This means two things:

  • A file containing only constants can be used by another file, since the file contains no executable code at the top level.
  • Constants can refer to themselves recursively.

The type annotation of a constant, aka. it’s signature, cannot have type placeholders. For example, the following constant is invalid:

x :: Maybe _
x : Some 42

Why not? Wipple’s type checker performs local type inference (within constant bodies), not global type inference (across constant bodies). Global type inference makes programs much harder to reason about. Consider the following (invalid) code:

-- a.wpl
t :: _ -> _
t : x -> x

-- b.wpl
use "a.wpl"
f :: Number
f : t 42

-- c.wpl
use "a.wpl"
g :: Text
g : t "hi"

-- d.wpl
use "b.wpl"
use "c.wpl"

If type placeholders were allowed in foo’s signature, then the type of foo depends on where it’s used first: if b.wpl is type-checked before c.wpl, then c.wpl will fail to type-check, and vice versa. Since Wipple type-checks the program in a non-determistic order (or even in parallel), you would get different errors every time you compile! That would be very frustrating.

Generic constants

In lieu of type placeholders, you can define a generic constant by using a type function in its signature. For example, we can define a function first which returns the first of its two inputs:

first :: A B => A -> B -> A
first : x -> _ -> x

Traits

Traits are a way to describe the “behavior” of one or more types. For example, the Show trait defines what the show function should display for a value of a given type. To define a trait, use the trait template in conjunction with a type function:

Default : A => trait A

Here, Default is a trait that defines the “default value” for a type.

We can implement a trait for a specific type using an instance pattern:

instance (Default Number) : 0
instance (Default Text) : ""
instance (Default Boolean) : False
-- ...and so on

To use a specfic implementation of a trait, refer to the trait by its name. The implementation used depends on the type of the surrounding expression:

a : (Default :: Number) -- a : 0
b : (Default :: Text) -- b : ""
c : (Default :: Boolean) -- c : False

You can also store more complicated values inside the trait’s implementation, eg. a function:

Equal : A => trait (A -> A -> Boolean)

Person : type {
    name :: Text
    age :: Number
}

instance (Equal Person) -> p1 -> p2 ->
    name of p1 = name of p2
        and age of p1 = age of p2

alice : Person {
    name : "Alice"
    age : 25
}

bob : Person {
    name : "Bob"
    age : 30
}

Equal alice bob -- False
Equal bob bob -- True

And you can use generics in instance declarations by providing a type function:

-- The "default" value of a list is the empty list
A => instance (Default (List A)) : (list)

Bounded constants and instances

You can use a where clause in a type function to provide bounds on the type parameters. For example:

show :: A where (Show A) => A -> ()
A B C where (Default A) (Default B) (Default C) =>
    instance (Default (A , B , C)) : Default , Default , Default