Generic Programming with Context Bounds
What is a context bound?
A context bound constrains a generic function: "this works for any
type T, as long as T has a specific typeclass instance."
// Scala — T must have a JSONWrite instance
def jsonify[T: JSONWrite](item: T): String
// Haskell — a must have a Show instance
show :: Show a => a -> String
// Python/funstruct — summon enforces the bound at runtime
def jsonify(item):
return summon(JSONWrite, type(item)).to_json_string(item)
The bound says: "I don't care what type item is — I just need
the right typeclass instance to exist."
How it works in funstruct
Step 1: summon resolves the instance
from funstruct.typeclasses import summon
# summon(Typeclass, Type) → instance or TypeError
writer = summon(JSONWrite, str) # → _StringJSONWrite()
writer = summon(JSONWrite, int) # → _IntJSONWrite()
writer = summon(JSONWrite, dict) # → TypeError: No instance of JSONWrite for dict
Step 2: use the instance
def jsonify(item) -> str:
writer = summon(JSONWrite, type(item)) # resolve
return writer.to_json_string(item) # use
Step 3: the bound is enforced
jsonify("hello") # works — JSONWrite[str] exists
jsonify(42) # works — JSONWrite[int] exists
jsonify({}) # TypeError: No instance of JSONWrite for dict
The TypeError IS the bound enforcement. In Scala/Haskell, this
happens at compile time. In Python, it happens at runtime — but the
error message is just as clear.
Patterns
Single bound
# Scala: def double[F[_]: Monad](fa: F[Int]): F[Int]
def double(F: Monad, fa):
return F.map(fa, lambda x: x * 2)
double(summon(Monad, Option), Some(21)) # Some(42)
double(summon(Monad, Result), Ok(21)) # Ok(42)
Multiple bounds
# Scala: def show_sorted[A: Ordering: Showable](items: List[A]): List[String]
def show_sorted(items: list) -> list[str]:
O = summon(Ordering, type(items[0])) # bound 1: Ordering
S = summon(Showable, type(items[0])) # bound 2: Showable
sorted_items = sorted(items, key=functools.cmp_to_key(O.compare))
return [S.show(item) for item in sorted_items]
Both bounds must be satisfied. If either is missing, summon raises
TypeError.
Auto-resolved bounds (tc_of)
For functions that receive a monadic value and need to resolve its type constructor:
from funstruct.typeclasses import tc_of
# tc_of(Some(42)) → Option
# tc_of(Ok(10)) → Result
def double(fa):
F = summon(Monad, tc_of(fa)) # auto-resolve from the value
return F.map(fa, lambda x: x * 2)
double(Some(21)) # Some(42) — auto-resolved
double(Ok(21)) # Ok(42) — auto-resolved
tc_of reads _type_constructor from the value's class. No explicit
type parameter needed.
Composition via bounds
The most powerful pattern: an instance that summons OTHER instances.
class _ListJSONWrite(JSONWrite, for_type=list):
def to_json_string(self, items: list) -> str:
elem_writer = summon(JSONWrite, type(items[0])) # bound on element type
return "[" + ", ".join(elem_writer.to_json_string(i) for i in items) + "]"
jsonify([1, 2, 3]) calls _ListJSONWrite, which summons
_IntJSONWrite for the elements. jsonify(["a", "b"]) summons
_StringJSONWrite. The list instance doesn't know about int or str —
it delegates via the registry.
Comparison
| Haskell | Scala | Python/funstruct | |
|---|---|---|---|
| Bound syntax | Show a => |
[A: Show] |
summon(Show, type(a)) |
| Resolution | Compile time | Compile time | Runtime |
| Error | Won't compile | Won't compile | TypeError at call site |
| Composition | Implicit | Implicit | Explicit via summon |
The mechanism is identical — a dictionary of operations looked up by type. The difference is WHEN the lookup happens and WHO does it (compiler vs runtime).
When to use context bounds
Use bounds when your function should work for any type that has the capability:
def serialize(item):
return summon(Serializable, type(item)).to_dict(item)
# Works for User, Address, Order — anything with Serializable
Don't use bounds when your function only ever works with one type:
# Just use the type directly — no summon needed
def get_user_email(user: User) -> str:
return user.email
See also
guides/typeclass_pattern_json.md— the JSON example in detailguides/typeclass_pattern_ordering.md— the Ordering exampledemos/12_trait_bounds.py— trait bounds with success + failure casesdemos/15_typeclass_pattern_json.py— full JSON typeclass demo