Safe Haskell | Safe |
---|---|
Language | Haskell2010 |
When I use Data.Aeson library to decode a json string into a Haskell value,
I want to provide default values for Maybe
fields, which will be Nothing
when these fields are omitted in the json string. It's a hard work when there
are lots of Maybe
fields or deeply nested fields, it will also make your code
hard to read and to maintain. This module provides a solution by using
Higher-kinded data (HKD).
See this blog
for more information about HKD
.
For example, if you have a Config
type as follows,
data Config = Config { dbHost :: String , dbPort :: Int , dbName :: String ... }
and you want to read these configuration data from a json file when you start
you application, you instantiate Data.Aeson.FromJSON for the Config
,
data Config = Config { dbHost :: String , dbPort :: Int , dbName :: String ... } deriving Generic instance FromJSON Config
and you want dbPort
can be omitted in the json string, a default value will be
used when it is omitted, you change String
type to Maybe String
,
data Config = Config { dbHost :: String , dbPort :: Maybe Int , dbName :: String ... } deriving Generic instance FromJSON Config
and decode and use the configuration data in main function as below,
main = do config <- fromJust <$> decodeFileStrict "./config.json" let host = dbHost config port = fromMaybe defaultDBPort $ dbPort config ... dbConn <- connectDB host port ... ...
it is neither elegant nor easy to maintain when you have lots of configuration items.
By using HKD
and type family
, it becomes easier to maintain your code.
type family HKD f a where HKD Identity a = a HKD f a = f a data ConfigH f = Config { dbHost = String , dbPort = HKD f String ... } deriving Generic instance Default ConfigH instance FromJSON (ConfigH Maybe) type Config = Config Identity instance FromJSON Config where parseJSON v = applyDef def $ parseJSON v where def = Config undefined 3306 ... main = do -- Enable RecordWildCards extension Config{..} <- fromJust <$> decodeFileStrict "./config.json" dbConn <- connectDB dbHost dbPort ...
More Examples
>>>
:set -XDeriveGeneric
>>>
:set -XFlexibleInstances
>>>
:set -XStandaloneDeriving
>>>
import Data.Functor.Identity
>>>
import GHC.Generics
>>>
import HKD.Default
>>>
:{
data Triple f = Triple String (f Int) (f Double) deriving Generic instance Default Triple deriving instance Show (Triple Identity) :} ...>>>
let def = Triple "hello" (Identity 123) pi :: Triple Identity
>>>
applyDef def $ Triple "world" (Just 456) Nothing
Triple "world" (Identity 456) (Identity 3.141592653589793)
>>>
:set -XDeriveGeneric
>>>
:set -XStandaloneDeriving
>>>
:set -XFlexibleInstances
>>>
:set -XOverloadedStrings
>>>
import Data.Aeson
>>>
import Data.Functor.Identity
>>>
import GHC.Generics
>>>
import HKD.Default
>>>
:{
data Name f = Name { first :: f String , last_ :: f String } deriving Generic instance Default Name deriving instance Show (Name Identity) instance FromJSON (Name Maybe) data Person f = Person { name :: Name f -- name is required , age :: f Int -- age is optional (can be omitted) } deriving Generic instance Default Person deriving instance Show (Person Identity) instance FromJSON (Person Maybe) instance FromJSON (Person Identity) where parseJSON v = applyDef def <$> parseJSON v where def = Person (Name (Identity "Jorah") (Identity "Gao")) (Identity 28) :}>>>
decode "{\"name\": {}}" :: Maybe (Person Identity)
Just (Person {name = Name {first = Identity "Jorah", last_ = Identity "Gao"}, age = Identity 28})>>>
decode "{}" :: Maybe (Person Identity)
Nothing