Skip to content

Instantly share code, notes, and snippets.

@chrisdone
Last active April 10, 2022 07:26
Show Gist options
  • Save chrisdone/7dddadd089e6a5d2e3e9445c4692d2c2 to your computer and use it in GitHub Desktop.
Save chrisdone/7dddadd089e6a5d2e3e9445c4692d2c2 to your computer and use it in GitHub Desktop.
Defaulting fields in a record in Haskell

Defaulting fields in a record (via HKD)

Do you have 20+ fields of configuration for your kitchen sink API? This approach might be for you.

An approach to specifying many (required) fields, where some are defaulted. What you get:

  1. The ability to specify what the defaults are in a single place.
  2. That set of defaults is decoupled from the function that uses the record (separating concerns).
  3. The ability to choose different sets of defaults easily (e.g. "dev" mode vs "production" mode).
  4. Type-safety; you can't specify defaults for required fields as an API provider.
  5. Overriding: you can provide values instead of using the default trivially.
  6. Light-weight syntax.
  7. Failing to provide a required field as an API end-user results in a type error.

A field which has no sensible default value (such as one enabling a new feature like TLSSettings) can still be Maybe a.

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
-- | Providing defaults for fields in a record.
module Data.Defaults where
-- | Purpose of a data type.
data Purpose
= Defaults -- For specifying defaults.
| Complete -- For making a complete record.
-- | Required fields are not usable from a defaults spec.
type family Required (p :: Purpose) a where
Required 'Defaults a = () -- When we're defining defaults, required fields are ().
Required 'Complete a = a
{-# LANGUAGE DataKinds #-}
-- | My database API.
module DBAPI where
import Data.Defaults
data ConnSpec p = ConnSpec
{ username :: !(Required p String)
, password :: !(Required p String)
, port :: !Int -- Optional and therefore requires a default.
, host :: !String -- Optional and therefore requires a default.
}
connSpecDefaults :: ConnSpec Defaults
connSpecDefaults = ConnSpec {
-- Required fields are ()
username = (), password = (),
-- Defaulted fields need defaults specified
port = 5432, host = "localhost"
}
-- Example func.
connect :: ConnSpec Complete -> IO ()
connect _ = pure ()
-- | Usage of API.
module Main where
import DBAPI
demo :: ConnSpec 'Complete
demo = connSpecDefaults {username = "test", password = "mypw"}
-- Omitting either username or password triggers a type error.
main = connect connSpecDefaults {username = "test", password = "mypw"}
-- To override defaults, just specify the field e.g. port:
main2 = connect connSpecDefaults {username = "test", password = "mypw", port = 1234}
-- Thanks Aleksey Khudyakov (@pineapple_zombi) for pointing out that plain record
-- update has the same typing rules as RecordWildCards.
--
-- Old version was: ConnSpec{username="..",password="..",..} where ConnSpec{..} = connSpecDefaults
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment