GHC 9からTemplate Haskellでinstanceを定義する時に相互参照させるにはまとめて定義する
前提となるソースコード
TH.hs
ファイルに以下のような定義を書いて、
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module TH where
import Data.OpenApi
import Language.Haskell.TH
deriveSchema :: Name -> DecsQ
deriveSchema name =
[d|
instance ToSchema $(conT name)
|]
Lib.hs
で以下のように呼び出します。
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TemplateHaskell #-}
module Lib
( someFunc
) where
import GHC.Generics
import TH
someFunc :: IO ()
someFunc = putStrLn "someFunc"
data VerbInstance
= VerbInstance
{ verbInstanceSynset :: Synset
, verbInstanceModify :: [ModifierInstance]
}
deriving (Eq, Ord, Read, Show, Generic)
data ModifierInstance
= ModifierInstance
{ modifierInstanceSynset :: Synset
, modifierInstanceModify :: [ModifierInstance]
}
deriving (Eq, Ord, Read, Show, Generic)
newtype Synset
= Synset
{ synsetLabel :: String
}
deriving (Eq, Ord, Read, Show, Generic)
deriveSchema ''VerbInstance
deriveSchema ''ModifierInstance
deriveSchema ''Synset
問題
このコードはGHC 8.10.7では問題なくコンパイルされます。
しかしGHC 9.0.2, GHC 9.2.5ではコンパイルされません。
/home/ncaq/Downloads/example-openapi3-cycle-instance/src/Lib.hs:33:1: error:
• No instance for (Data.OpenApi.Internal.Schema.ToSchema Synset)
arising from a use of ‘Data.OpenApi.Internal.Schema.$dmdeclareNamedSchema’
• In the expression:
Data.OpenApi.Internal.Schema.$dmdeclareNamedSchema @(VerbInstance)
In an equation for ‘Data.OpenApi.Internal.Schema.declareNamedSchema’:
Data.OpenApi.Internal.Schema.declareNamedSchema
= Data.OpenApi.Internal.Schema.$dmdeclareNamedSchema
@(VerbInstance)
In the instance declaration for
‘Data.OpenApi.Internal.Schema.ToSchema VerbInstance’
|
33 | deriveSchema ''VerbInstance
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
というエラーになります。
VerbInstance
のinstance
定義段階ではSynset
にinstance
が定義されてないということですね。
これまでは通ってたのに何故?
この例ぐらいのinstance
単独ならばわざわざTemplateHaskellを使わなくても良いのですが、まとめてinstance
に共通する設定を書きたいという場面で問題になります。またこの例では単純なものになっているので、単にTemplateHaskellを呼び出す順序をトップダウンからボトムアップに変えれば解決するのですが、依存関係が循環するとどうにもならなくなります。
aesonのFromJSON
に書き換えてみても同じ問題が発生しました。ということはopenapi3の問題ではありません。
しかしaesonのFromJSON
の方はGHC 9.2のNoFieldSelectors
でカスタムしなくてもderiving
するだけで良くなったので、ほぼ問題ではありません。
template-haskellのバージョンを合わせて検証してみようかと思ったのですが、 GHCのバージョンに合わせた正確なバージョンを要求してくるのでそれは難しそうです。
どこかにChangeLogとしてはっきりとGHC 9の非互換性として書かれていれば少しは諦めもつくのですが、探しても中々見つかりません。
そもそもGHC 9の非互換性なのか、 template-haskellパッケージの非互換性かもよく分かりません。
GHC 9での非互換性を示す文書
haskell-jp Slackで@mod_poppoさんに教えてもらったのですが、はっきりと非互換で順序を気にするようになったと変更があったようです。
The order of TH splices is more important · 9.0 · Wiki · Glasgow Haskell Compiler / GHC · GitLab
ワークアラウンド
解決策の一つとして、
deriveSchema
を一回ずつ呼び出すのではなく、
Name
のリストを受け取って一回で定義してしまうというものがあると思います。
しかし逆にlensのinstance
とかは存在しない場合class
を作る処理が入るので、それが出来なかったので型ごとに導出処理を一気に書くという手法を使っていたのでした。
なのでlensのTHだけは個別に呼び出して、こういう問題が起きるものだけはリストを受け取って一度で定義するのが現実的な回避策でしょうか。しかし本当にバージョンで破壊的変更が起きているのか定かではないのに回避するというのも少し気持ち悪いですね。
もしくは括弧で括るだけで良さそうです。
$(do
x0 <- deriveSchema ''VerbInstance
x1 <- deriveSchema ''ModifierInstance
x2 <- deriveSchema ''Synset
pure $ x0 <> x1 <> x2
)
もう少し見た目なんとかならないだろうか…
$(concat <$> mapM deriveSchema [''VerbInstance, ''ModifierInstance, ''Synset])
これでだいぶマシになりました。
foldMap deriveSchema [''VerbInstance, ''ModifierInstance, ''Synset]
これでもう少しマシ。