Interfaces, Traits & Duck Typing
The same task in every language: model "any shape with an area", give a Circle and a Square their own area, then dispatch over a heterogeneous list and sum the results. Watch how each language abstracts over behavior rather than data - a Go interface, a Rust trait, an OCaml object's structural type, Perl's and Python's duck typing, and - because user-defined traits are deferred in Guji v0 - a Guji enum with a method that matches on itself.
# Guji has no user-defined traits/interfaces yet - they stay deferred (spec
# §21). The idiomatic way to abstract over "a shape with an area" is an
# `enum`: a Shape *is one of* these variants, and a method dispatches by
# matching on $self. The compiler checks the match is exhaustive.
enum Shape {
Circle($radius: Float)
Square($side: Float)
sub area($self): Float {
match $self {
Circle($r) { 3.14159 * $r * $r }
Square($s) { $s * $s }
}
}
sub name($self): Str {
match $self {
Circle($r) { "circle" }
Square($s) { "square" }
}
}
}
sub main() {
@shapes = [Circle(1.0), Square(2.0), Circle(3.0)]
mut $total = 0.0
for $shape in @shapes {
print("{ $shape.name() } has area { $shape.area() }")
$total = $total + $shape.area()
}
print("total area: $total")
}Guji (v0.1-alpha, a statically-typed compiled language) has no user-defined traits or interfaces yet - they stay deferred (spec §21), so there is no user polymorphism beyond generics. The idiom is the sum type: a Shape enum whose area and name methods take $self and match on it, the construct the spec uses for dispatch by a value's shape. One enum holds every variant, a List[Shape] is naturally heterogeneous, and a for loop dispatches each $shape.area() / $shape.name() - the same loop-and-accumulate shape as the Go and Rust versions. The trade-off versus an open interface: a Shape is closed - adding a variant means editing the enum, but then the compiler forces every match to handle it. The whole program compiles ahead-of-time to a native binary (guji build) and runs identically under the reference interpreter.
package main
import "fmt"
// An interface lists the methods a type must have. Any type with an
// Area() and Name() method satisfies Shape *implicitly* - no "implements".
type Shape interface {
Area() float64
Name() string
}
type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }
func (c Circle) Name() string { return "circle" }
type Square struct{ Side float64 }
func (s Square) Area() float64 { return s.Side * s.Side }
func (s Square) Name() string { return "square" }
func main() {
shapes := []Shape{Circle{1.0}, Square{2.0}, Circle{3.0}}
total := 0.0
for _, s := range shapes {
fmt.Printf("%s has area %g\n", s.Name(), s.Area())
total += s.Area()
}
fmt.Printf("total area: %g\n", total)
}Go interfaces are satisfied structurally and implicitly: Circle and Square never name Shape, yet because each has the right methods they are usable as a Shape. A []Shape is therefore a heterogeneous list, and each call dispatches dynamically through the interface's method table. This duck-typing-with-static-checking is Go's only polymorphism mechanism besides generics, and the closest mainstream analogue to a Rust trait or a Guji enum method.
(* OCaml objects are structurally typed: any object with the right methods
has the right type, no class hierarchy needed. The list's element type
< area : float; name : string > is inferred from the methods called. *)
let circle r =
object
method area = 3.14159 *. r *. r
method name = "circle"
end
let square s =
object
method area = s *. s
method name = "square"
end
let () =
let shapes = [ circle 1.0; square 2.0; circle 3.0 ] in
List.iter
(fun s -> Printf.printf "%s has area %g\n" s#name s#area)
shapes;
let total = List.fold_left (fun acc s -> acc +. s#area) 0.0 shapes in
Printf.printf "total area: %g\n" totalOCaml's object system gives a structural interface: an object's type is just the set of methods it answers to, so circle and square share the inferred type < area : float; name : string > and live in one list without any declared Shape type or inheritance. Methods are called with #, as in s#area. This row-polymorphic, duck-typed flavor of objects is closer to Go interfaces than to nominal classes - the functional default in OCaml would instead be a variant + a function, much like Guji's enum.
{-# LANGUAGE ExistentialQuantification #-}
import Text.Printf (printf)
-- A type class declares the behavior "a shape with an area".
class Shape a where
area :: a -> Double
name :: a -> String
data Circle = Circle Double
data Square = Square Double
instance Shape Circle where
area (Circle r) = 3.14159 * r * r
name _ = "circle"
instance Shape Square where
area (Square s) = s * s
name _ = "square"
-- A heterogeneous list needs an existential wrapper.
data AnyShape = forall a. Shape a => AnyShape a
main :: IO ()
main = do
let shapes = [AnyShape (Circle 1.0), AnyShape (Square 2.0), AnyShape (Circle 3.0)]
mapM_ (\(AnyShape s) -> printf "%s has area %g\n" (name s) (area s)) shapes
let total = sum [area s | AnyShape s <- shapes]
printf "total area: %g\n" totalHaskell's interface is a type class: class Shape a declares area/name, and each type opts in with an instance block, an explicit "this type satisfies that class" much like a Rust trait impl. A list must be homogeneous, so mixing Circle and Square needs an existential wrapper - data AnyShape = forall a. Shape a => AnyShape a (the ExistentialQuantification extension) - which hides the concrete type while keeping the Shape dictionary for dispatch. Type classes give compile-time (static) dispatch by default; the existential box is what buys the dynamic, heterogeneous collection.
use strict;
use warnings;
# Perl polymorphism is pure duck typing: any object whose package defines
# area() and name() can stand in as a "shape" - there is no interface type
# and no shared base class. If it can ->area, it is a shape.
package Circle {
sub new { my ($class, $r) = @_; bless { radius => $r }, $class }
sub area { my $self = shift; 3.14159 * $self->{radius} ** 2 }
sub name { "circle" }
}
package Square {
sub new { my ($class, $s) = @_; bless { side => $s }, $class }
sub area { my $self = shift; $self->{side} ** 2 }
sub name { "square" }
}
my @shapes = (Circle->new(1.0), Square->new(2.0), Circle->new(3.0));
my $total = 0;
for my $shape (@shapes) {
printf "%s has area %s\n", $shape->name, $shape->area;
$total += $shape->area;
}
print "total area: $total\n";Perl has no interface or trait declaration: dispatch is by method name at run time, so a list of mixed Circle and Square objects works simply because each responds to ->area and ->name. Nothing is checked ahead of time - calling a missing method is a run-time error, the cost of this freedom. Modern Perl can add structure with Role::Tiny roles or Moose, but the bare blessed-object idiom shows duck typing in its purest form.
# A `role` is Raku's trait/interface: it declares required behavior, and a
# class `does` it. A stub method (`{...}`) makes implementing it mandatory.
role Shape {
method area( --> Numeric) { ... }
method name( --> Str) { ... }
}
class Circle does Shape {
has $.radius;
method area { 3.14159 * $.radius ** 2 }
method name { "circle" }
}
class Square does Shape {
has $.side;
method area { $.side ** 2 }
method name { "square" }
}
my Shape @shapes = Circle.new(radius => 1.0),
Square.new(side => 2.0),
Circle.new(radius => 3.0);
my $total = 0;
for @shapes -> $shape {
say "{ $shape.name } has area { $shape.area }";
$total += $shape.area;
}
say "total area: $total";Raku spells an interface as a role: Shape declares area/name as stub methods ({ ... }, the "yada" operator), and a class that does Shape must supply them or fail to compose. Roles are flatter and more flexible than classes - they mix in behavior and can be composed into several classes - and a typed Shape @shapes array then holds any of them. This role/does model is the direct conceptual ancestor of the traits Guji defers to a later version.
// A trait declares the behavior a type must provide.
trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }
fn name(&self) -> &str { "circle" }
}
impl Shape for Square {
fn area(&self) -> f64 { self.side * self.side }
fn name(&self) -> &str { "square" }
}
fn main() {
// A heterogeneous list needs trait objects: Box<dyn Shape>.
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
Box::new(Circle { radius: 3.0 }),
];
let mut total = 0.0;
for s in &shapes {
println!("{} has area {}", s.name(), s.area());
total += s.area();
}
println!("total area: {}", total);
}Rust's interface is a trait, implemented for each type in a separate impl Shape for Circle block - the impl keyword is the explicit "this type satisfies that trait" that Go leaves implicit. To put differing types in one list you need a trait object Box<dyn Shape>, which boxes each value and dispatches dynamically through a vtable; without dyn, generics would monomorphize and could not mix types. Traits also carry static dispatch and bounds (T: Shape), making them Rust's single, central abstraction mechanism.
import math
from typing import Protocol
# A Protocol describes a *structural* interface (PEP 544): any object with
# area() and name() is a Shape to a type checker - no inheritance required.
# At run time it is plain duck typing: if it has the methods, it works.
class Shape(Protocol):
def area(self) -> float: ...
def name(self) -> str: ...
class Circle:
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
def name(self) -> str:
return "circle"
class Square:
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
def name(self) -> str:
return "square"
shapes: list[Shape] = [Circle(1.0), Square(2.0), Circle(3.0)]
total = 0.0
for s in shapes:
print(f"{s.name()} has area {s.area()}")
total += s.area()
print(f"total area: {total}")Python dispatches by duck typing at run time: a list of mixed Circle and Square objects works because each defines area and name - neither class inherits anything. A typing.Protocol (PEP 544) adds an optional, structural interface a checker like mypy can verify, mirroring Go interfaces and OCaml objects, but it is erased at run time and imposes nothing. Older code used abc.ABC for a nominal base class; Protocol is the modern way to name a duck-typed contract.