Haskell's Kind system explained, from types to higher-Kinded data

A post on thunk.blog explains Haskell's Kind system. The author says it grew out of adding typed configuration to his Haskell Redis server clone, the redis-haskell project, where he picked up several techniques that all stem from Kinds.

The explanation builds up in layers. Values (also called terms) are things like 1, True or "hello". Types group related values, and the author's main argument for them is abstraction: a type tells both the programmer and the compiler what operations a value allows. He compares it to weapon classes in an RPG, where the class dictates which attacks and skills you can use. Kinds come next: "Kinds are to types what types are to values." They classify everything at the type level. Regular types have the Kind Type. Haskell also has type-level numbers (Kind Nat), type-level strings (Kind Symbol) and constraints from typeclasses (Kind Constraint). The Show typeclass has Kind Type -> Constraint, and Show Int has Kind Constraint. Type constructors such as Maybe, Either, Last, Proxy and Dual get their Kind from their arity and the Kinds of their parameters. Regular types are technically type constructors with no parameters, sometimes called "nullary type constructors". The author also notes that older docs and some GHC error messages write * where he writes Type; he says that, as far as he can tell, the two are synonyms, that * is the older name, and that GHC plans to deprecate it, with -Wall already warning on its use.

A Kind names a collection of type-level constructs, much as a function signature names a set of functions. Type -> Type covers constructors that take one regular type, such as Maybe, and partial applications such as Either String. Type -> Type -> Type covers constructors that take two. Just as (Int -> Int) -> Bool describes a higher-order function, a Kind like (Type -> Type) -> Type, or (Type -> Type) -> (Type -> Type) -> Type, describes a higher-Kinded type (HKT). His small example is data Wrap (f :: Type -> Type) = Wrap (f Int), whose Kind is (Type -> Type) -> Type; Wrap Maybe and Wrap [] are regular types. He also offers another way to see it: if parameterized types are what other languages call generics, HKTs are generics that can take other generics as arguments.

HKTs matter most together with typeclasses. Functor has Kind (Type -> Type) -> Constraint. It is not an HKT itself, since its Kind ends in Constraint rather than Type, but its parameter is a type constructor, which makes it higher-Kinded. Because the instance is defined on Maybe and not on Maybe a, one Functor instance lets fmap work on Maybe Int, Maybe String, Maybe Bool and every other specialization. The author illustrates this with a microwave-safe bowl: the label belongs to the bowl and applies whatever food is inside.

Higher-Kinded data (HKD) is a pattern where a data type takes one or more type constructors as parameters and uses them to wrap some or all of its fields. His example is a Profile record with age, name, profession and address fields, each of type f applied to a value type. With f = Identity every field must be present; with f = Maybe any field can be missing, like a half-filled sign-up form; with f = Const String every field holds a String, like a form label.

He then applies this to redis-haskell, where he added typed Redis configuration sourced from command-line options and from a redis.conf file. The problem was duplication. A default config needs every field present, with official default values, while command-line and redis.conf configs need optional fields. His first plan was two record types, RedisConfig with bare fields (a directory path, a filename path, useRDBCompression, genRdbChecksum, port) and PartialRedisConfig with each field wrapped in Last. He disliked it: he wanted a single source of truth for config field names and types, and with two types it would be easy to change one and forget the other. The type system might catch the mismatch, but he judged that not good enough. Wrapping fields in Last also has a catch: the combining operator <> needs a Semigroup instance for each wrapped type, and Bool and Int have none. Even for types that do, <> would combine two values rather than pick one, whereas Last keeps the latest value that is present, so later sources override earlier ones.

A second option was one config type with every field wrapped in Last. Since Last keeps the right-most Just value, defaults <> confFileOpts <> cliOpts would give command-line options priority, then redis.conf options, then defaults, which is exactly the semantics he wanted. The drawback was that the final result would still be a record with every field wrapped in Last. The available text ends there, before the author's final HKD-based solution.

Key facts

  • The post defines Kinds as classifying types the way types classify values: regular types have Kind Type, and Show has Kind Type -> Constraint.
  • Higher-Kinded types are type constructors whose Kind takes a type constructor as a parameter, such as Wrap with Kind (Type -> Type) -> Type.
  • Higher-Kinded data wraps record fields in a type constructor parameter; the Profile example uses Identity, Maybe and Const String as that parameter.
  • The motivating case is typed configuration for the author's redis-haskell project, where defaults <> confFileOpts <> cliOpts with Last-wrapped fields gives command-line options priority over redis.conf, then defaults.
  • The author rejected two separate config record types because he wanted a single source of truth for field names and types.

Why it matters

Higher-Kinded types and higher-Kinded data are often called advanced; the author argues the HKD pattern is straightforward once HKTs make sense. The post builds that understanding from values to types to Kinds and ties it to a real problem: avoiding two parallel config record types that could drift apart. It is a teaching piece, not a release or a research result.

Who it affects

Haskell programmers who know basic types and typeclasses and want to understand Kinds, higher-Kinded types and the HKD pattern. The Redis configuration example is aimed at anyone who needs to merge settings from several sources, such as defaults, a config file and command-line flags.

How to use it

The post offers patterns rather than a tool. For a record that must appear in a full form and a partial form, parameterize it by a type constructor f and wrap fields in f. Use f = Identity for the complete version and f = Maybe for one where fields may be missing. For layered config sources, wrapping fields in Last lets later sources override earlier ones, so defaults <> confFileOpts <> cliOpts gives command-line options priority. The author's actual redis-haskell implementation is in a PR he links; its contents are not in the available text.

How solid is it

This is a personal blog post by an author whose name is not given in the available text. Its claims are explanations of language concepts, and the author hedges the one uncertain point himself ("AFAICT" on * and Type being synonyms). The statement that GHC plans to deprecate * is given without a source or version. The available text ends before the author's final HKD-based solution, so the payoff of the Redis example is not covered here.

Risks and caveats

The text cuts off before the final solution, so conclusions about how the HKD approach turned out in redis-haskell cannot be drawn from it. The table of Kinds the author refers to is not reproduced in the available text. The deprecation of * is described as a plan with no timescale. No performance or adoption figures are given for any of the patterns.

“Kinds are to types what types are to values.”

— the post's author, thunk.blog