Table of Contents
1. Introduction
In typeclass oriented languages, there exists a concept called a Summoner. A Summoner is something which can fetch a typeclass from the environment.
1.1. What is a Typeclass?
A typeclass is a unit of behavior which is attached to a piece of data externally.
For example, in Python I might say something supports + by defining __add__.
class Person:
def __init__(self, name, age, parents = None):
self.name = name
self.age = age
self.parent = None
def __add__(self, other):
return Person("Jeremy", 0, [self, other])
Now, this is a bad and insufficient definition for a million different reasons, but it's good enough for teaching. This says a Person can be added to something else and it'll produce another Person whose parents are the operands to the add operation. (It also says all children are named Jeremy.)
In a language like Scala (or Haskell or Rust), we'd declare this ability-to-add externally.
trait Addable[T] {
def add(left: T, right: T)
}
case class Person(name: String, age: String, parents: List[Person])
implicit val personAddable = new Addable[Person] { override def add(left: Person, right: Person) = Person("Jeremy", 0, List(left, right))}
Notice I'm attaching the "You can add Persons" behavior after I've created the Person class and outside the Person class definition. Now, anywhere I need some type which can be added I can pass around Person instances.
One thing I like about Scala's implementation is you can have multiple implementations of Addable[T] for any type T. This is explicitly disallowed in Haskell and Rust for coherence and optimization reasons.
In Scala, these little units of behavior (i.e. our Addable[Person]) are just objects with functions on them. They're first class. They can be passed into functions and manipulated themselves. The behavior is itself expressed as data. I could even attach behavior to the behavior! (i.e. What does it mean to add Addables? What would it look like to define Addable[Addable[T]]?) We can find ourselves deep in recursive strange loops pretty quick.
2. Summoners in Scala
In Scala, I can fetch the behavior for any particular type with `Implicitly`. For teaching purposes, I'll define my own and call it `summon`.
def summon[T](implicit t: T) = t
and then I can invoke this to fetch types from the implicit scope and make them explicit.
val personAddable = summon[Addable[Person]]() personAddable.add(Person(...), Person(...))
Notice that since personAddable is just a trait instance (just a regular ol' object) I can access its fields like any other piece of data.
3. Summoners in Rust
Things are a little different in Rust. Rust does not support first class typeclasses. I cannot pass around typeclass instances as data, I cannot define multiple implementations of a typeclass for any single type, nor can I define typeclasses over typeclass instances.
But I can fetch a typeclass into an object for the sake of invoking its methods as static functions!
use std::ops::*; let result = <i32 as Mul<i32>>::mul(2, 3); dbg!(result); let v = vec![1, 2, 3, 2, 1]; let result = <Vec<i32> as Index<usize>>::index(&v, 1); dbg!(result);
In Rust <i32 as Mul<i32>> and <Vec<i32> as Index<usize>> are my summoners.
It can be read as "From the type i32 give me the behavior for how to multiply it to another i32" and "From a Vec<i32> give me the behavior for how to fetch the nth element".
Notice I am required to immediately invoke mul and index. I am not able to save the typeclass instance itself into a variable.
In both Scala and Rust, I'm not allowed to summon a typeclass that does not exist.
<i32 as Mul<Boolean>>::mul(2, True); // *fails*