HaskellでBuilderパターンをやってMaybeをなるべく除去したい(型引数が複数ある場合)
- 単一項ならbarbiesが使える
- 複数項ならボイラープレートを書いていくしかない
- Kindを潰す方法を思い出すのに時間がかかった
以下は悩んでた時のメモと、悩んだ人向けのインデックスです。
Maybeをなるべく消したい
Haskellで、 RustのちょっとやりすぎなBuilderパターン | κeenのHappy Hacκing Blog のようなことをしようとしました。
HaskellではKind概念があるからHKDで楽勝では?
import Data.Functor.Identity
data Foo haveHoge
= Foo
{ hoge :: haveHoge Int
, huga :: Int
} deriving Show
type MaybeHaveHogeFoo = Foo Maybe
maybeHaveHogeFoo = Foo Nothing 1
type IdentityHaveHogeFoo = Foo Identity
identityHaveHogeFoo = Foo (Identity 0) 1
残念ながらこれはエラーになります。
[1 of 1] Compiling Main ( builder-pattern.hs, builder-pattern.o )
builder-pattern.hs:7:14: error:
• No instance for (Show (haveHoge Int))
arising from the first field of ‘Foo’ (type ‘haveHoge Int’)
Possible fix:
use a standalone 'deriving instance' declaration,
so you can specify the instance context yourself
• When deriving the instance for (Show (Foo haveHoge))
|
7 | } deriving Show
| ^^^^
haveHoge
がShow
ではないよと。そりゃそうである。
Kindであることを伝えてみる
まずhaveHoge
はTypeではなくKindなんだよなあ。それを伝えてなかったらそりゃエラーにもなりますね。
修正しましょう。
{-# LANGUAGE KindSignatures #-}
import Data.Functor.Identity
import Data.Kind
data Foo (haveHoge :: Type -> Type)
= Foo
{ hoge :: haveHoge Int
, huga :: Int
} deriving Show
type MaybeHaveHogeFoo = Foo Maybe
maybeHaveHogeFoo = Foo Nothing 1
type IdentityHaveHogeFoo = Foo Identity
identityHaveHogeFoo = Foo (Identity 0) 1
これもエラーになります。
[1 of 1] Compiling Main ( builder-pattern.hs, builder-pattern.o )
builder-pattern.hs:9:14: error:
• No instance for (Show (haveHoge Int))
arising from the first field of ‘Foo’ (type ‘haveHoge Int’)
Possible fix:
use a standalone 'deriving instance' declaration,
so you can specify the instance context yourself
• When deriving the instance for (Show (Foo haveHoge))
|
9 | } deriving Show
| ^^^^
エラーの内容同じやんけ。
deriving instanceしてみる
まあここはちゃんとエラーメッセージが表示している推奨方法を一度試してエラーを見てみるべきですね。
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE StandaloneDeriving #-}
import Data.Functor.Identity
import Data.Kind
data Foo (haveHoge :: Type -> Type)
= Foo
{ hoge :: haveHoge Int
, huga :: Int
}
deriving instance Show a => Show (Foo a)
type MaybeHaveHogeFoo = Foo Maybe
maybeHaveHogeFoo = Foo Nothing 1
type IdentityHaveHogeFoo = Foo Identity
identityHaveHogeFoo = Foo (Identity 0) 1
公開当初deriving instance Show a => Show Foo a
とカッコを忘れていたのは修正しました。
エラー内容は、
[1 of 1] Compiling Main ( builder-pattern.hs, builder-pattern.o )
builder-pattern.hs:12:39: error:
• Expected kind ‘* -> *’, but ‘a’ has kind ‘*’
• In the first argument of ‘Foo’, namely ‘a’
In the first argument of ‘Show’, namely ‘(Foo a)’
In the stand-alone deriving instance for ‘Show a => Show (Foo a)’
|
12 | deriving instance Show a => Show (Foo a)
| ^
まあKindにShowであることを期待したらそうなりますよね。
ついでにKindであることを伝えなくても暗黙に推論して同じようなエラーを投げてきます。
Kind自体がShow
であるのではなくてKindの結果の型がShow
である必要があるのでそうですね。
Kindを完全に潰したら一応は通るけど解決策ではない
どう書くんだよこれ、どこかでそう言う構文を見た気がするけど思い出せない。
型族とかはtype classでの問題になるし…
Haskellの種(kind)について (Part 2) - Haskell-jp の > ですが、私たちは完全に種推論に頼らないような種注釈を提供することで、T :: (k -> ) -> k -> というような種多相化された型コンストラクタを作ることができます: を参考にしてみましょう。
ダメですね。
ConstraintKinds
でググって行くか。
さて、HListはShowのインスタンスにできるでしょうか。HListに登場する全ての型がShowのインスタンスであればできそうです。このように型への制限を表すのがConstraintで、それをカインドのレベルで扱えるようにするのがConstraintKinds拡張です。
Printf実装を通して学ぶGADTs, DataKinds, ConstraintKinds, TypeFamilies - Just $ A sandbox
おっこれそのものじゃないですか?
いやこれ手動でShow
のinstance
を記述するものですね…
そりゃ手動で書けば行けるだろうけど、それはミスを誘発するからやりたくない…
よく分からなくなってきてシノニムで完全にKindを適応してTypeにしたらコンパイル通りました。
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
import Data.Functor.Identity
import Data.Kind
data Foo m
= Foo
{ hoge :: m Int
, huga :: Int
}
type MaybeHaveHogeFoo = Foo Maybe
deriving instance Show MaybeHaveHogeFoo
maybeHaveHogeFoo = Foo Nothing 1
type IdentityHaveHogeFoo = Foo Identity
deriving instance Show IdentityHaveHogeFoo
identityHaveHogeFoo = Foo (Identity 0) 1
main :: IO ()
main = do
print maybeHaveHogeFoo
print identityHaveHogeFoo
うーん実用上アプリケーション側ではこれで問題は無いのですが、気持ち悪いからどうにかしたい。
なんかライブラリ使えば書けるらしい
ですが安心してください。もちろん Haskell には barbies という便利なライブラリがあり、Generics の力によりボイラープレートを劇的に減らすことができます。
と言うか… これに答え書いてるじゃないか。
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-}
import Data.Functor.Identity
import Data.Kind
data Foo m
= Foo
{ hoge :: m Int
, huga :: Int
}
deriving instance (Show (a Int)) => Show (Foo a)
type MaybeHaveHogeFoo = Foo Maybe
maybeHaveHogeFoo = Foo Nothing 1
type IdentityHaveHogeFoo = Foo Identity
identityHaveHogeFoo = Foo (Identity 0) 1
main :: IO ()
main = do
print maybeHaveHogeFoo
print identityHaveHogeFoo
これで動きます。フツーにKindを潰してやれば動いたわけですね… Kindのことを分かってあげられなかった。しばらく業務であんまりHaskell書いてなかったからHaskellの書き方を忘れてしまったのではと言う疑惑が自分に生まれてしまった。
ただしUndecidableInstances
拡張を使っている、コワイ!
後、多分無駄な拡張残しまくってる。
前にHaskell Day行った時にこれ見たから素直にそこから導線辿れば良かった。当初はそんな難題だと思わなかったので普通に検索したら出てくる情報だと思ってたのですよね。
これで一見落着かと思いましたが、そうでもない。
複数のフィールドをそれぞれMaybeで包みたい
元々の目標として、複数のフィールドがそれぞれ埋まってるかどうかを型レベルで判定したいと言うのがありました。
barbiesは果たしてその期待に答えることが出来るのでしょうか。
AllBF
に直接渡してやってもうまくいきませんでしたし、ドキュメント見ても2引数以上の型引数の対応はよく分かりませんね…
適応したい型自体はそんなに多くないわけなので、愚直に対応しましょう。
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
data Foo haveHoge haveHuga
= Foo
{ hoge :: haveHoge Int
, huga :: haveHuga Double
}
deriving instance (Show (a Int), Show (b Double)) => Show (Foo a b)
type InitFoo = Foo Maybe Maybe
noneFoo :: InitFoo
noneFoo = Foo (Just 0) (Just 1.0)
main :: IO ()
main = do
print noneFoo
うーんボイラープレートが多い。
Bi-functors and nesting
の項があるし対応してるんじゃないですか? いや、ダメですね。今回対応するのはBiとかそう言う次元ではない。レイヤーが少なくとも3つはあるから2つでは対応できない。
しばらくは諦めてボイラープレートを書きます。あまりにも量が多くなるならコード自動生成するコードを書いても良いかもしれません。
GHC.Generics
にあまり詳しくないのでサクッとお出しすることは難しいかもしれませんが…
AesonのFromJSONがフィールドの省略を受け付けない
omitNothingFields の説明によると、
In particular, if the type of a field is declared as a type variable, it will not be omitted from the JSON object, unless the field is specialized upfront in the instance.
とのことなので、型変数としてMaybe
が選択される場合、
JSONのフィールドを省略することは出来ないようです。
{"hoge": null}
のようにnull
バリューを入れる必要があります。
これが今回の場合だと不便なので、
Aesonの型クラスに対してだけは、結局型シノニムに対してinstance FromJSON
することになりました。
これは完全にボイラープレートになるので、面倒臭さが増えてしまいました。