Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Type Generic Programming

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Anupam Anupam
August 01, 2026
13

Type Generic Programming

A talk given at the Functional Programming India Bangalore meetup, 1 Aug 2026.

A whirlwind tour of type generic programming in Haskell and PureScript. Motivates functional encodings to implement advanced type level features.

Avatar for Anupam

Anupam

August 01, 2026

More Decks by Anupam

Transcript

  1. Motivation data Company = Company (Array Dept) data Dept =

    Dept Name Manager (Array SubUnit) data SubUnit = EmpUnit Employee | DeptUnit Dept data Employee = Employee Person Salary data Person = Person Name Address data Salary = Salary Number Give everyone a raise increase �� (Salary �� Salary) �� Company �� Company 2 / 90
  2. Boilerplate increase k (Company ds) = Company (map (incD k)

    ds) incD k (Dept nm mgr us) = Dept nm (incE k mgr) (map (incU k) us) incU k (EmpUnit e) = EmpUnit (incE k e) incU k (DeptUnit d) = DeptUnit (incD k d) incE k (Employee p s) = Employee p (incS k s) incS k (Salary s) = Salary (k s) 3 / 90
  3. Boilerplate increase k (Company ds) = Company (map (incD k)

    ds) incD k (Dept nm mgr us) = Dept nm (incE k mgr) (map (incU k) us) incU k (EmpUnit e) = EmpUnit (incE k e) incU k (DeptUnit d) = DeptUnit (incD k d) incE k (Employee p s) = Employee p (incS k s) Six functions of plumbing… 4 / 90
  4. Boilerplate incS k (Salary s) = Salary (k s) …for

    one line that does the work. 5 / 90
  5. Ad hoc overloading? class MapSalary a where increase �� (Salary

    �� Salary) �� a �� a instance MapSalary Salary where increase f a = f a else instance MapSalary a where increase _ a = a Even more boilerplate 7 / 90
  6. In a less typed language increase �� forall a. (Salary

    �� Salary) �� a �� a increase f a | a `instanceOf` Salary = f a | otherwise �� `Salary` is a�� Type? = a instanceOf: forall a. a �� Type�� �� Boolean 8 / 90
  7. Proxy doesn't suffice data Proxy a = Proxy booleanType ��

    Proxy Boolean booleanType = Proxy instanceOf �� forall a b. a �� Proxy b �� Boolean instanceOf a Proxy = ��� 9 / 90
  8. Reify Types re•i•fy /ˈriː.ɪ.faɪ/ (transitive verb), reified, reifying To make

    (an abstraction) more real, concrete, or tangible. foreign data TypeRep typeRep �� forall a. Proxy a �� TypeRep instanceOf �� forall a. a �� TypeRep �� Boolean instanceOf a t = eqTypeRep (typeRep (Proxy �� _ a)) t 10 / 90
  9. API Contract foreign data TypeRep �� Opaque typeRep �� forall

    a. Proxy a �� TypeRep �� type of a �� type of b ��� typeRep a �� typeRep b eqTypeRep �� TypeRep �� TypeRep �� Boolean 11 / 90
  10. Manual representation data TypeRep = TInt | TChar | TArray

    TypeRep | TFunc TypeRep TypeRep eqTypeRep �� TypeRep �� TypeRep �� Boolean eqTypeRep TInt TInt = true eqTypeRep (TArray a) (TArray b) = eqTypeRep a b ��� Now there is something to compare at runtime. 12 / 90
  11. Creating TypeReps data TypeRep = ��� class Typeable a where

    typeRep �� Proxy a �� TypeRep instance Typeable Int where typeRep _ = TInt instance Typeable a �� Typeable (Array a) where typeRep _ = TArray (typeRep (Proxy �� _ a)) ��� 13 / 90
  12. Solution increase �� forall a. Typeable a �� (Salary ��

    Salary) �� a �� a increase f a | a `instanceOf` (typeRep (Proxy �� _ Salary)) = f a | otherwise = a 14 / 90
  13. Solution increase �� forall a. Typeable a �� (Salary ��

    Salary) �� a �� a increase f a | a `instanceOf` (typeRep (Proxy �� _ Salary)) = f a | otherwise = a Cannot match a with Salary 15 / 90
  14. Solution increase �� forall a. Typeable a �� (Salary ��

    Salary) �� a �� a increase f a | a `instanceOf` (typeRep (Proxy �� _ Salary)) = f (coerceToSalary a) | otherwise = a coerceToSalary �� forall a. a → Salary coerceToSalary = unsafeCoerce 16 / 90
  15. What went wrong increase �� forall a. Typeable a ��

    (Salary �� Salary) �� a �� a increase f a | a `instanceOf` (typeRep (Proxy �� _ Salary)) = f (coerceToSalary a) | otherwise = a coerceToSalary �� forall a. a → Salary coerceToSalary = TRUST ME BRO I WAS THERE We checked the TypeRep at runtime. We know a is a Salary . How do we bear witness to the compiler? 17 / 90
  16. Prove, Don't Validate A check that returns Boolean throws its

    evidence away. Return the proof instead. The most useful proof that a is a Salary is a function a �� Salary . increase �� forall a. Typeable a �� (Salary �� Salary) �� a �� a increase f a = case a `instanceOf` (typeRep (Proxy �� _ Salary)) of Just coerce �� f (coerce a) Nothing �� a 18 / 90
  17. But instanceOf �� forall a. a �� TypeRep �� Maybe

    (a �� ���) If we don't constrain the return type then instanceOf �� forall a b. a �� TypeRep �� Maybe (a �� b) let Just coerce = "hello" `instanceOf` (typeRep (Proxy �� _ String)) coerce "hello" �� Int �� proof about String, used to make an Int! b is unconstrained, so the caller picks it. Nothing can implement this. 19 / 90
  18. Indexed TypeReps foreign data TypeRep a typeRep �� forall a.

    Proxy a �� TypeRep a instanceOf �� forall a b. a �� TypeRep b �� Maybe (a ~ b) instanceOf a t = eqTypeRep (typeRep (Proxy �� _ a)) t TypeRep a can only represent the typerep for the type it reifies. a ~ b is a value witnessing that a and b are the same type coerce �� forall a b. (a ~ b) �� a �� b �� all we need for now Haskell calls it a :~: b . We will build it in the next section. 20 / 90
  19. Recover unindexed representation �� Haskell GADTs data SomeTypeRep where SomeTypeRep

    �� forall a. TypeRep a �� SomeTypeRep �� PureScript type SomeTypeRep = Exists TypeRep �� Conceptually stored as, but really unsafeCoerced for efficiency type Exists f = forall r. (forall a. f a �� r) �� r mkExists �� forall f a. f a �� Exists f runExists �� forall f r. (forall a. f a �� r) �� Exists f �� r The type variable a is now Existential 21 / 90
  20. Haskell: Manual Representation requires GADTs data TypeRep a where TInt

    �� TypeRep Int TChar �� TypeRep Char TArray �� TypeRep a �� TypeRep (Array a) TFunc �� TypeRep b �� TypeRep c �� TypeRep (Function b c) TTuple �� TypeRep a �� TypeRep b �� TypeRep (Tuple a b) eqTypeRep �� forall a b. TypeRep a �� TypeRep b �� Maybe (a :~: b) eqTypeRep TInt TInt = ��� ��� 22 / 90
  21. PureScript: Replace GADTs With Type Coercion No GADTs. Evidence becomes

    an explicit field data TypeRep a = TInt (Int ~ a) | TChar (Char ~ a) | TArray (TypeRep b) (a ~ Array b) | TFunc (TypeRep b) (TypeRep c) (a ~ Function b c) | TTuple (TypeRep b) (TypeRep c) (a ~ Tuple b c) eqTypeRep �� forall a b. TypeRep a �� TypeRep b �� Maybe (a ~ b) eqTypeRep (TInt ia) (TInt ib) = ��� ��� The compiler no longer infers the equality. You apply it. ~ again! Simplified: b and c are existential; the real constructors wrap them in Exists . 23 / 90
  22. Haskell: ~ is primitive f �� (a ~ Int) ��

    a �� Int f x = x + 1 Part of the type system, not a library. data a :~: b where Refl �� a :~: a coerce �� (a :~: b) �� a �� b coerce Refl x = x �� inside this branch, `a` and `b` are the same type 24 / 90
  23. Haskell: the same trick, without GADT syntax :~: is a

    value, so it fits in a field. No GADT needed data TypeRep a = TInt (Int :~: a) | TChar (Char :~: a) | forall b. TArray (TypeRep b) (Array b :~: a) | forall b c. TFunc (TypeRep b) (TypeRep c) (Function b c :~: a) | forall b c. TTuple (TypeRep b) (TypeRep c) (Tuple b c :~: a) useAsInt �� TypeRep a �� a �� Int useAsInt (TInt proof) n = coerce (symm proof) n + 1 25 / 90
  24. PureScript: Type coercion is Isomorphism data Same a b =

    Same (a �� b) (b �� a) instance Category Same where identity �� forall a. Same a a identity = Same identity identity (���) �� forall a b c. Same a b �� Same b c �� Same a c (���) (Same ab ba) (Same bc cb) = Same (ab ��� bc) (cb ��� ba) 26 / 90
  25. Constructing Proofs of type Isomorphism refl �� forall a. Same

    a a refl = identity symm �� forall a b. Same a b �� Same b a symm (Same a b) = Same b a trans �� forall a b c. Same a b �� Same b c �� Same a c trans = (���) 27 / 90
  26. Extracting type witnesses from type reps eqTypeRep (TInt (Same ia

    ai)) (TInt (Same ib bi)) = Just (Same ab ba) where ab a = ib (ai a) ba b = ia (bi b) 28 / 90
  27. Extracting type witnesses from type reps OR equivalently eqTypeRep (TInt

    tia) (TInt tib) = Just (symm tia ��� tib) 29 / 90
  28. Constructing TypeReps typeRep �� forall a. TypeRep a �� Still

    a class. The type selects the instance, the method delivers a value class Typeable a where typeRep �� TypeRep a instance Typeable Int where typeRep = TInt identity instance Typeable a �� Typeable (Array a) where typeRep = TArray typeRep identity Each constructor carries identity , which is reflexivity. Haskell: since GHC 7.10 the solver supplies Typeable for every type; you cannot write one. PureScript: you write them all. 30 / 90
  29. Solution increase �� forall a. Typeable a �� (Salary ��

    Salary) �� a �� a increase f a = case a `instanceOf` typeRep of Just (Same coercion _) �� f (coercion a) Nothing �� a 31 / 90
  30. Alternate API, Dynamic cast increase �� forall a. Typeable a

    �� (Salary �� Salary) �� a �� a increase f a = case cast a of Just n �� f n Nothing �� a cast �� forall a b. Typeable a �� Typeable b �� a �� Maybe b cast a = case a `instanceOf` (typeRep �� _ b) of Nothing �� Nothing Just (Same ab _) �� Just (ab a) 32 / 90
  31. Leibniz Equality Equal if either substitutes for the other in

    any context newtype Leibniz a b = Leibniz (forall f. f a �� f b) infix 4 type Leibniz as ~ runLeibniz �� forall f a b. (a ~ b) �� f a �� f b runLeibniz (Leibniz f) = f 33 / 90
  32. Easier to work with instance Category Leibniz where identity ��

    forall a. Leibniz a a identity = Leibniz identity (���) �� forall a b c. Leibniz a b �� Leibniz b c �� Leibniz a c Leibniz ab ��� Leibniz bc = Leibniz (ab ��� bc) Why bother, when Same already worked? coerceArray �� forall a b. Same a b �� Array a �� Array b coerceArray (Same ab _) = map ab �� O(n), allocates coerceArray �� forall a b. (a ~ b) �� Array a �� Array b coerceArray w = runLeibniz w �� free: pick f = Array 34 / 90
  33. The same three operations refl �� forall a. Leibniz a

    a refl = Leibniz identity symm �� forall a b. Leibniz a b �� Leibniz b a symm w = unFlip (runLeibniz w (Flip identity)) �� f = Flip a trans �� forall a b c. Leibniz a b �� Leibniz b c �� Leibniz a c trans (Leibniz ab) (Leibniz bc) = Leibniz (ab ��� bc) newtype Flip a b = Flip (Leibniz b a) 35 / 90
  34. Isomorphic to Isomorphism Leibniz is at least as strong as

    Same sameLeib �� forall a b. Leibniz a b �� Same a b sameLeib (Leibniz f) = Same ab ba where ab �� a �� b ab a = un Identity (f (wrap a)) �� f = Identity g �� forall h. h b �� h a �� symmetry, via Symm a g = runLeibniz (un Symm (f (Symm refl))) ba �� b �� a ba b = un Identity (g (wrap b)) newtype Symm a b = Symm (Leibniz b a) Isomorphic. But Leibniz composes for free. 36 / 90
  35. Cast Using Leibniz cast �� forall a b. Typeable a

    �� Typeable b �� a �� Maybe b cast a = eqTypeRep (typeRep �� _ a) (typeRep �� _ b) �� �� Maybe (Leibniz a b) # map \w �� runLeibniz w (Identity a) �� �� Maybe (Identity b) # map unwrap �� �� Maybe b 37 / 90
  36. Back to the boilerplate increase k (Company ds) = Company

    (map (incD k) ds) incD k (Dept nm mgr us) = Dept nm (incE k mgr) (map (incU k) us) incU k (EmpUnit e) = EmpUnit (incE k e) incU k (DeptUnit d) = DeptUnit (incD k d) incE k (Employee p s) = Employee p (incS k s) incS k (Salary s) = Salary (k s) We have Typeable . We still have six functions. 38 / 90
  37. Typeable only sees the surface increase �� forall a. Typeable

    a �� (Salary �� Salary) �� a �� a increase f a = case cast a of Just s �� f s Nothing �� a increase on a Company asks one question. Is this a Salary ? No. Done. The Dept s, Employee s and Salary s inside are never looked at. 39 / 90
  38. What we actually need A magic increase that works on

    any data we know how to recurse into. Call that Recursable . increase �� forall anything. Recursable anything �� (Salary �� Salary) �� anything �� anything Now that we have TypeRep , we can write it. 40 / 90
  39. Haskell calls it Data increase �� forall anything. Data anything

    �� (Salary �� Salary) �� anything �� anything And the function that does the recursing is everywhere everywhere �� forall a. Data a �� (forall b. Data b �� b �� b) �� a �� a 41 / 90
  40. Haskell: Generic Traversals Scrap your boilerplate class Typeable a ��

    Data a where gmapT �� (forall b. Data b �� b �� b) �� a �� a instance Data Employee where gmapT f (Employee per sal) = Employee (f per) (f sal) instance Data Boolean where gmapT f x = x instance Data a �� Data (Array a) where gmapT f xs = map f xs 42 / 90
  41. Why the argument must be rank-2 class Typeable a ��

    Data a where gmapT �� (forall b. Data b �� b �� b) �� a �� a Employee holds a Person and a Salary gmapT f (Employee per sal) = Employee (f per) (f sal) �� ^^^^^ ^^^^^ �� f �� Person �� Person �� f �� Salary �� Salary A rank-1 (b �� b) fixes one b at the call site. f has to stay polymorphic inside gmapT . 43 / 90
  42. Our one level increase function refactored a bit increase f

    a = case cast a of Just s �� f s Nothing �� a Abstract the function out and it works for any type. Call it mkT mkT �� forall a b. Typeable a �� Typeable b �� (b �� b) �� a �� a mkT f = case cast f of Just g �� g Nothing �� identity raiseSalary �� (Salary �� Salary) �� (forall b. Data b �� b �� b) raiseSalary k = mkT k Data only because that's the shape everywhere wants. It works because Data is a subclass of Typeable class Typeable a �� Data a 44 / 90
  43. Why both a and b must have the constraint class

    Typeable a �� Data a where gmapT �� (forall b. Data b �� b �� b) �� a �� a �� ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^ �� b: so f can recurse a: so we have gmapT at all a is the class parameter, so gmapT only exists for types we can take apart. b is the child, and everywhere hands gmapT a function that recurses everywhere f x = gmapT (everywhere f) (f x) �� ^^^^^^^^^^^^^^ needs `Data` on whatever it touches Without Data b , f could change a child but never descend into it. 45 / 90
  44. Tying the knot everywhere �� forall a. Data a ��

    (forall b. Data b �� b �� b) �� a �� a everywhere f x = gmapT (everywhere f) (f x) everywhere f (Employee per sal) = gmapT (everywhere f) (f (Employee per sal)) | | | +�� apply f to the node itself +�� then recurse into each child = Employee (everywhere f per) (everywhere f sal) 46 / 90
  45. No boilerplate everywhere �� forall a. Data a �� (forall

    b. Data b �� b �� b) �� a �� a everywhere f x = gmapT (everywhere f) (f x) increase �� forall anything. Data anything �� (Salary �� Salary) �� anything �� anything increase k = everywhere (raiseSalary k) One line, replacing the six we started with, for any type. 47 / 90
  46. One primitive, not many class Typeable a �� Data a

    where gfoldl �� forall c . (forall d b. Data d �� c (d �� b) �� d �� c b) �� apply to a child �� (forall g. g �� c g) �� lift a value �� a �� c a instance Data a �� Data (Maybe a) where gfoldl k z Nothing = z Nothing gfoldl k z (Just a) = z Just `k` a 48 / 90
  47. PureScript: no constraints under a forall gfoldl 's first argument

    is a rank-2 type carrying a constraint class Typeable a �� Data a where gfoldl �� (forall d b. Data d �� c (d �� b) �� d �� c b) �� ��� �� ^^^^^^^^ a constraint inside a higher-rank argument PureScript won't accept that. Move the method into a newtype newtype DataDict a = DataDict ( forall c . (forall d b. Data d �� c (d �� b) �� d �� c b) �� (forall g. g �� c g) �� a �� c a ) class Typeable a �� Data a where dataDict �� DataDict a instance Data (Maybe a) where dataDict = DataDict \k z m �� case m of Nothing �� z Nothing Just a �� z Just `k` a 49 / 90
  48. Dynamic Values data Dynamic' t a = Dynamic' (TypeRep a)

    (t a) data Dynamic t = Dynamic (Exists (Dynamic' t)) dynamic �� forall t a. Typeable a �� t a �� Dynamic t dynamic a = Dynamic (mkExists (Dynamic' typeRep a)) unwrapDynamic �� forall t a. TypeRep a �� Dynamic t �� Maybe (t a) unwrapDynamic ta (Dynamic e) = e # runExists \(Dynamic' ti v) �� map (\w �� runLeibniz w v) (eqTypeRep ti ta) Pack a value with its type, recover it only for the matching type. Dynamic typing, safely, inside a static language. 51 / 90
  49. De/Serialisation serialiseTypeRep �� forall a. TypeRep a �� String serialiseTypeRep

    (TInt _) = "TypeInt" ��� deserialiseTypeRep �� String �� Maybe SomeTypeRep deserialiseTypeRep "TypeInt" = Just (mkExists (TInt identity)) ��� Deserialising can't know the type in advance, so it returns SomeTypeRep , an existential. 52 / 90
  50. De/Serialisation data Expr = Expr String (Array Expr) type Serialiser

    a = a �� Expr type Deserialiser r = forall a. TypeRep a �� Expr �� Either Err a 53 / 90
  51. De/Serialisation serialise (arr �� Array Int) = Expr "Array" (Expr

    (show (typeRep �� _ a)) [] : map serialise arr) serialise ([1, 2, 3] �� Array Int) = Expr "Array" [Expr "TypeInt" [], Expr "1" [], ���] 54 / 90
  52. De/Serialisation type Deserialiser r = forall a. TypeRep a ��

    Expr �� Either Err a deserialiseArray �� forall a. TypeRep a �� Expr �� Either Error (Array a) deserialiseArray aTyp (Expr "Array" (Expr typstr [] : rest)) | aTyp �� deserialiseTypeRep typstr = traverse (deserialise aTyp) rest | otherwise = Left $ "Expected type: " �� show aTyp �� ", found " �� show typstr The wire format is checked against the expected type before a single element is decoded. 55 / 90
  53. Manual Representation Is Unsatisfactory data TypeRep a = TInt ���

    | TChar ��� 1. Unsafe. Nothing stops you writing instance Typeable Foo where typeRep = TInt ��� 2. Closed sum. For our example we'd need to add | TEmployee (TypeRep Person) (TypeRep Salary) ��� …to the library, for every user type. 57 / 90
  54. Opaque Representation TypeRep goes opaque, and all Typeable instances come

    from the library. instance Tagged Employee where tag = makeTag unit instance Tagged Person where tag = makeTag unit instance Tagged Salary where tag = makeTag unit All of these are compile errors instance Tagged (Employee p s) where ��� instance Tagged Employee where tag = somethingElse instance Typeable Employee where ��� • Instances only for tags, so they are unique • No way to provide an invalid tag • No user-defined Typeable instances 58 / 90
  55. One class for every arity class Typeable a where typeRep

    �� TypeRep a What kind is a ? • Int has kind Type • Array has kind Type �� Type • Either has kind Type �� Type �� Type A monokinded class covers only one of these. 60 / 90
  56. Both: forall k. data TypeRep �� forall k. k ��

    Type data TypeRep a class Typeable �� forall k. k �� Constraint class Typeable a where typeRep �� TypeRep a One class, every arity: Typeable Int , Typeable Array , Typeable Either . PolyKinds in both. Haskell since GHC 7.4, PureScript since 0.14. 61 / 90
  57. Haskell: the compiler knows typeRep @Int �� Int typeRepFingerprint (typeRep

    @Int) �� 0x8bfd0e2b3f2c9f4e��� GHC assigns every type constructor a fingerprint derived from package:Module.Name . Stable across runs, unforgeable, generated for free. You cannot write a wrong Typeable instance because you cannot write one. 62 / 90
  58. PureScript: mint identity by hand Opaque type with equality foreign

    import data Tag �� forall k. k �� Type foreign import makeTag �� forall t. Unit �� Tag t function Tag() { } �� compared with ��� export function makeTag(_) { return new Tag(); �� fresh, unequal to all others } The Unit argument is not decoration. It forces a call foreign import makeTag �� forall t. Tag t �� ✗ one shared value foreign import makeTag �� forall t. Unit �� Tag t �� ✓ called per instance Without it every instance shares the one Tag the module made at load time, so every type compares equal to every other. 63 / 90
  59. Tags User Defined Instances class Tagged �� forall k. k

    �� Constraint class Tagged a where tag �� Tag a instance Tagged Int where tag = makeTag unit instance Tagged Array where tag = makeTag unit ��� 64 / 90
  60. Tag chaining A tagged constructor applied to a typeable argument

    yields another tag instance (Tagged t, Typeable a) �� Tagged (t a) where tag = tagFromTag foreign import tagFromTag �� forall t a. Tagged t �� Typeable a �� Tag (t a) It looks argument-less, but the two constraints are the arguments, passed as dictionaries tagFromTag (tag �� Tag t) (typeRep �� TypeRep a) �� Tag (t a) �� a tag for `t a` pairs the two; eqTypeRep compares pairs structurally export const tagFromTag = (tagT) �� (repA) �� [tagT, repA]; 65 / 90
  61. Tag chaining Tag (A …) → Typeable B → Tag

    (A … B) Tagged Array ⇒ Tag Array Tagged Array + Typeable Int ⇒ Tag (Array Int) Tagged Either ⇒ Tag Either Tagged Either + Typeable String ⇒ Tag (Either String) ⇒ Tag (Either String Int) + Typeable Int (user-written) (user-written) 66 / 90
  62. Deriving it for your own types data Optional a =

    Some a | None instance Tagged Optional where tag = makeTag unit One mechanical line, any arity. Typeable (Optional Int) and Typeable Optional both come free. 67 / 90
  63. Typeable Chaining instance (Tagged t, Typeable a) �� Typeable (t

    a) where typeRep = typeRepFromTag1 else instance Tagged t �� Typeable t where typeRep = typeRepDefault0 foreign import typeRepDefault0 �� forall a. Tagged a �� TypeRep a Tag A → Typeable A Tag (A …) → Typeable B → Typeable (A … B) 68 / 90
  64. The sharp edge instance Tagged Person where tag = makeTag

    unit �� instance Foo �� Tagged Person where tag = makeTag unit �� silently broken A constrained instance is re-evaluated per call site, minting a different tag each time. Two TypeRep Person values then compare unequal. The type checker cannot catch this. 69 / 90
  65. Typeable for Row Types PureScript records are structural, not nominal

    �� Haskell: a record IS a data type, and gets a fingerprint data Person = Person { name �� String } In Haskell there's a constructor, so it gets a fingerprint like anything else. 70 / 90
  66. Typeable for Row Types �� PureScript: a row, with no

    constructor to tag type Person = { name �� String } makeTag needs something to attach to. A row has nothing, so the rep is built field by field. 71 / 90
  67. Typeable for Row Types data TypeRow r �� A ��

    A convert a finished row foreign import typeRowToTypeRep �� RL.RowToList r rl �� TypeRow rl �� TypeRep (Record r) �� [] the empty row foreign import typeRowNil �� TypeRow RL.Nil �� S �� T �� R �� (S,T):R cons one field on foreign import typeRowCons �� Proxy s �� String �� TypeRep t �� TypeRow rs �� TypeRow (RL.Cons s t rs) 72 / 90
  68. Typeable for Row Types instance TypeableRecordFields (RL.Cons key focus rowlistTail)

    where typeableRecordFields _ = typeRowCons key (reflectSymbol key) (typeRep �� _ focus) tail where key = SProxy �� _ key tail = typeableRecordFields (RLProxy �� _ rowlistTail) Recursive case, one field then the rest. A type-level fold, resolved by instance search. 74 / 90
  69. Typeable for Row Types instance (RL.RowToList rs ls, TypeableRecordFields ls)

    �� Typeable (Record rs) where typeRep = typeRowToTypeRep (typeableRecordFields (RLProxy �� _ ls)) It slots into the same instance chain, ahead of the tag-based cases else instance (Tagged t, Typeable a) �� Typeable (t a) where ��� else instance Tagged t �� Typeable t where ��� 75 / 90
  70. Haskell vs PureScript features Need Haskell PureScript Typed rep GADT

    explicit ~ field Type equality ~ / :~: Leibniz + rank-2 Type identity fingerprints makeTag + FFI Record identity nominal, free RowToList + TypeRow Hide a type existentials Exists + rank-2 Ordered instances OVERLAPPING instance chains 76 / 90
  71. Takeaway Functional encodings are powerful Many advanced type features can

    be built with them. GADTs, type equality, TypeRep , existentials and generic traversals are all built into Haskell. PureScript has none of them. We built them all as patterns or libraries. And for everything else, there's the FFI 77 / 90