Example: Trait Intersections A Story About Animals Defining The Abilities Introducing The Animals Telling A Story The Full Example

Example: Trait Intersections

The Intersection proxy does not exclusively work on types; it can be used to combine traits as well. Intersecting two traits means creating an intersection type that multimethods can dispatch on to make sure an object can be used with all of the multimethods defined by the traits.

Let's look at an example.

A Story About Animals

Let's say we have different kinds of animals in our program and we want to write a multimethod that comments on each animal's ability when given an instance of that animal as an argument.

To keep this simple, we'll only implement two abilities: flying and swimming.

This is interesting because there are animals that can only swim, those that can fly but not swim, and those that can do both.

Defining The Abilities

Since abilities are about doing something, we can use traits to define this behavior:

import {
    Generic, def, Trait, Intersection, TAny, Struct, JSString
} from "@fed1/jsos";

/* ---------------------------------------------------------------- ABILITIES */

const { fly, swim } = Generic;

const { IFlies } = Trait(
    [fly, TAny]
);

const { ISwims } = Trait(
    [swim, TAny]
);

When an animal type implements IFlies, it means that the fly() multimethod can be called with an instance of that type. Similarly, an animal type with the ISwims trait can be used to call the swim() multimethod.

Here comes the interesting part:

const TFliesAndSwims = Intersection(IFlies, ISwims);

With this, we create a TFliesAndSwims type that combines both traits. This is not itself a trait, it's an abtract type describing an object that conforms to both traits. It cannot be instantiated or implemented, but it can be used to dispatch on objects that implement both traits.

With this, we have all our abilities covered.

Introducing The Animals

Now it's time to define our animal types. Each animal should have a name property. To implement our animals, we can use structs:

/* ----------------------------------------------------------------- PENGUINS */

const { Penguin } = Struct({
    name: JSString
});

This creates a Penguin type with a name property that must be a string.

Penguins are birds that can swim, but they can't fly. So we implement only the ISwims trait:

Penguin.implement(ISwims, [
    def(ISwims.swim, [Penguin], p =>
        console.log(`Penguin ${ p.name } plunges through the waves.`))
]);

With this, we define that Penguin is an ISwims and that when we call the swim() multimethod with an instance of Penguin, a text is printed to the console that the penguin, who is referenced by name, swims in the water.

Let's now create an animal that can fly but not swim:

/* --------------------------------------------------------------------- BEES */

const { Bee } = Struct({
    name: JSString
});

Bee.implement(IFlies, [
    def(IFlies.fly, [Bee], s =>
        console.log(`Bee ${ s.name } buzzes through the air.`))
]);

Finally, we need an animal that can both swim and fly.

/* -------------------------------------------------------------- FLYING FISH */

const { FlyingFish } = Struct({
    name: JSString
});

FlyingFish.implement(ISwims, [
    def(ISwims.swim, [FlyingFish], f =>
        console.log(`Flying fish ${ f.name } swims through the water.`))
]);

FlyingFish.implement(IFlies, [
    def(IFlies.fly, [FlyingFish], f =>
        console.log(
            `Flying fish ${ f.name } jumps out of the water and glides ` +
            `through the air.`
        )
    )
]);

Telling A Story

Now that our animals are defined, let's create a multimethod that tells a story that changes depending on the abilities of the animal given to it:

/* ---------------------------------------------------------- TELLING A STORY */

const { tellStory } = Generic;

def(tellStory, [IFlies], animal => {
    console.log(`This animal can't swim, only fly, but that's cool too!`);
    fly(animal);
    console.log("");
});

def(tellStory, [ISwims], animal => {
    console.log(`This animal can't fly, only swim, but that's also nice!`);
    swim(animal);
    console.log("");
});

def(tellStory, [TFliesAndSwims], animal => {
    console.log(`Wow, look at that! An animal that can swim AND fly!`);
    swim(animal);
    fly(animal);
    console.log("Impressive!\n");
});

As you can see, we can use the TFliesAndSwims type for multimethod dispatch just like we would any other type, even though IFlies and ISwims are not types but traits.

We can now instantiate some animals to test our new story feature:

tellStory(Penguin.new({name: "Carl"}));
tellStory(FlyingFish.new({name: "Matilda"}));
tellStory(Bee.new({name: "Nana"}));

This gives us the following output:

This animal can't fly, only swim, but that's also nice!
Penguin Carl plunges through the waves.

Wow, look at that! An animal that can swim AND fly!
Flying fish Matilda swims through the water.
Flying fish Matilda jumps out of the water and glides through the air.
Impressive!

This animal can't swim, only fly, but that's cool too!
Bee Nana buzzes through the air.

The Full Example

The full example is available on our Codeberg repo.