Higher-Kinded Types — Senior Level¶
Topic: Higher-Kinded Types Focus: The kind system as a real type theory; Traversable/Foldable and effect-polymorphic programs; the three "higher" terms disambiguated; why Rust said no (and what GATs do instead); and the defunctionalization encoding in full.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Real-World Analogies
- Mental Models
- Code Examples
- Pros & Cons
- Use Cases
- Coding Patterns
- Best Practices
- Edge Cases & Pitfalls
- Summary
- Further Reading
Introduction¶
Focus: The kind system as a type theory in its own right, the abstractions that only HKTs make possible (
Traversable, effect-polymorphism, MTL/tagless-final), the precise difference between higher-kinded, higher-rank, and higher-order, and the deep reason mainstream systems languages — Rust above all — have refused HKTs while shipping Generic Associated Types as a partial substitute.
By now you can read and write Functor/Applicative/Monad instances and judge which constraint to demand. This level is about the system underneath and the engineering consequences at the top.
Three threads run through it:
-
Kinds are a small type system, one level up. There is a kind for types (
*), kind arrows (* -> *), kind variables, kind polymorphism, and even higher-order kinds. Treating kinds as "types of types" is not a slogan —Functorliterally has kind(* -> *) -> Constraint. We make the analogy exact. -
The abstractions that justify the machinery.
Traversable'straverse :: Applicative f => (a -> f b) -> t a -> f (t b)is quadratically higher-kinded — it abstracts over two constructors (tthe structure,fthe effect) at once. Tagless-final and MTL turn entire programs into values polymorphic in the effectF. These are the things you cannot express at all without HKTs. -
Why systems languages refuse them. Rust deliberately omits HKTs. The reasons are concrete — type inference, coherence, monomorphization, and the interaction with lifetimes — and Rust's GATs (stable since 1.65) are a partial answer that handles the "lending iterator" use case but still cannot express a general
Functor. Understanding exactly what's missing is the senior payoff.
The judgment a senior brings: HKTs are a power tool. The question is never "are they elegant" (they are) but "does the reuse they unlock exceed the comprehension tax they impose on this team, in this language, on this codebase?" We'll arm both sides of that argument.
Prerequisites¶
- Required: The middle page — typeclass mechanism, Functor/Applicative/Monad with laws,
Either epartial application, the encoding sketch. - Required: Comfort reading real Haskell (typeclasses,
do, constraints) and Scala 3 (given/using,F[_], type lambdas[X] =>> ...). - Required: Solid grasp of parametric polymorphism and trait/typeclass dispatch.
- Helpful: Familiarity with Rust generics, trait objects, and the idea of monomorphization.
- Helpful: Exposure to
traverse/sequenceand the notion of an "effect" (IO,Future,Task).
Glossary¶
| Term | Definition |
|---|---|
| Kind | The type of a type-level expression. Base kind * (a.k.a. Type); arrows k1 -> k2. |
Kind arrow * -> * | The kind of a unary type constructor; (* -> *) -> * is a higher-order kind. |
| Kind polymorphism | Type-level generics over kinds (Haskell PolyKinds): a definition that works for any kind k. |
Traversable | Typeclass with traverse :: Applicative f => (a -> f b) -> t a -> f (t b); abstracts over structure t and effect f. |
Foldable | Typeclass with foldr/foldMap; consume a structure t a into a summary. |
| Higher-kinded | Abstraction over a type constructor (* -> *+). The subject of this page. |
| Higher-rank | A function type with forall nested inside an argument position: (forall a. a -> a) -> .... About polymorphic function values. |
| Higher-order | A value-level function taking/returning functions. Unrelated to kinds. |
| Tagless-final | Encoding a DSL as typeclass methods over an abstract effect F, interpreted by choosing an F. |
| MTL | "Monad Transformer Library" style: stacked effect capabilities expressed as MonadState, MonadError, … constraints. |
| GAT | Generic Associated Type (Rust): an associated type parameterized by generics/lifetimes. A partial step toward HKTs. |
| Coherence | The guarantee that there is at most one instance of a typeclass/trait for a type, so resolution is unambiguous globally. |
| Monomorphization | Compiling a generic by generating a specialized copy per concrete instantiation (Rust, C++ templates). |
| Defunctionalization | Encoding a (type-level) function as data plus an Apply operation — the basis of HKT emulation. |
Core Concepts¶
1. Kinds form a typed lambda calculus, one level up¶
The clean way to see kinds: they are the types of the simply-typed lambda calculus whose terms are type constructors. Concretely:
VALUE level: terms 3, \x -> x have types Int, a -> a
TYPE level: types Int, Maybe, [] have kinds *, * -> *, * -> *
KIND level: kinds *, * -> * have sorts (just one: BOX/□)
Everything you know about function types transfers:
- Application:
Maybe IntisMaybeapplied toInt; kind*. Just likef 3appliesf : Int -> a. - Currying / partial application:
Either :: * -> * -> *;Either String :: * -> *. Mirrors(+) :: Int -> Int -> Int,(+) 3 :: Int -> Int. - Higher-order:
Functor :: (* -> *) -> Constrainttakes a constructor as an argument. This is a higher-order kind — the type-level analogue of a higher-order function.Constraintis GHC's kind for "a thing you can put left of=>". - Kind variables and polymorphism: with
PolyKinds, a definition can quantify over a kind variablek, giving type-level generics over kinds (e.g.Proxy :: forall k. k -> *).
So "higher-kinded" is precisely "uses a type variable whose kind is itself an arrow (* -> * or richer)". The whole edifice is one typed calculus stacked on another.
2. Traversable: doubly higher-kinded, the crown jewel¶
traverse is the abstraction that most convincingly needs HKTs, because it is generic in two constructors simultaneously:
class (Functor t, Foldable t) => Traversable t where
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
-- ^t :: * -> * (the structure)
-- ^f :: * -> * (the effect)
Read it: "for a structure t (list, tree, Maybe) and an effectful function a -> f b, visit every a in the structure, run the effect, and turn t (f b) inside-out into f (t b)." A single function gives you:
traverse validate [x, y, z] :: Either Err [Validated] -- validate a list, collect/short-circuit
traverse httpGet urls :: IO [Response] -- fire N requests, gather results
traverse lookup keys :: Maybe [Value] -- all-or-nothing lookups
sequence [Just 1, Just 2] :: Maybe [Int] == Just [1,2]
sequence [Just 1, Nothing] :: Maybe [Int] == Nothing
You cannot type traverse in Java, Go, or Rust: there is no way to write a function parameterized by two arbitrary * -> * constructors. This is the concrete capability HKTs buy that nothing else replicates.
3. The three "highers", disambiguated precisely¶
These collide constantly. Pin them down:
HIGHER-ORDER (value level, about functions):
map :: (a -> b) -> [a] -> [b]
A function whose ARGUMENT is a function. Ordinary. Every language has it.
HIGHER-KINDED (type level, about type constructors):
class Functor f where fmap :: (a -> b) -> f a -> f b
A type abstracts over a TYPE CONSTRUCTOR f :: * -> *.
Java/Go/Rust cannot do this.
HIGHER-RANK (type level, about WHERE forall sits):
applyToBoth :: (forall a. a -> a) -> (Int, Bool) -> (Int, Bool)
An argument that is ITSELF polymorphic — the caller cannot choose `a`;
the function gets to use it at multiple types internally.
(Rank-1 = all foralls at the outside, the usual case. Rank-2+ nests them.)
Diagnostic questions: - Does it take a function value? → higher-order. - Does it abstract over a container/constructor (F<_>)? → higher-kinded. - Does an argument carry its own forall (must stay polymorphic inside)? → higher-rank.
They're orthogonal: traverse is higher-kinded and higher-order; ST-monad's runST :: (forall s. ST s a) -> a is higher-rank but not higher-kinded.
4. Tagless-final and MTL: whole programs polymorphic in the effect¶
The ultimate payoff of HKTs is writing an entire program against an abstract effect F[_], then choosing F at the edge.
Tagless-final encodes capabilities as typeclasses:
trait Console[F[_]]: def putLine(s: String): F[Unit]; def getLine: F[String]
trait Clock[F[_]]: def now: F[Long]
def greet[F[_]: Monad](using C: Console[F], K: Clock[F]): F[Unit] =
for {
name <- C.getLine
t <- K.now
_ <- C.putLine(s"Hi $name at $t")
} yield ()
// Production: F = IO. Tests: F = a pure State monad that records output. SAME program.
MTL stacks capabilities as constraints (MonadState s, MonadError e, MonadReader r). A function (MonadState Int m, MonadError String m) => m Result declares "I need state and error", and the concrete monad transformer stack that provides them is chosen elsewhere. Both styles are only possible because m/F is a higher-kinded parameter the whole program quantifies over. This is the architecture behind Cats Effect, ZIO (which goes further with ZIO[R, E, A]), and Haskell's mtl/polysemy/effectful.
5. Why Rust refuses HKTs — the concrete reasons¶
Rust wants HKTs (RFCs and discussions span a decade) but has not shipped them. The obstacles are real and instructive:
- Type inference & coherence interact badly. Rust's trait resolution must stay decidable and coherent (one impl per type globally, the orphan rules). Adding type-constructor variables makes unification and overlap checking dramatically harder;
F<_>appearing inwhereclauses explodes the search space. - Lifetimes. Rust types carry lifetimes. A "container"
F<_>would frequently need to beF<'a, _>— abstracting over a constructor that is itself lifetime-parameterized — which is a much harder problem than HKTs in a GC'd language. Many natural Rust "functors" (iterators borrowing their source) want exactly this. - Monomorphization & associated-type machinery. Rust compiles generics by monomorphizing. A truly HKT-generic function would interact with the trait/associated-type system in ways the team judged not yet sound or ergonomic.
- No
pure/return-type polymorphism by default.pure :: a -> f areturns anfchosen by context; Rust resolves methods by receiver, so "produce the rightF" is awkward without HKTs.
The Functor you cannot write in Rust:
// THIS DOES NOT COMPILE. Rust has no `Self<U>` / higher-kinded `Self`.
trait Functor {
fn map<A, B>(self: Self<A>, f: impl Fn(A) -> B) -> Self<B>;
// ^^^^^^^^ you cannot apply a type parameter `Self` to `A`
}
There is no way to say "the same constructor, different element type". You can write Iterator::map, Option::map, Result::map individually, but not one trait that unifies them, so no generic traverse, no effect-polymorphism.
6. GATs: the partial step Rust did take¶
Generic Associated Types (stable Rust 1.65) let an associated type be parameterized by generics or lifetimes:
trait LendingIterator {
type Item<'a> where Self: 'a; // associated type with a LIFETIME parameter
fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}
GATs solve the "lending iterator / streaming iterator" problem (yielding items that borrow from the iterator) — long the poster child for "Rust needs HKTs". But GATs are not HKTs:
- A GAT parameterizes an associated type member of a trait. An HKT parameterizes the type implementing the trait by a constructor.
- You still cannot write
trait FunctorwhereSelfis applied to a varying element type. GATs give youtype Item<'a>, notSelf<A>. - They unblock specific patterns (lending iterators, some "type families") that resemble HKT use cases, which is why they're called "a step toward". They are not a general substitute, and the Rust community is explicit that full HKTs remain unsolved.
Mentally: GATs add parameters to the outputs of a trait; HKTs would let the trait itself range over constructors. Different axis.
7. The defunctionalization encoding, rigorously¶
The Yallop–White trick (TypeScript fp-ts, OCaml, Kotlin Arrow's earlier versions) turns "apply type constructor F to A" into a defunctionalized operation. The essence:
- Each constructor gets a unique brand/tag (a phantom marker type).
- A single open type-level function
Apply<F, A>(calledKind/URItoKindin fp-ts,Kind<F, A>in Arrow) maps(tag, A)to the real applied type via a registry. - Typeclasses are parameterized by the tag, and operations use
Apply<F, A>in their signatures.
interface URItoKind<A> {} // open registry (declaration merging)
type URIS = keyof URItoKind<unknown>;
type Kind<F extends URIS, A> = URItoKind<A>[F];
// register Option
declare module './hkt' { interface URItoKind<A> { Option: Option<A> } }
interface Monad<F extends URIS> {
of<A>(a: A): Kind<F, A>;
flatMap<A, B>(fa: Kind<F, A>, f: (a: A) => Kind<F, B>): Kind<F, B>;
}
It is sound and type-safe — but it is first-order emulation of a higher-order kind feature. The costs: instances are threaded explicitly (no given resolution unless you build it), error messages reference Kind<...> noise, and partial application of multi-arg constructors needs extra tags (URIS2, URIS3 in older fp-ts) or HKT-with-fixed-params encodings. Kotlin's Arrow used this for years and ultimately leaned away from heavy HKT emulation toward more direct, less generic APIs — a telling signal about the ergonomics ceiling.
Real-World Analogies¶
| Concept | Real-world thing |
|---|---|
| Kinds as a type system one level up | A floor plan (kind) constrains buildings (types) the way a building constrains rooms (values). Each level governs the one below. |
Traversable | A photocopier that takes a folder of slips and, for each slip, performs the same approval workflow, then hands you a single approval result for the whole folder — generic in both the folder shape and the workflow. |
| Tagless-final / effect polymorphism | A play script (the program) that can be staged by any theatre company (F): a full production (IO), a dress rehearsal that just notes cues (test F). Same script, swapped runtime. |
| Higher-rank | A skeleton key the function may use on many locks; the caller hands over a key that must open anything, not a key for one specific lock. |
| Rust refusing HKTs | A precision machine shop that won't add a feature until it's proven not to compromise the existing tolerances (coherence, lifetimes, monomorphization). |
| GATs | Adding adjustable jaws to one tool in the shop (the iterator) — genuinely useful, but not retooling the whole shop for arbitrary container-generic work. |
| Defunctionalization | A valet system: you can't hand the garage your car-builder, so you hand a ticket; a lookup desk reconstructs which vehicle the ticket means. |
Mental Models¶
The "two stacked lambda calculi" model¶
Values inhabit types; types inhabit kinds. The same rules — application, currying, arrows, variables, even polymorphism — appear at both levels. Once you see kinds as "the type system of type constructors", every HKT question reduces to a question you already know how to answer at the value level. Functor :: (* -> *) -> Constraint? That's just "a function taking a function" — one floor up.
The "what can't I write here" model¶
For any language, the fastest classifier is: can I write one generic Functor/traverse that works for List, Option, and Future at once? If yes (Haskell, Scala, PureScript), HKTs are native. If no (Java, Go, Rust, native TS/Kotlin), you're in kind-*-only territory and must encode or specialize. GATs don't move Rust into "yes".
The "abstraction tax ledger" model¶
Every HKT abstraction has a credit (duplication removed, capabilities unlocked) and a debit (concept count, error-message opacity, onboarding time, slower compiles in some stacks). A senior keeps the ledger explicitly per codebase. Cats/ZIO in a team fluent in them: net positive. The same in a team of Go-trained engineers shipping CRUD: usually net negative. The technology doesn't decide; the ledger does.
Code Examples¶
traverse unifying validation, IO, and optionality (Haskell)¶
-- ONE function; three radically different effects via the Applicative f.
parseRow :: String -> Either Error Int
loadRows :: [String] -> Either Error [Int]
loadRows = traverse parseRow -- short-circuits on first parse error
fetch :: Url -> IO Response
fetchAll :: [Url] -> IO [Response]
fetchAll = traverse fetch -- sequences N IO actions, collects results
-- And sequence is traverse id:
sequence' :: Applicative f => [f a] -> f [a]
sequence' = traverse id
Effect-polymorphic program, two interpreters (Scala / Cats)¶
import cats.Monad
import cats.syntax.all.*
trait KVStore[F[_]]:
def get(k: String): F[Option[String]]
def put(k: String, v: String): F[Unit]
// Business logic: pure, generic, testable. Mentions no concrete effect.
def upsertUpper[F[_]: Monad](k: String)(using kv: KVStore[F]): F[Unit] =
for {
cur <- kv.get(k)
_ <- kv.put(k, cur.getOrElse("").toUpperCase)
} yield ()
// At the edge: instantiate F = IO in prod, F = State[Map[...], *] in tests.
The Rust wall, made explicit¶
// What we WANT (does not compile in stable Rust):
// trait Functor { fn map<A, B>(self: Self<A>, f: impl Fn(A)->B) -> Self<B>; }
//
// What we can do instead: a trait with an ASSOCIATED OUTPUT type, but it must
// name the target constructor concretely — no general `Self<B>`.
trait MapOption<A> {
type Output<B>; // a GAT-ish slot, still not Self<B> in general
fn fmap<B>(self, f: impl Fn(A) -> B) -> Self::Output<B>;
}
// You end up writing one impl per concrete container; nothing unifies them,
// so there is still NO generic `traverse`. This is the HKT gap.
GAT: the lending iterator HKTs can't be conflated with¶
trait LendingIterator {
type Item<'a> where Self: 'a;
fn next(&mut self) -> Option<Self::Item<'_>>;
}
struct Windows<'s> { data: &'s [u8], i: usize, n: usize }
impl<'s> LendingIterator for Windows<'s> {
type Item<'a> = &'a [u8] where Self: 'a; // each item BORROWS from self
fn next(&mut self) -> Option<&[u8]> {
if self.i + self.n > self.data.len() { return None; }
let w = &self.data[self.i..self.i + self.n];
self.i += 1;
Some(w)
}
}
This is the GAT success story — yielding borrowed items, impossible before 1.65. Note it parameterizes the associated type Item<'a>, not the implementing type by a constructor. It does not give you Functor.
fp-ts defunctionalized traverse consumer (TypeScript)¶
import { array } from 'fp-ts/Array';
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
// array.traverse is generic over the target Applicative, encoded via Kind tags.
const parse = (s: string): O.Option<number> => {
const n = Number(s);
return Number.isNaN(n) ? O.none : O.some(n);
};
array.traverse(O.Applicative)(['1', '2', '3'], parse); // Option<number[]> = Some([1,2,3])
array.traverse(O.Applicative)(['1', 'x'], parse); // None
Functionally identical to Haskell's traverse parse, but the Applicative instance (O.Applicative) is passed explicitly and the higher-kinded plumbing lives in Kind/URItoKind behind the scenes.
Pros & Cons¶
| Aspect | Pros | Cons |
|---|---|---|
| Expressive ceiling | Traversable, effect-polymorphism, tagless-final/MTL — abstractions with no kind-* equivalent. | These are exactly the features that confuse readers and explode error messages. |
| Architecture | Whole programs polymorphic in F → swap prod/test/mock effects without rewriting logic. | Stacks (monad transformers) can be performance traps; ZIO/Cats-Effect mitigate but add their own surface. |
| Language fit | Native and ergonomic in Haskell/Scala/PureScript. | A square peg in Rust/Go/native-TS; encodings are sound but heavy and leak. |
| Coherence/inference | In languages designed for it, instance resolution is automatic. | The very feature that breaks Rust's coherence/inference; partial encodings shift the burden onto the programmer. |
| Team | Force-multiplier for fluent FP teams. | High onboarding cost; a common, defensible reason teams ban HKTs in shared codebases. |
Use Cases¶
- Generic structure-and-effect traversal: validate, fetch, or transform every element of any
Traversableunder anyApplicativewith onetraverse/sequence. - Effect-polymorphic core + thin edge: write business logic
program[F[_]: Monad], run it underIOin prod and a pure interpreter in tests (tagless-final, MTL, ZIO environment). - Library combinators users extend: ship
map/flatMap/traverse/foldMapso callers plug their own constructors in by providing instances. - Accumulating, parallelizable validation: Applicative
Validated/Parallelto gather all failures or fan out independent work.
Avoid / reconsider when: the language lacks native HKTs and the encoding cost dominates; the team isn't fluent; the codebase has one effect and no foreseeable second; latency-critical paths where transformer overhead matters and a concrete effect is clearer.
Coding Patterns¶
Pattern 1: Program to the weakest abstraction (Functor < Applicative < Traversable < Monad)¶
Constrain to the least powerful typeclass that compiles. Applicative instead of Monad keeps doors open (parallelism, error accumulation, more instances). This is the single highest-leverage habit in HKT code.
Pattern 2: Effect at the last hole; structure abstracted separately¶
In traverse, t is the structure and f the effect. Keep them distinct in your signatures; don't conflate "what shape am I walking" with "what effect am I producing".
Pattern 3: Tagless-final for swappable interpreters¶
Express capabilities as Capability[F[_]] traits/typeclasses, write logic against F[_]: Monad, and choose F (IO, test, mock) at the boundary. Keeps the core pure and the effect a deployment decision.
Pattern 4: In Rust, prefer GATs / concrete impls; don't fake HKTs¶
When you hit "I want a Functor" in Rust, step back: usually a concrete trait with an associated Output<B> GAT, or a hand-written per-type impl, is clearer than a heroic HKT emulation. Reserve emulation for libraries with a strong reason.
Pattern 5: Quarantine encoding machinery (TS/Kotlin)¶
If you must encode HKTs, isolate Kind/tag/registry plumbing in one module and expose clean F-generic APIs. Don't let URItoKind leak into application code or error messages users see daily.
Best Practices¶
- Default to native HKTs only in languages that have them. In Rust/Go/native-TS, treat HKT emulation as a last resort with explicit justification.
- Make the effect a parameter, not a hardcode, in core logic — but interpret it at one well-defined edge. That's the whole architectural value; don't sprinkle concrete
IOthrough the core. - Choose Applicative over Monad whenever steps are independent to preserve parallelism and full-error accumulation.
- Budget the abstraction tax explicitly. Write down what reuse you gain vs the comprehension/onboarding/compile cost. Revisit when team or scope changes.
- Lean on law-tested library instances (Cats
Discipline, Haskell QuickCheck laws) rather than bespoke ones; one unlawful instance corrupts every generic caller. - Disambiguate "higher" precisely in design docs and reviews. Confusing higher-kinded with higher-rank/higher-order derails discussions; name the exact axis.
- For Rust, frame GATs accurately: they solve lending-iterator/type-family problems, not general container-genericity. Don't promise a
Functoryou can't deliver.
Edge Cases & Pitfalls¶
- Monad transformer stacks have hidden cost.
StateT (ExceptT IO)allocates and indirects per bind; deep stacks can dominate runtime. Cats-Effect/ZIO/effectfulexist partly to flatten this. Profile effect-heavy code. pure's return-type polymorphism trips inference.pure(3)with no expectedFis ambiguous; annotate (3.pure[Option],pure @Maybe 3). Rust's lack of this is one of its HKT blockers.- Coherence assumptions differ across languages. Haskell/Rust enforce global coherence (one instance per type); Scala's implicits do not, so two conflicting
Monad[F]can be in scope. Generic HKT code can behave differently depending on which instance resolves. Set/constrained containers still aren't lawful Functors. Anything needingOrd/Eqon elements can't satisfy the unconstrained* -> *signature; you need constrained Functor classes (e.g.CFunctor) — a sign you're at the edge of the abstraction.- Conflating GATs with HKTs in design. They live on different axes (output-parameterization vs constructor-genericity). Claiming GATs "give Rust HKTs" leads to designs that hit a wall.
- Higher-rank vs higher-kinded mix-ups in type errors.
runST :: (forall s. ST s a) -> ais higher-rank; itsforall-under-arrow errors look alien if you assume it's an HKT issue. Identify the axis before debugging. - Encoded HKTs leak in error messages.
Kind<'Option', A>noise andURISconstraints make TS/Kotlin HKT errors hard for non-experts — a real onboarding hazard that argues against using them in shared code. - Over-generalization upfront. Writing
program[F[_]: Monad]before a second concreteFexists is speculative generality; you pay the tax with no current benefit. Generalize on the second use.
Summary¶
- Kinds are a typed calculus one level up: base kind
*, arrows* -> *, higher-order kinds likeFunctor :: (* -> *) -> Constraint, kind variables, and kind polymorphism. Every value-level intuition (application, currying, higher-order) transfers exactly. Traversable/traverseis the keystone HKT abstraction — doubly higher-kinded (structuretand effectf) — and is inexpressible without HKTs. It unifies validation, IO sequencing, and optional lookups in one function.- Higher-kinded ≠ higher-rank ≠ higher-order. Constructor-genericity vs
forall-under-arrow vs functions-taking-functions. Three orthogonal axes that share a word. - Tagless-final and MTL make whole programs polymorphic in the effect
F, with the concrete interpreter chosen at the edge — the architecture behind Cats-Effect, ZIO, and Haskell's effect libraries. - Rust deliberately omits HKTs for concrete reasons: coherence/inference complexity, lifetime-parameterized constructors, monomorphization interactions, and return-type-polymorphic
pure. The generalFunctortrait cannot be written in stable Rust. - GATs (Rust 1.65) are a partial, different step: they parameterize associated types (solving lending iterators), not the implementing type by a constructor. Useful, but not a
Functor. - Defunctionalization (
Kind<F, A>+ tags, fp-ts/Arrow/OCaml) soundly emulates HKTs but is verbose, leaks into errors, and threads instances by hand — native support is what makes HKTs ergonomic. - The senior call is a ledger, not a fashion: weigh reuse and capability unlocked against comprehension tax, language fit, and team fluency — per codebase.
Further Reading¶
- Lightweight Higher-Kinded Polymorphism — Yallop & White (ML 2014). The defunctionalization encoding. https://www.cl.cam.ac.uk/~jdy22/papers/lightweight-higher-kinded-polymorphism.pdf
- The Essence of the Iterator Pattern — Gibbons & Oliveira. Where
traverse/Traversableis dissected. https://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf - Finally Tagless, Partially Evaluated — Carette, Kiselyov, Shan. The tagless-final foundation. https://okmij.org/ftp/tagless-final/
- Rust RFCs and tracking issues on Generic Associated Types (rust-lang/rust #44265) and the long HKT discussion threads.
- Niko Matsakis, "Generic associated types to be stable in Rust 1.65" (Inside Rust blog) — what GATs do and explicitly do not do.
- Programming with Effects / ZIO and Cats-Effect documentation — effect-polymorphic architecture in practice.
- Thinking with Types — Sandy Maguire. Kinds,
PolyKinds, and type-level programming in Haskell. - Kotlin Arrow documentation and its retrospective on HKT emulation — a candid account of the ergonomics ceiling.
In this topic
- junior
- middle
- senior
- professional