これは Haskell Advent Calendar 12 日目の記事です。今日は 12/12+12 日です。
今回は、最近流行りの Higher-Kinded Data (HKD) について簡単に紹介したいと思います。
// 以前、某社の採用ビアバッシュ的イベントで、Haskell ほとんど知らない人の中で HKD の話をした際のスライドを公開し忘れていたので、せっかくなので多少手直しして公開します。
Haskell Day 2019 でも、@fumieval さんによる HKD のトークがあったので、そちらも参考にすると良いと思います。
https://twitter.com/fumieval/status/1193016480574500865
設定はモノイド
Haskell 界隈には設定はモノイドであるべきという一派があります。
次のようなプログラムの設定を表すデータ型を例にして考えます。
data Setting = Setting
{ verbose :: Bool
, host :: String
, port :: Int
, timeout :: Int
} deriving (Generic, Eq, Show)
このデータの入力元は、環境変数や設定ファイル、コマンドラインオプションなどが考えられます。この例だと項目が 4 つしか無いので、これらの情報源から Setting を構築するのはさほど大変ではないですが、項目が増えてくると面倒です。
Setting のモノイド版が定義できれば、モノイド版で合成してから Setting を構築することができるので便利そうです。
そこで Setting
型の全てのフィールドが Last で囲まれた SettingPartial
型を考えます。
data SettingPartial = SettingPartial
{ verbose_ :: Last Bool
, host_ :: Last String
, port_ :: Last Int
, timeout_ :: Last Int
} deriving (Generic, Eq, Show)
SettingPartial
は全てのフィールドが Last で包まれているので、Semigroup と Monoid のインスタンスを簡単に定義することができます。
instance Semigroup SettingPartial where
(<>) = mappenddefault
instance Monoid SettingPartial where
mempty = memptydefault
ここでは generic-deriving を使い、面倒な記述を減らしています。
次に、設定ファイルやコマンドライン引数のパーサーを Setting
のかわりに SettingPartial
を返すように実装し、デフォルト値を定義しておきます。
getSettingFromEnvironment :: IO SettingPartial
getSettingFromEnvironment = ...
getSettingFromFile :: IO SettingPartial
getSettingFromFile = ...
getSettingFromCommandLine :: IO SettingPartial
getSettingFromCommandLine = ...
defaultSetting :: SettingPartial
defaultSetting =
SettingPartial
{ verbose_ = Last (Just False)
, host_ = Last Nothing
, port_ = Last Nothing
, timeout_ = Last (Just 60)
}
後は Monoid の力でこれらを簡単に組み合わせることができます。
getSetting :: IO SettingPartial
getSetting = do
settingFromEnv <- getSettingFromEnvironment
settingFromFile <- getSettingFromFile
settingFromCmdLine <- getSettingFromCommandLine
return (defaultSetting <> settingFromEnv <> settingFromFile <> settingFromCmdLine)
Last で包んだおかげで、デフォルト値 < 環境変数 < 設定ファイル < コマンドライン引数の優先順位で設定が合成できています。
SettingPartial のままだと全てのフィールドが Last で包まれてしまっているので、プログラム本体から扱いやすいように SettingPartial を Setting に戻す必要があります。
validate :: SettingPartial -> Last Setting
validate s =
Setting <$> verbose_ s
<*> host_ s
<*> port_ s
<*> timeout_ s
各種の設定をモノイドにしておくことで、設定項目それぞれについて、複数の設定情報源がある場合についても一貫性のある形で設定値を合成することができました。
一方で、実際のプログラムではより多くの設定項目があるため、いちいち Setting と SettingPartial のように 2 つのデータ型を定義するのは面倒になってくるかもしれません。
また、各パーサーにおいては Last a
といった型よりも Maybe a
や Either err a
などといった型の方が便利な場合があり、SettingPartial
だけでは不足かもしれません。
そこで登場するのが Higher-Kinded Data です。
Higher-Kinded Data とは
先ほどの SettingPartial
の Last
を、型パラメータ f
として取る Setting_
型を考えてみましょう。
data Setting_ f = Setting_
{ verbose_ :: f Bool
, host_ :: f String
, port_ :: f Int
, timeout_ :: f Int
} deriving Generic
type SettingPartial = Setting_ Last
type Setting = Setting_ Identity
f は f String
のように使われているので、f の kind は * -> *
で、Setting_
の kind は (* -> *) -> *
であることがわかります。
元々の Setting の定義も、Identity を使って Setting_ Identity
としてしまえば、使う側で runIdentity する必要がありますが、重複した定義をする必要がなくなります。もちろん、Maybe 版が欲しければ Setting_ Maybe
とするだけです。
なるほど便利そうです。しかし、実際には一筋縄では行きません。
例えば、Setting_
と SettingPartial
を見比べると、 deriving 節から Show
, Eq
が消えていることが分かります。
試しに指定してみると、、
example.hs:110:26: error:
• No instance for (Show (f String))
arising from the second field of ‘Setting_’ (type ‘f String’)
Possible fix:
use a standalone 'deriving instance' declaration,
so you can specify the instance context yourself
• When deriving the instance for (Show (Setting_ f))
example.hs:110:32: error:
• No instance for (Eq (f String))
arising from the second field of ‘Setting_’ (type ‘f String’)
Possible fix:
use a standalone 'deriving instance' declaration,
so you can specify the instance context yourself
• When deriving the instance for (Eq (Setting_ f))
f String
や f Bool
が、Eq
や Show
のインスタンスと判定できないよと怒られてしまいました。
GHC のエラーメッセージは親切にも、 standalone な deriving instance 定義を使えばコンテキストを指定できるよと教えてくれているので、その通りにすれば Eq
, Show
を導出できます。
deriving instance (Show (f Bool), Show (f String), Show (f Int))
=> Show (Setting_ f)
deriving instance (Eq (f Bool), Eq (f String), Eq (f Int))
=> Eq (Setting_ f)
いや、これは面倒ですね。。
barbies
ですが安心してください。もちろん Haskell には barbies という便利なライブラリがあり、Generics の力によりボイラープレートを劇的に減らすことができます。
data Setting_ f = Setting_
{ verbose_ :: f Bool
, host_ :: f String
, port_ :: f Int
, timeout_ :: f Int
} deriving (Generic, FunctorB, TraversableB, ProductB, ConstraintsB, ProductBC)
deriving instance AllBF Show f Setting_ => Show (Setting_ f)
deriving instance AllBF Eq f Setting_ => Eq (Setting_ f)
前の定義と異なり、standalone deriving のコンテキストで各フィールドの型についての言及をするかわりに、AllBF Show f Setting_
などと書くだけで良くなっていることが分かります。
barbies の力はこれだけではありません。例えば、先ほどの validate :: SettingPartial -> Last Setting
の HKD 版は、barbies で定義されている bsequence'
を使うと、次のように書くことができます。
validate :: Applicative f => Setting_ f -> f (Setting_ Identity)
validate = bsequence'
bsequence' :: (Applicative f, TraversableB b) => b f -> f (b Identity)
なんと、実装が 1 行で済んでしまいました。というか名前を変えて、型 b
を Setting_
に制限しただけです。
他にも barbies には様々な便利関数があるので、いろいろ探してみると良いでしょう。
higgledy
HKD + barbies は便利そうですが、もう既に古き良きレコード型でプログラムを書いてしまったという人や、使う度に Identity をいちいち外すのは面倒という人も多いと思います。
そのような人も、 higgledy を使えば後からでも HKD の恩恵を得ることができます。
いちばん最初に定義した、古き良きレコード型の Setting
に立ち返ります。
data Setting = Setting
{ verbose :: Bool
, host :: String
, port :: Int
, timeout :: Int
} deriving (Generic, Eq, Show)
higgledy を使うと、これの HKD 版がタダで手に入ります。
type Setting_ = HKD Setting
この定義をするだけで、Setting_
は Show
, Eq
をはじめ、Monoid
や barbies の FunctorB
や TraversableB
などもタダで手に入ります。便利ですね。
一方で、値の構築や、フィールドのアクセスで Lens が必要など、若干面倒な部分もあります。
元の値から deconstruct する方法:
ghci> setting = Setting True "example.com" 8080 300
ghci> deconstruct setting :: Setting_ Maybe
Setting {verbose = Just True, host = Just "example.com", port = Just 8080, timeout = Just 300}
build を使う方法:
ghci> build @Setting (Just True) (Just "example.com") (Just 8080) (Just 300)
Setting_
から元の Setting へは、construct
を使えば戻すことができます。
ghci> construct example
Just (Setting {verbose = True, host = "example.com", timeout = 300})
フィールドのアクセスは Lens を使います。
ghci> example = build @Setting (Just True) (Just "example.com") (Just 8080) (Just 300)
ghci> example ^. field @"timeout"
Just 300
ghci> example & field @"port" .~ Just 3000
Setting {verbose = Just True, host = Just "example.com", port = Just 3000, timeout = Just 300}
また、内部構造が Linked List 相当の物になるので、パフォーマンスが非常にシビアな場所で使うと問題を起こすかもしれません。
試しに GHCi で :print してみると……
:print example
example = HKD (:*:
(:*: (Just True) (Just (: 'e' "xample.com")))
(:*:
(Just (ghc-prim-0.5.3:GHC.Types.I# 8080))
(Just (ghc-prim-0.5.3:GHC.Types.I# 300))))
しかし、これが実際に問題になる事は無いでしょう。というのも higgledy は元の古き良きレコード型に簡単に戻すことができるので、実際の処理では通常のレコード型を使えば良いからです。
まとめ
さわりだけの紹介でしたが、Higher-Kinded Data を使うことで、従来のレコード型で書いていた様々なボイラープレートコードを排除し、自由度を飛躍的に高めることができました。
なお言語拡張
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}
と
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DeriveAnyClass #-}
の提供でお送りしました。