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 ofmatch
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 isNone
,Error
, or another type that can be converted into aResult
.
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 x
s 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 provideText
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 asfoo
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 thewhere
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 typeT
, you can’t writex :: 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
use
d 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 offoo
depends on where it’s used first: ifb.wpl
is type-checked beforec.wpl
, thenc.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