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