Link to
hereこの記事は
この記事はHaskell Advent Calendar その2兼プリキュアAdvent Calendar 20185日目の記事です。
毎度同時投稿で失礼します。
今年は私用で忙しかったので、のんびり書いてできあがったら空いてる日に投稿する、という楽なスタイルで書かせていただきました。なのでタイムスリップして5日目の記事と言うことにします(それにしてもずいぶん時間かかってしまってすみません、もうクリスマスも過ぎたし…😥)。
今回も例年の私のAdvent Calendarどおり、タイプセーフプリキュア!に、最近追加しようとした機能と、その際使用したもろもろの要素技術についての記事です。
タイプセーフプリキュア!そのものについては今年9月の記事や、そこで言及しているもっと古い記事をご覧ください。
Link to
here課題: プリキュアに変身していない状態で浄化技を使おうとした場合、型エラーにしたい
従来より、タイプセーフプリキュア!には、PreCureMonad
と呼ばれる、プリキュアの台詞をdo
記法で組み立てる機能があります。
例えばGHCi上で下記のように書くだけで、「Go! プリンセスプリキュア」のあの名シーンを再現できます1。
> :m ACME.PreCure
> :{
> let scene = do
> say "この罪を抱いたまま、もう一度、グランプリンセスを目指す!"
> scarlet <- transform Towa (PrincessPerfume DressUpKeyScarlet)
> scarletModeElegant <- transform scarlet (PrincessPerfume DressUpKeyPhoenix)
> purify scarletModeElegant (ScarletViolin DressUpKeyPhoenix)
> :}
名シーンを単純な文字列のリストとして使いたい場合はこう👇しましょう(出力は手で見やすく加工しています)。
> composeEpisode scene
ghci"この罪を抱いたまま、もう一度、グランプリンセスを目指す!"
[ "プリキュア!プリンセスエンゲージ!"
, "深紅の炎のプリンセス!キュアスカーレット!"
, "冷たい檻に閉ざされた夢、返していただきますわ。"
, "お覚悟を決めなさい!"
, "エクスチェンジ!モードエレガント!"
, "スカーレット・バイオリン!フェニックス!"
, "羽ばたけ炎の翼!"
, "プリキュア! フェニックス・ブレイズ!"
, "ごきげんよう。"
, ]
さらにprintEpisode
という関数で実行すれば、1行ごとに間隔を置いてあの台詞を再生できます。
> printEpisode scene
ghci
この罪を抱いたまま、もう一度、グランプリンセスを目指す!
プリキュア!プリンセスエンゲージ!
深紅の炎のプリンセス!キュアスカーレット!
冷たい檻に閉ざされた夢、返していただきますわ。
お覚悟を決めなさい!
エクスチェンジ!モードエレガント!
スカーレット・バイオリン!フェニックス!
羽ばたけ炎の翼!
プリキュア! フェニックス・ブレイズ! ごきげんよう。
そんなPreCureMonad
ですが、先ほどのコードをよく読めばわかるとおり、ちょっと不格好ですよね。
具体的には下記の2行です。
<- transform Towa (PrincessPerfume DressUpKey_Scarlet)
scarlet <- transform scarlet (PrincessPerfume DressUpKeyPhoenix) scarletModeElegant
1行目のtransform
関数が、変身する女の子であるTowa
(赤城トワ)と変身アイテムを受け取ってCureScarlet
を返し、さらにそのCureScarlet
を2行目のtransform
関数に渡すことでキュアスカーレットのモード・エレガント(CureScarlet_ModeElegant
)を取得しています。
「transform
関数が、変身する女の子であるTowa
(赤城トワ)と変身アイテムを受け取ってCureScarlet
を」返すという箇所について、Towa
に加えてCureScarlet
を新しく作っているように聞こえます。
本来同一人物であるはずのTowa
とCureScarlet
を、あたかも別々のものとして扱っているように捉えられかねません。
そう、本来プリキュアの「変身」は女の子自身の状態を書き換えるものとして表現した方が自然なのです。
Haskellでそうした「状態」を表現する場合、名前のとおりState
Monadを使うのが割と一般的な方法です(プログラム全体で状態を管理する場合、IORef
やTVar
などを使う方が例外に強く安全ではありますが、それはさておき)。
しかし、従来のState
Monadでプリキュアの変身や浄化技を表現する場合、女の子が変身していない状態で浄化技(purify
)を使おうとした場合をどのように扱うか、という問題があります。
先ほどの例で言うところの
ScarletViolin DressUpKeyPhoenix) purify scarletModeElegant (
という行でまさにその「浄化技」を実行しているのですが、プリキュアの設定上、特定の浄化技を使うには、特定のプリキュアのフォームに、専用のアイテムを渡さなければなりません。
タイプセーフプリキュア!ではこの点に強くこだわり、浄化技が使用できる組み合わせごとに型クラスのインスタンスを定義することで、間違った組み合わせをpurify
関数に渡すと、型エラーになります(詳しくはタイプセーフプリキュア!を最初に技術的に解説した記事をご覧ください)。
当然、まだ変身していない状態の女の子をpurify
関数に渡しても、エラーになってしまいます。
> scene = purify Towa (ScarletViolin DressUpKeyPhoenix)
<interactive>:4:9: error:
No instance for (Purification
• Towa (ScarletViolin DressUpKeyPhoenix))
of ‘purify’
arising from a use In the expression: purify Towa (ScarletViolin DressUpKeyPhoenix)
• In an equation for ‘scene’:
= purify Towa (ScarletViolin DressUpKeyPhoenix) scene
プリキュア実装の大先輩であるrubicureでは、同じようなケースで実行時エラーを出すようにしていますし、PreCure Monadにおいても、ExceptT
を使ってエラーにする、という方法が採れるでしょう。
しかしそこは「タイプセーフプリキュア!」。どうにかして、変身していない状態でのpurify
関数の実行を型エラーにして、従来のこの振る舞いと一貫させたいところですよね。
というのが今回の課題です。
Link to
here実現方法: Indexed Monadと型レベル連想配列を使う
今回の課題のとおり、「変身していない状態でのpurify
関数の実行を型エラー」としつつ、「変身した状態でのpurify
を型エラーとしない」ためには、purify
やtransform
を実行する前後で、State
Monad内で共有している値の型を変更できるようにする必要があります。
残念ながら、これは従来のState
Monadでは不可能です。
State s
に対する>>=
の型が(>>=) :: State s a -> (a -> State s b) -> State s b
となっていることから察せられるとおり、State
Monadの中で共有する型は、アクションの実行前後にかかわらず同じs
でないといけないためです。
これはそもそも従来のMonadの仕様上やむを得ないことです。
従来のMonadはそもそもアクションの実行前後で、アクションの実行結果以外の型を変えることができないようになっています。
>>=
の型が(>>=) :: Monad m => m a -> (a -> m b) -> m b
となっていることからしても、アクションの実行前後でm
はm
のままであることがわかります。
この、「アクションの実行前後で、m
の型を変えることができる」ようにしたのがIndexed Monadです。
Indexed Monadは次のような型宣言にすることで、アクションの実行前後で異なる型の “index” を挟めるようになっています。
class IxApplicative m => IxMonad m where
ibind :: (a -> m j k b) -> m i j a -> m i k b
IxApplicative
は名前のとおりIxMonad
と同様に”index”が付いたApplicative
となっています。詳しい定義はドキュメントをご覧ください。
唯一のメソッドであるibind
が、普通のMonadにおける>>=
の引数をひっくり返して”index”を追加したものです。
(>>=) :: Monad m => m a -> (a -> m b) -> m b
のm
に、型引数が2つ追加されていますね?これが”index”です。
あるIxMonad
m
がm i j a
という形で型引数を渡されている時、i
がアクションを実行する前の型、j
がアクションを実行した後の型を表します。
a
は普通のMonad
と同様、アクションの実行結果となっています。
さらにIndexedなState
Monad (IxState
)で使えるアクションの型宣言を見れば、IxState
で共有している状態の型が、アクションの実行前後で変更できることがよりはっきりとわかるでしょう。
iget :: IxState i i i
-- ^ igetしてもIxStateが管理している状態は変わらないため、型もやはり変わらず。
iput :: j -> IxState i j ()
-- ^ iputするとIxStateが管理している状態は、引数で渡した値の型に変わる。
こちらもおなじみmtlパッケージにあるState
Monadに、単純に “index” を加えただけのものとなっています。
Indexed Monadの世界 - モナドとわたしとコモナドで紹介された際のIndexed Monadは、ido
というQuasi Quoteを使ってdo
記法を無理矢理シミュレートしていましたが、現在はGHCのRebindableSyntax
という拡張を使うことで、普通のdo
記法をそのまま利用することができるようになりました(例は後で紹介します)。
さらに、現在はRebindableSyntax
を使った場合の諸々の問題を回避するべく、Indexed Monadを一般化したSuper Monadと、それを簡単に使えるようにしたGHCの型チェッカープラグインが作られたり、do-notationという、Indexed Monadと普通のMonadを型クラスで抽象化したパッケージが作られたりしています。
今回は純粋にIndexed Monadを使うだけで十分だったので、Super Monadやdo-notationは使用しませんでしたが、今後Indexed Monadをもっと実践的に使用する機会があれば、使用してみたいと思います。
Indexed Monadを使用することで、State
Monadで共有している状態の型を、アクションの実行前後で変更できるようになりました。
続いて、各女の子の状態を、State
Monadで共有している状態の型として、どのように管理するかを検討しましょう。
というのも、タイプセーフプリキュア!には最新のmasterの時点で59人の女の子が収録されている2のですが、それらすべてを変身前と変身後に分けて管理するだけでも、2 ^ 59通りの状態を型として表現できなければなりません。
これを直感的に表現できるようにするために、ちょっと型レベルプログラミングの力を借りましょう。そこで登場するのが「型レベル連想配列」です。
「型レベル連想配列」という言い方はあまりしないのでピンとこないかも知れませんが、要するに型(タイプセーフプリキュア!の場合、プリキュアに変身する女の子一人一人に個別の型を割り当てているので、その個別の型)と、それに対応する値のペアを含んだ型レベルリストです。
大雑把に言うと、下記👇のような内容となります(実際にはもう少し違う型で構成されています)。
Hana, HasTransformed 'True)
[ (-- ^ プリキュアに変身する女の子を表す型(この場合「HUGっと!プリキュア」の野乃はな)
Saaya, HasTransformed 'False)
, (-- ^ 対応する女の子が変身しているかどうかを表すsingleton type。
-- DataKindsで型に持ち上げられたBoolを、普通の値として扱えるよう変換するためのラッパー。
-- 申し訳なくもsingleton typeについては割愛します。
-- Haskell-jpのSlack Workspaceあたりでリクエストがあったら書こうかな。
Homare, HasTransformed 'False)
, (...
, ]
別の視点で見ると、これはいわゆるExtensible Recordとも似ています。
extensibleパッケージやlabelsパッケージ、superrecordパッケージがそうしているように、Extensible Recordは、フィールドのラベルを表す(型レベルの、静的な)文字列をキーとして、それに対応する値を含んだ連想配列として見なすことができるためです。
事実私は今回、extensibleを使ってこの機能を実装しました。他のExtensible Recordの実装でも良かったのですが、これ以外のものを全く使ったことがないので😅。
Link to
hereできたもの
Indexed MonadとExtensible Recordを組み合わせることで、PreCureMonadの各種アクションを、次のように置き換えられることがわかりました。
transform <girl> <item>
:IxState
(実際にはそのMonad Transformer版であるIxStateT
)で共有している型レベル連想配列のキー<girl>
に対応する値を「変身した状態」に更新する。<girl>
がすでに変身している状態の場合は、型レベル連想配列のキー<girl>
に対応する値が「変身した状態」になっているので型エラーとする。IxStateT
をかぶせたWriter
Monadで共有しているリストに、<girl>
と<item>
に対応した、変身時の台詞(文字列)を追記する。
purify <precure> <item>
:IxStateT
で共有している型レベル連想配列のキーを取得するため、<precure>
にあらかじめ定義しておいたType FamilyAsGirl
を適用する。AsGirl
で取得した型を、これ以降<girl>
と呼びます。
<girl>
が「変身した状態」になっていない場合は、型レベル連想配列のキー<girl>
に対応する値が「変身していない状態」になっているので型エラーとする。IxStateT
をかぶせたWriter
Monadで共有しているリストに、<precure>
と<item>
に対応した、浄化技を使用したときの台詞(文字列)を追記する。
このように生まれ変わったPreCure Monadを✨Super PreCure Monad✨と呼ぶこととします💪
下記がSuper PreCure Monadのサンプルコードです。
野乃はながキュアエールに変身して、「ハート・フォー・ユー」という浄化技を放つまでを表しています。
cureYell :: PreCureM (StatusTable '[]) (StatusTable '[Hana >: HasTransformed 'True]) ()
= do
cureYell Hana
enter Hana (PreHeart MiraiCrystalPink)
transform CureYell (PreHeart MiraiCrystalPink) purify
enter
は、旧PreCureMonadにはない、Super PreCure Monadに新しく追加されたアクションです。
引数で指定された女の子や、女の子が変身したプリキュアを「登場」させます。
具体的には、以下のように振る舞います。
- 引数で指定された値が女の子
<girl>
であれば、IxStateT
で共有している型レベル連想配列のキー<girl>
に対応する値を「変身していない状態」で追加する。 - 引数で指定された値がすでに変身したプリキュア
<precure>
であれば、<precure>
にType FamilyAsGirl
を適用し、女の子を表す値<girl>
を取得する。IxStateT
で共有している型レベル連想配列のキー<girl>
に対応する値を「変身した状態」で追加する。
したがって、transform
するにしてもpurify
するにしても、事前に変身前の女の子かその変身後のプリキュアがenter
していないといけません。
これは単純にその方が実装が簡単だから、という理由もありますし、一旦「登場」させたほうがなんとなくかっこいいかな、と感じたからです。
Link to
here✨Super PreCure Monad✨を試す方法
ここまで述べたような基本的な仕様は実装できたものの、まだ解決すべき技術的な問題が見つかったので、残念ながらリリースはされていません(その詳細は気が向いたら書きます)。
なので、試す場合は下記のように実行してください。
$ chcp 65001
-- ^ Windowsの方は恐らく必要
$ git clone -b super-precure-monad https://github.com/igrep/typesafe-precure.git
$ cd typesafe-precure
$ stack build
$ stack exec ghci
> :set -XRebindableSyntax -XFlexibleContexts -XTypeFamilies
> import Prelude hiding ((>>), (>>=))
> :m + ACME.PreCure ACME.PreCure.Monad.Super
> :{
> scene = do
> enter Makoto
> transform Makoto (LovelyCommuneDavi CureLoveads)
> purify CureSword (LovelyCommuneDavi CureLoveads)
> :}
> printEpisode scene
(ダビィー!)
プリキュア!ラブリンク!L! O! V! E!)
(
勇気の刃! キュアソード!
このキュアソードが 愛の剣で、あなたの野望を断ち切ってみせる! 閃け!ホーリー・ソード!
「変身していない状態でのpurify
関数の実行を型エラーとする」といった仕様を試す場合は、こちらに置いた、全プリキュアの変身と浄化技を列挙したテスト用ファイルをghciで読んでみるといいでしょう。
先ほど👆の手順でgit clone
したディレクトリーにおいて、あらかじめstack build
を実行しておくのをお忘れなく。
$ stack build
$ stack exec ghci gen/AllPreCureM.hs
適当にgen/AllPreCureM.hs
を書き換えて:r
してみれば、概ねいい感じに動いていることがわかるはずです。
例えば冒頭付近にある、
= printEpisode $ do
act_CureDiamond_LovelyCommuneRaquel_CureLoveads Rikka
enter Rikka (LovelyCommuneRaquel CureLoveads)
transform CureDiamond (LovelyCommuneRaquel CureLoveads) purify
というSuper PreCure Monadによるアクションから、transform Rikka (LovelyCommuneRaquel CureLoveads)
という行を削除した上で:r
してみると、次のようなエラーになります。
> :r
[1 of 1] Compiling AllPreCureM ( gen\AllPreCureM.hs, interpreted )
gen\AllPreCureM.hs:22:3: error:
• Couldn't match type ‘'False’ with ‘'True’
arising from a use of ‘purify’
• In a stmt of a 'do' block:
purify CureDiamond (LovelyCommuneRaquel CureLoveads)
In the second argument of ‘($)’, namely
‘do enter Rikka
purify CureDiamond (LovelyCommuneRaquel CureLoveads)’
In the expression:
printEpisode
$ do enter Rikka
purify CureDiamond (LovelyCommuneRaquel CureLoveads)
|
22 | purify CureDiamond (LovelyCommuneRaquel CureLoveads)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, no modules loaded.
ちゃんと、変身していない状態でpurify
することを型エラーにできていますね!
ここまでできていながら残念ですが、リリースは、来年のプリキュアハッカソンかAdvent Calendarあたりに乞うご期待と言うことで!💦
それでは2019年もHaskellでSuper PreCure Hackingを❣️❣️❣️