Susan Potter

The superpowers that higher-kinded types provides

Wed March 3, 2021

Lately I have been working with TypeScript developers introducing stronger forms of functional programming and during our explorations about how to exploit the abstractions found in libraries like fp-ts and io-ts, I find myself missing higher-kinded types when one developer asked me to explain why higher-kinded types are so useful.

Here is my attempt.

Functional Programming as Programming with Values

We first get introduced to functional programming with the big idea that we can pass functions around as arguments or return functions as values from other functions.

The benefit, we are told in our functional programming thirst, is that functions compose and one of the big problems in software today is curbing complexity.

Anyone who has had to grow, evolve, or maintain software understands the appeal of this promise. We hope to eliminate all (or most of) that special application-level glue that needs to be specifically weaved in and out of our subroutines and checking errors along the way which makes our codebase brittle to changing requirements.

We embrace functional programming hoping to reap the rewards of this vision and we are told to programming everything with values.

Let's start with some basic types so we can construct simple term values:

import Prelude
import Data.Generic.Rep (class Generic)
import Data.Generic.Rep.Show (genericShow)

data AssetType = CommonStock | ETF | Bond | Cash

derive instance genericAssetType :: Generic AssetType _
instance showAssetType :: Show AssetType where
  show = genericShow

data Currency = USD | EUR | JPY | CHF | CNY | GBP

derive instance genericCurrency :: Generic Currency _
instance showCurrency :: Show Currency where
  show = genericShow

type Money numeric
  = { currency :: Currency
    , amount :: numeric
    }

type Asset numeric
  = { assetType :: AssetType
    , value :: Money numeric
    , note :: String
    }

assetType = Cash
value = { currency: CHF, amount: 250 }
note = "Left over cash from trip to Zurich"
asset = { assetType, value, note }

Above we have two sum types (AssetType, Currency) and two parameterized record types (Money, Asset). Next we have four term values: assetType, value, note, asset.

So far nothing dazzling yet we can see how quickly we can build data from smaller pieces to represent something from a business domain very quickly.

Let's continue on to defining functions.

getAssetType = _.assetType

-- we are using getAssetType in getAllAssetTypes
getAllAssetTypes
  :: forall numeric
   . Array (Asset numeric)
  -> Array AssetType
getAllAssetTypes = map getAssetType

groupAssets
  :: forall numeric
   . Array (Asset numeric)
  -> Map AssetType (Array (Asset numeric))
groupAssets = ?todo

With functions we notice that we can express relevant business logic again very quickly by building up from more simple building blocks (other functions).