はじめに
BIG MOON では、業務に必要なツールを自社開発しており、プログラミング言語に Haskell を採用しています。実用的に利用し始めて3年ぐらい?です。
僕らが Haskell を利用していて一番困った点はレコードの取り扱いです。
- 異なる型のフィールドラベルに同じ名前を付けたい
- フィールド全体対して関数を適用したい
- フィールド多相な関数を定義したい
このような問題に対して extensible という、(当初は謎に包まれていた) パッケージの利用を検討し、実際に既存のシステムを extensible で置き換えました。(当時アルバイトしていた matsubara0507 さんが居なければ実現不可能だったと思います)
今回、縁あって作者の fumieval さんと一緒に仕事できる機会に恵まれました。fumieval さんは簡単な質問でも、とても気さくに答えてくれます。
僕達のノウハウはまだまだとても少ないですが、この素晴らしいパッケージを広く知って欲しいと思い、まだまだ作成途中ではありますが extensible 攻略Wiki という親しみやすい雰囲気で情報を発信していくことになりました。
この wiki もまた Haskell で作られており apus という名前で公開されています。
今回の extensible-0.4.9 のアップデートは、攻略wiki のコンテンツを拡充していく中で出てきたアイデアや、関数などがいくつか追加されました。(matsubara0507 さんと弊社も色々と貢献できているはずです!)
今回はその内容について簡単な例とともに解説を行いたいと思います。
アップデート内容
type Person = Record
'[ "name" :> String
, "age" :> Int
]
person :: Person
person = #name @= "bigmoon"
<: #age @= 10
<: nil
以降の例では、上記の Person
型と person
変数が宣言されているものとします。
- MonadIO のインスタンスを一般化しました。
ベースモナドとして ResourceT IO などが使えるようになりました。
今までは Associate "IO" (ResourceT IO)
のように書けませんでしたが、こんな感じのコードが書けるようになりました。また、ResourceT IO 以外にも MonadIO のインスタンスであれば何でも指定可能です。
ここでは MonadResource のインスタンスを自分で定義しましたが、次回リリース (0.4.10) でライブラリに追加される予定?です。(たぶん)
type ExampleM = Eff '[ "IO" >: ResourceT IO ]
main :: IO ()
main = runResourceT . retractEff . runConduit $
bracketP (openFile "data.csv" ReadMode) hClose $ \handle ->
(sourceHandle handle :: ConduitT i ByteString ExampleM ()) .| stdoutC
instance (Associate "IO" (ResourceT IO) xs) => MonadResource (Eff xs) where
liftResourceT = liftEff (Proxy @ "IO")
ただ単に csv
ファイルを読み込んで表示するだけの例です。
-- data.csv
"bigmoon", 10, "watch"
"wado", 100, art
- 新しい制約コンビネータ And を追加しました。
このコンビネータを利用することで Forall の制約を二つ以上指定することができます。
以下は拡張可能レコードの値が Show かつ Typeable の両方を満たすという制約で hfoldMapFor 関数を使う例です。
debug :: Forall (ValueIs (And Show Typeable)) xs => Record xs -> IO ()
debug = hfoldMapFor c (print . fork id typeOf . view _Wrapper)
where
c = Proxy @ (ValueIs (And Show Typeable))
fork f g x = (f x, g x)
例として定義した debug 関数は与えられた拡張可能レコードの 値 と 型 を表示することができます。
- stringAssocKey 関数を追加しました。
この関数を使えば、拡張可能レコードのキーを文字列として取得することができます。
例えば、拡張可能レコードのキーを全て集めてリストにして返す関数は以下のように作ることができます。
keys :: (IsString key, Forall (KeyIs KnownSymbol) xs) => proxy xs -> [key]
keys xs = henumerateFor (Proxy @ (KeyIs KnownSymbol)) xs ((:) . stringAssocKey) []
IsString のインスタンスであれば何でも良いので、String に限らず Text, ByteString などを返すことができます。
*Main> mapM_ putStrLn $ keys person
name
age
*Main> mapM_ Data.Text.IO.putStrLn $ keys person
name
age
- prettyprinter パッケージの Pretty のインスタンスを追加しました。
prettyprinter パッケージについては過去のブログ記事で少し紹介しているので、興味ある方はそちらをご確認ください。
以下のような出力になるそうです。
[ name: DA-192H
weight: 260.0
price: 120
featured: True
description: High-quality (24bit 192kHz), lightweight portable DAC
quantity: 20
, name: HHP-150
weight: 200.0
price: 330
featured: False
description: Premium wooden headphone
quantity: 55 ]
現状は prettyprinter
側のバグ?で上手く表示されていないようですが、そのうち直ると思います。
*Main> pretty person
{ name: bigmoon; age: 10
*Main> pretty [person, person]
[{ name: bigmoon; age: 10 }, { name: bigmoon; age: 10 }]
- th-lift の Lift のインスタンスを追加しました。
Lift のインスタンスになったので例えば、Data.Yaml.TH モジュールの decodeFile 関数を使ってコンパイル時に yaml ファイルから一気に拡張可能レコードを作り上げることができます。
実行例:
- hmapWithIndexFor を追加しました。
hmapWithIndex の制約付きバージョンです。
例えば以下のようにして拡張可能レコードから aeson パッケージの Value をフィールドとして持つ拡張可能レコードに変換できます。
toJSONRecord :: Forall (ValueIs ToJSON) xs => Record xs -> RecordOf (Const' Value) xs
toJSONRecord = hmapWithIndexFor c $ \m ->
Field . Const' . toJSON . view _Wrapper
where c = Proxy @ (ValueIs ToJSON)
実行例:
*Main> person
name @= "bigmoon" <: age @= 10 <: nil
*Main> toJSONRecord person
name @= String "bigmoon" <: age @= Number 10.0 <: nil
- Const’ に Monoid のインスタンスを追加しました。
- Wrapper に Either e のインスタンスを追加しました。
まとめ
extensible パッケージは初見では全く使い方がわからないレベルで難しいですが、実際に使ってみると、今までリアルワールド Haskell っぽいコードだね。仕方ないね。と妥協していた部分がとても綺麗に書けるようになります。
extensible 攻略Wiki の内容はこれからもっと充実して行くので、気になる人はチェックしてみてください!