Wipple Home Blog Docs

Mutable projections

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!

Made by Wilson Gramer