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.