Blog
November 13, 2023
Wipple’s Mutable
type is used to provide a binding to a value that’s shared across multiple places in the program. Now, Mutable
is more flexible: you can create a binding to a part of a value! Let’s take a look at an example to see why this is useful.
Say you have a Mutable Person
with a name
, and you want to add a suffix to the name. Previously, you would have to retrieve the name
, change it, and then build a whole new Person
value to pass to set!
.
Person : type {
name :: Text
age :: Natural
}
graduate! :: Mutable Person -> ()
graduate! : person -> person . set! (Person {
name : (name of get person) + ", Ph.D."
age : age of get person
})
Now, you can use projections to make this code much simpler!
graduate! :: Mutable Person -> ()
graduate! : project-field name | add! ", Ph.D."
In a language with traditional references like C++, that code might look like this:
void graduate(Person &person) {
person.name += "Ph.D.";
}
So how does project-field
work? Under the hood, Wipple has two new constructs. The first is where
for simplifying the process of updating a single field in a structure. It can be used anywhere, not just for Mutable
values!
-- The functional way
graduate :: Person -> Person
graduate : person -> \
person where { last-name : (last-name of person) + ", Ph.D." }
And the second is the way Mutable
is implemented. Previously, Mutable
was essentially a reference to a value on the heap. That functionality has been moved to the new Reference
type, and Mutable
is now implemented in terms of Reference
. But in addition to reference-based Mutable
values, you can now create computed Mutable
values that act like two-way bindings:
-- Remove leading and trailing whitespace from a `Text` value
trim-whitespace :: Text -> Text
trim-whitespace : ...
-- Project a `Mutable Text` so that it never contains leading or trailing whitespace
project-trim-whitespace :: Mutable Text -> Mutable Text
project-trim-whitespace : project trim-whitespace (new _ -> trim-whitespace new)
That new project
function is best explained by looking at its type. You provide a function that computes a B
from an A
, and a function that applies the new B
to the original A
. project
is intended to be partially applied; that is, you usually don’t provide the Mutable A
immediately. Instead, you use project
to define your own functions that operate on Mutable
values.
project :: A B => (A -> B) -> (B -> A -> A) -> Mutable A -> Mutable B
project-field
isn’t magic, either — it’s implemented as a syntax rule!
project-field : syntax {
project-field 'field -> \
project ({ 'field } -> 'field) (new val -> val where { 'field : new })
}
Finally, you can create a Mutable
value that ignores changes with the constant
function:
one : constant 1
increment! one
show (get one) -- 1
The API for interacting with mutable values hasn’t changed at all — you still use get
and set!
as normal, and all the new features work automatically!
November 10, 2023
Inspired by the reading guides students use to focus on one line at a time in a book, Wipple now has a focus mode that highlights the active line and fades away the other lines!
You can enable it in the Wipple Playground settings.
November 6, 2023
For a long time, Wipple has used the list
syntax to construct a list and the ,
syntax to construct a tuple. Today, this changes — the ,
syntax is now used for lists!
numbers : 1 , 2 , 3
numbers . each show
1
2
3
I decided to make this change for two reasons. First, lists are used much more often than tuples, so it makes sense to give list syntax priority. Second, Wipple parses syntax rules before resolving variables, so having a syntax rule named list
means that you can’t declare a variable named list
as well. The standard library worked around this by using names like l
or input
, but now you can just use the obvious variable name list
.
If you provide elements of different types, you still get a nice error message:
my-list : 1 , "2"
error:
┌─ test.wpl:1:15
│
1 │ my-list : 1 , "2"
│ ^^^
│ │
│ expected `Number`, but found `Text`
│ this element must have the same type as the other elements
│
= for more information, see https://wipple.dev/playground/?lesson=errors/mismatched-types
To create an empty list, use the ,
operator by itself (or the Default
implementation defined below):
instance (Default (List _)) : (,)
And to create a list with a single element:
just-one-number :: List Number
just-one-number : 1 ,
Trailing commas are allowed, so you can easily add a new item to a large list:
constants : (
1.41 ,
1.62 ,
2.72 ,
3.14 ,
6.28 ,
)
The ,
syntax for lists is defined in Wipple, too, meaning Wipple now supports variadic operators!
[operator Variadic-Precedence]
, : syntax {
, ...elements -> ...
}
And finally, to create a tuple, you now separate each element with a semicolon (;
):
my-tuple :: Number ; Text ; Boolean
my-tuple : 1 ; "a" ; True
first ; second ; third : my-tuple
show first
show second
show third
1
a
True
These changes are live on the Wipple Playground, and the lessons have been updated to use the new syntax.
November 4, 2023
Let’s say we want to implement Default
for a tuple, defined to be a tuple of the default value of each element. To accomplish this, we can define an instance Default (A , B)
for any types A
and B
:
A B => instance (Default (A , B)) : (Default , Default)
This will fail to compile…
error:
┌─ test.wpl:1:38
│
1 │ A B => instance (Default (A , B)) : (Default , Default)
│ ^^^^^^^ could not find instance `Default A` for any type `A`
│
= for more information, see https://wipple.dev/playground/?lesson=errors/missing-instance
error:
┌─ test.wpl:1:48
│
1 │ A B => instance (Default (A , B)) : (Default , Default)
│ ^^^^^^^ could not find instance `Default B` for any type `B`
│
= for more information, see https://wipple.dev/playground/?lesson=errors/missing-instance
…because whatever types A
and B
end up being don’t necessarily have a Default
implementation. For example, there is no default Grade
:
Grade : type { A B C D F }
-- What implementation would we use here???
(Default) :: (Grade , Grade)
To resolve this, we can use a where
clause to add bounds to the instance, propagating the Default
requirements to the caller:
A B where (Default A) (Default B) => \
instance (Default (A , B)) : (Default , Default)
Great — now within the instance, we can assume that Default A
and Default B
exist, and our code compiles!
However, before today, there was a bug in Wipple that caused the compiler to crash or even allow invalid code to compile. Let’s say we want to infer the second element of the tuple:
my-tuple : Default :: (Number , _) -- instance (Default Number) : 0
Previous versions of Wipple would accept this code, inferring the second element to be Number
as well! Logically, this doesn’t make sense — within the Default (A , B)
instance, A
and B
have no relation to each other; their Default
bounds are separate, so there’s no reason the type of one should be able to determine the type of the other.
Even worse, the information about B
’s type wasn’t passed back to the instance, meaning the type of B
was still unknown within the instance and no Default
implementation was ever found. Due to the order in which Wipple performs type inference, this caused the compiler to crash after typechecking completed, or sometimes even accept code with mismatched types!
So why did this bug occur? When Wipple encounters a trait in expression position, it searches for an instance
that’s compatible in the current context. For example, the following code prints X
because the instance Show X
is chosen over Show Y
:
X : type
instance (Show X) : "X"
Y : type
instance (Show Y) : "Y"
value : X
show value -- the input to `show` is a value of type `X`
This works fine because we’re at the top level. But inside the body of a generic constant or instance, we are dealing with abstract type parameters about which no information can be assumed except what is provided by bounds.
show :: A where (Show A) => A -> ()
show : input -> {
-- First, produce a `Text` value using `Show`...
text : Show input
-- Then, display it on the screen.
intrinsic "display" text
}
A generic constant by itself doesn’t ever appear in the final program — it only appears in monomorphized form, where the type parameter A
is replaced with a concrete type like Number
or Text
. If we refer to show
in the program, the compiler immediately creates a new copy of show
’s body where all the type parameters are replaced with placeholders that can be substituted with any type. For example, if we have the following program (ignoring the bounds for a moment):
show :: A => A -> ()
show : <body>
show 3.14
Then the equivalent monomorphized program looks like this:
(<body> :: (_ -> ())) 3.14
And the placeholder is inferred to be Number
due to 3.14
. You can see this effect more clearly if you assign a generic function to a variable, and then attempt to call the variable with inputs of different types:
monomorphized-show : show
monomorphized-show 3.14
monomorphized-show "Hello, world!"
error:
┌─ test.wpl:3:20
│
3 │ monomorphized-show "Hello, world!"
│ ^^^^^^^^^^^^^^^ expected `Number`, but found `Text`
│
= for more information, see https://wipple.dev/playground/?lesson=errors/mismatched-types
The next step is to resolve the bounds. Just like with the body, any type parameters in the bounds are also replaced with placeholders. Bounds are evaluated after inferring the concrete types of the type parameters (unless you mark the type parameter with infer
), and once a bound is monomorphized, it is added to the list of available instances.
So let’s go back to our original example and perform monomorphization (I’ll denote the different placeholders with lowercase letters):
-- We have:
A B where (Default A) (Default B) => \
instance (Default (A , B)) : (Default , Default)
-- So when Wipple sees this:
Default :: (Number , _)
-- It generates this:
((Default :: a) , (Default :: b)) :: (Number , _)
After type inference, a
is known to be Number
and b
is still unknown. And here lies the bug: bound instances have a higher priority than declared instances. This means that when searching the list of available instances, we check instance (Default a)
and instance (Default b)
before checking any instances defined on concrete types. This search is done in the same order as the bounds.
So, because a
is Number
and Default a
is the first bound, there are no other high-priority instances to choose from yet, and we fall back to the declared instance Default Number
. We then register the body of Default Number
as the body of the Default a
bound.
But when searching for a suitable instance Default b
, we now have this high-priority Default a
bound available! And since we know a
is Number
, we again choose the Default Number
instance and infer b
as Number
.
Before this bug was fixed, you could actually see the order of bounds checking in the problem. This code compiled:
A B where (Default A) (Default B) => \
instance (Default (A , B)) : (Default , Default)
(Default) :: (Number , _)
But this code did not:
A B where (Default A) (Default B) => \
instance (Default (A , B)) : (Default , Default)
(Default) :: (_ , Number)
error:
┌─ test.wpl:4:2
│
4 │ (Default) :: (_ , Number)
│ ^^^^^^^
│ │
│ could not determine the type of this expression
│ this has type `_ , Number`
│
= annotate the type with `::`: `:: {%type%}`
= for more information, see https://wipple.dev/playground/?lesson=errors/unknown-type
The fix for the bug is actually pretty simple — just wait to add the bounds to the list of available instances until after all bounds have been monomorphized. That way, type inference within bounds can only use the low-priority instances declared for concrete types. Now, Wipple correctly raises an error at compile time!
error:
┌─ test.wpl:4:2
│
4 │ (Default) :: (Number , _)
│ ^^^^^^^
│ │
│ could not determine the type of this expression
│ this has type `Number , _`
│
= annotate the type with `::`: `:: {%type%}`
= for more information, see https://wipple.dev/playground/?lesson=errors/unknown-type
I hope this article helped you understand a bit more about how Wipple’s type system works under the hood. As you can see, there are a lot of parts interacting with each other, and things can fail in subtle ways. I’m working on improving Wipple’s automated test suite to catch issues like this — as of this writing, there are 75 tests!
If you’re interested in learning more about Wipple’s type system, try exploring this lesson on type-level programming in the Wipple Playground!
October 30, 2023
Learning to read error messages is an important part of learning to code, and I want Wipple’s error messages to be useful to beginners and provide help to fix the code. When I first implemented error reporting in the Wipple Playground, it looked like this:
There’s a lot going on here, and it can get overwhelming very quickly! So to make it easier for beginners, I hid all the errors when beginner mode was enabled:
Now the user had to hover over each place in the code where an error occurred, but it was still pretty overwhelming (not to mention annoying if you’re just trying to place your cursor).
In response to these issues, I have redesigned the way errors are displayed in the Wipple Playground! First, I switched out the red for a calmer blue color. By default, only the primary error message is displayed; you can click "Show more" to reveal all the details and the location in the source code. This button is per error message, so you can read about a single issue in more depth without expanding all the other diagnostics. And finally, the fix-it button appears right below the description so it can be easily applied.
Here’s a GIF of the new design in action!
October 28, 2023
Previously, Wipple’s built-in math operations like /
and sqrt
would cause the program to crash if provided an invalid input. This caused problems when graphing functions using the math
library:
plot (x -> 1 / x) -- crashed when x = 0!
Really, what we want is to skip graphing any points that produce an undefined result. So now, Wipple’s Number
type has a new member — undefined
!
Rather than crashing, all of Wipple’s math operations now return undefined
if provided an invalid or undefined
input. This means undefined
propagates through the program:
x : sqrt -1
show x -- undefined
show (x + 1) -- undefined
When comparing undefined
with another number, the result is always False
. If you need to check whether an number is undefined
, you can use undefined?
:
x : 0 / 0
show (x = undefined) -- False
show (undefined? x) -- True
This behavior matches the NaN value in the IEEE 754 floating-point standard (Wipple’s Number
type has a decimal representation, however), and indeed undefined
is represented as NaN
in JavaScript.
October 23, 2023
Last week, I visited Tyngsborough Elementary School in Tyngsborough, Massachusetts to teach Wipple to the 4th and 5th graders in Science and Technology class. Students spent about 30 minutes creating a Turtle drawing using the Wipple Playground. For many students, Wipple was their first text-based programming language, so learning Wipple was also an opportunity to practice typing. As I walked around the classroom, I was amazed by everyone’s creativity!
On my first day at TES, I showed the students how to use repeat
to run a block of code multiple times. I instructed them to copy their code to the clipboard, type repeat (4 times) {}
, and paste their code between the braces. I noticed many students found this challenging — all the keyboard shortcuts were overwhelming them and taking away their attention from the code itself. So this week, I made three changes to make Wipple more mouse-friendly: an Edit menu, snippets, and a new Insert button!
Edit menu
Previously, the code editor in the Wipple Playground operated entirely on keyboard shortcuts. Now, there’s a new Edit menu in the top left of the screen to activate common commands like Copy, Paste, and Select All with your mouse. Keyboard shortcuts are an important part of learning to type, but I want first-timers to be able to write and manipulate short programs quickly without getting overwhelmed. Learning to code is already a challenging task!
Snippets
Wipple now supports defining "snippets" that expand to new code or wrap existing code. Here are a few examples:
snippet "Move forward" : forward (50 pixels)
snippet "Show" : show 'code
snippet "Repeat" : repeat (5 times) {
'code
}
These snippets are parsed by the compiler and are available in the analyzed program so IDEs can suggest them. You write snippets alongside your other code, all in the same file, so there’s no setup required. You can also insert a placeholder ('code
) to indicate that the snippet wraps the user’s selection.
Insert button
The + button in the top right of the editor now lives right next to your cursor, and suggests snippets based on your selection. Check it out!
Once I made these changes, students were able to get up to speed much more quickly and spend more time being creative. I think the best way to learn to code is by applying what you’re already passionate about, and I look forward to bringing Wipple to more students in the near future. I can’t wait to see what they’ll create!