Wilson Gramer

Trait-Oriented Programming in Wipple

January 11, 2021

I’ve come up with a paradigm called “trait-oriented programming”. After experimenting with it for a while I think it’s the most natural way to think about data, and it fits Wipple perfectly! So how does it work?

Pillars of Trait-Oriented Programming

  1. Traits uniquely describe how a value can be used in the program.
  2. Values are simply collections of traits.
  3. Conformances describe how to derive a trait from a value if a condition is met.

In Wipple, traits are represented by values, meaning that values are just collections of other values. (Eventually, of course, there must be a few intrinsic traits that are actually represented differently by the interpreter.) The important thing to understand here is that values do not have a single representation in TOP — instead, traits are used to describe all representations of a value. For example, the number 42 can be represented as an integer, a decimal, a string, a DOM node, etc. All of these representations are equally valid depending on the context, and you can freely switch between them. Given this, there are a few rules regarding the use of traits:

  • Use traits to describe lossless, equivalent representations of a value; eg. a numeric representation and a string representation can be converted between each other without losing any information.
  • Traits that describe lossy representations of a value describe the value’s behavior, and should be used in contexts when the original value is not important.
  • Traits must be pure (ie. no mutation, IO or other side effects) because it doesn’t matter where/how they are used.

The rule of using traits to describe lossless, equivalent representations of a value ensures that Wipple remains a “safe” language in regards to datatypes — unlike JavaScript or other loosely-typed languages, nothing is being coerced from one type to another.

Because defining every single trait on every single value would be tedious and inflexible, conformances allow you to describe a new representation or behavior of an existing value, assuming it meets certain conditions. When a trait from a value is used that isn’t directly defined on the value, it is automatically derived as implemented by the conformance. This means that, in general, you can explicitly define one trait of a value (eg. during the value’s creation) and use it as if any of the traits were defined explicitly.

Some code examples

Basic traits

Let’s consider a trait called Text that stores a string representation of a value. If we have some value x, we can give it a string representation by explicitly defining Text’s value:

x :: Text "the amazing x value"

Remember that we aren’t reassigning x here — we’re just giving it another representation (: is for assignment, :: is for traits). Now we can define a function, print, that uses the Text value:

print : Text value -> ...
print x

In the context of the function, value will refer to "the amazing x value" when we pass in x.

Another important thing to understand is that "the amazing x value" is also a value with its own traits. In particular, "the amazing x value" has the Text trait, which contains itself.

Conformances

Say we have a Color trait that stores an RGB color representation of a value, and say we have red, orange, yellow defined as values containing the Color trait. Now if we wanted to make these colors (read: values with a color representation) applicable to print, we could manually add the Text trait to each of them. The problem, though, is that there may be many more colors, and possibly new ones defined in the future. To solve this, we can define a conformance:

Color color ::= Text ...

This conformance says that all values containing the trait Color can also be represented by the trait Text (the ... is the omitted implementation, which returns a string value and has access to color). Now any function accepting a Text value can also accept a Color value, with no modification to the function’s implementation!

print red -- works!

green : <some color>
print green -- also works!

Keep in mind that conformances can accept any condition — for example, we can say that a list of Text values is also Text:

(List and (each Text)) list ::= Text ...

We can also use this approach to create “generic” functions:

-- given a trait Add:
add : Add a -> a

How does this work? Remember that functions accept the value of the trait, not the original value passed into the function. This means that add simply delegates to the implementation provided by the Add trait of the input. This pattern is called “implementing” a trait, and is more generically defined by the impl function:

impl : Trait T -> T x -> x

Say we have some value foo and a trait Bar:

foo :: Bar (x -> do-something-with x)

Then we can call impl Bar on foo like this:

do-something-using-foo : impl Bar foo
foo 42 -- result of 'do-something-with 42'

The transitive property

Wipple does not automatically apply the transitive property to conformances; eg. this code will not work:

A x ::= B x
B x ::= C x
x :: A _
C for x

You need to explicitly conform A to C using B as the implementation:

A x ::= C (B for x)

The reason for this is twofold — first, it would be extremely expensive to calculate all possible paths considering the conformance condition can be any validator, and second it causes ambiguity when a value has multiple traits that can be used to derive a conformance through the transitive property.

Interacting with traits in Wipple

Operator Description Usage
: Assign a value to a name x : 42
:: Add a trait to a value x :: Text "the amazing x value"
::= Define a conformance (List and (each Text)) list ::= Text ...
is Perform a validation when (x is Number) ...
trait Create a new, unique trait that validates its value Color : trait (shape (r Number g Number b Number))
Foo : trait _
_ The value containing no traits directly; can be used as a validator that accepts all values _
T for x Get the trait value from a value Number for x
impl Create a macro that delegates to the trait value add : impl Add
new Create a value containing the provided trait red : new Color (r 255 g 0 b 0)
of Get the validator of a trait’s associated value Text = of Text
-> Create a function inc : x -> x + 1
inc : Number x -> x + 1
=> Create a macro inc! : x => (x : x + 1)

How does new work?

new : Trait T -> value -> {
    x : _
    x :: T value
    x
}

How does x is T work?

Trait values (the container holding the actual trait so it can be referred to by name) conform to the Validate trait, which the is operator uses to validate the input. If the input contains the trait, or the trait can be derived via a conformance, the validation succeeds.


This was a very technical explanation of traits, but I will be publishing more accessible examples soon, as well as a comparison between TOP and other paradigms — stay tuned!

© 2021 Wilson Gramer

Source code