In this post I shortly compare:
- Haskell Typeclass
- Rust Trait
- Typescript type constraints
Demystifying some big words (wikipedia definitions):
- Polymorphism - (many forms) "Polymorphism is the provision of a single interface to entities of different types"
I remember facing "polymorphism" when I just started to learn Java as a first OOP lang (~8 years ago).
The teacher taught us that one of the basics principles of an OOP lang is "polymorphism".
Whenever I hear of it I think of the famous animal example. having "Animal" class which the "Dog" and "Cat" inherit from and I can iterate a collection of Animals and invoke the "makeSound()" function they all implement cause they all of the same type (object) - "Animal".
But If being honest I don't remember the last time I used inheritance (even the prototype kind..) I usually define a structure type by an interface and defines another "behavioural" interface containing the methods I need to implement. (a matter of taste? Or a better segregation...).
Ad hoc polymorphism or just overloading: "Defines a common interface for an arbitrary set of individually specified types". on top of that we have functions overloading - the ability to create multiple functions of the same name with different implementations (handled by the compiler).
Parametric polymorphism "A way to make a language more expressive, while still maintaining full static type-safety" (Generics..)
Dynamic Dispatch - The process of selecting which implementation of a polymorphic operation (method or function) to call at run time.
Definitions:
TypeClass | Trait | Interface |
---|---|---|
A type system construct that supports ad hoc polymorphism. This is achieved by adding constraints to type variables in parametrically polymorphic types. If a type is a part of a typeclass it should implement the behavior described on that typeclass. | A trait is a language feature that tells the Rust compiler about functionality a type must provide | We use an interface to define a shape and we use Generics constraints for saying that a certain behaviour (or an attribute) is required by a type. |
Examples:
Haskell:
Following example is from: learnyouahaskell
data Person = Person { firstName :: String
, lastName :: String
, age :: Int
} deriving (Eq)
let adRock = Person {firstName = "Adam", lastName = "Horovitz", age = 41}
let mca = Person {firstName = "Adam", lastName = "Yauch", age = 44}
-- mca == adRock will be false
I'm far of being an haskell developer but it seem to fit the typeclass definition, there's a class Eq, it contains "==" and "/=" functions,
deriving (Eq) tells the compiler to generate instance of the Eq class for the Person type, this way it implements / has the "==" "/=" operators.
Rust:
trait NoisyAnimal {
fn make_noise<'staic>(&self) -> &'staic str;
}
struct Cat{}
impl NoisyAnimal for Cat {
fn make_noise<'staic>(&self) -> &'staic str {
"meow"
}
}
struct Dog{}
impl NoisyAnimal for Dog {
fn make_noise<'staic>(&self) -> &'staic str {
"woof"
}
}
fn make_noises(animals: Vec<Box<dyn NoisyAnimal>>) {
// ----- trait object
for animal in animals.iter() {
println!("{}", animal.make_noise());
}
}
fn make_noises_with_constraints<T: NoisyAnimal>(animal: T) {
println!("{}", animal.make_noise());
}
pub fn call_make_noises() {
make_noises_with_constraints(Dog{});
let animals: Vec<Box<dyn NoisyAnimal>> = vec![
Box::new(Dog{}),
Box::new(Cat{}),
];
make_noises(animals);
}
Struct is a type that is composed of other types. (in Typescript we model in an interface). In the above example we're implementing the trait obj functionality to 2 structs Cat & Dog.
The make_noises fn iterates a vector of types that implements the trait object (dynamic dispatch) - pretty cool :D.
I also created the make_noises_with_constraints() methos which demonstrates a trait constraint which is more relevant to the concepts I raised..
Typescript:
interface Instrument {
play: () => void;
}
class Guitar implements Instrument {
play() {}
}
class Drums implements Instrument {
play() {}
}
function playPart <T extends Instrument>(instruments: T[]) : void {
for (const instrument of instruments) {
instrument.play();
}
}
playPart([new Guitar(), new Drums()]);
Using Generics constraints I'm not limiting the playPart() function for a single type of instruments, we can make sound of many things, e.g. a bottle of beer, so in case that bottle implements the Instrument interface, it will be playable - (which could be a better name actually :P ).
Cheers,
Liron.
Top comments (2)
I want to challenge the Typescript part of your post. Hope that is ok.
Your Typescript example doesn't really use the generic type variable
T
. What it illustrates is subtype polymorphism, not ad-hoc. Typescript only supports a rather primitive form of ad-hoc polymorphism at runtime through function overloading. You can overload a single name with several function types and than provide the implementation, where each type must be represented in a corresponding if/else branch.Type classes in Haskell are a compile time mechanism. They go beyond simple name overloading by giving the mapping of a name to several types itself a name. Now you can refer to this named mapping and assign several overloaded names to it. You can express laws its members must abide by. You can form hierarchies where a subclass inherits overloaded names from a superclass. Named mappings are type classes.
Challenging is more than OK! my purpose of writing about those topics is "technical/personal grow"! The generic type just gives a certain agility.. I like comparing different languages to get a bit sharper on my daily lang and keep an interesting conversion :)