• 作成:
  • 更新:

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
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^

というエラーになります。 VerbInstanceinstance定義段階ではSynsetinstanceが定義されてないということですね。

これまでは通ってたのに何故?

この例ぐらいの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]

これでもう少しマシ。