Original post: Upcoming Yesod breaking changes

2018年 1月 11日 Michael Snoyman

私が作ったライブラリの破壊的変更点について話をしてきましたが、Yesod をその変更から取り残したくはありませんでした。yesod-core は 2014年からバージョン1.4 で安定しています。しかし、私のパッケージエコシステムにおける MonadUnliftIO の変更は Yesod にも影響してくるでしょう。問題は、どの程度かということです。

知らない人のために補足しておくと、MonadUnliftIOmonad-controlexceptions にそれぞれ存在する MonadBaseControlMonadCatch/MonadMask クラスの代替となる型クラスです。私はこれまでこの新しいアプローチのメリットを至る所で言及してきましたが、おそらく一番良い資料はリリースアナウンスのブログ記事でしょう。

予定されている Yesod の変更点を簡潔に述べると:

  • WidgetT の内部表現を変更する。現在は WriterT として実装されているので変更が必要です。MonadUnliftIO とマッチさせるために、IORef を持つ ReaderT にする必要があります。この変更は内部モジュールにしか影響しないため、私の想像ではかなり小さな変更になります (非破壊的変更と言い換えることもできる)。
  • MonadBaseControlMonadCatch/MonadMask のインスタンスを削除する。これは厳密には必要ないですが、2つのアドバンテージがあります: 依存関係の数を少なくし、HandlerT の上に StateT を乗せた状態で concurrently を使うような危険な振る舞いを避けることができます。
  • 依存しているライブラリを、変更後の新しいバージョンに切り替える。特に conduit や resourcet など。これも厳密には破壊的変更ではないですが、私は依存関係のメジャーバージョンへのサポートを打ち切ることを半破壊的変更だと捉えています。
  • 破壊的変更に伴う、いくつもの小さなお片付け。いくつかの場所に正格化のためのアノテーションを追加するとか、死んでいる GoogleEmailBrowserId 等のモジュールの削除などです。

これは完全に筋の通った変更で、Yesod 1.5 (2.0) としてリリースします。私が実験しているもう少し大きな変更があるので、それをここで共有しておきます。この変更が Yesod のユーザーにとって価値があるかどうか、フィードバックをお願いしたいです。

トランスフォーマーから離れろ!

以下の説明は、こういう議論ではいつもそうであるように、仕方なく IO に入れなければならないコードについて言及したものです。純粋なコードは今回はパスです。

(実際の変更よりも大きく見えますが) 変更は no-transformer ブランチ で確認できます。まぁすぐに嘘だと分かると思いますが、意図を正確に表しています。ここ 1年間のブログ記事の雰囲気と推奨しているベストプラクティスについての私の議論を見れば、次の簡単な主張に帰結します: モダンな Haskell はモナドトランスフォーマーを使いすぎなのです

この主張に対して最も過激な反応をするなら、全てのトランスフォーマーを削除し、全てのコードを IO に入れる、というものになります。私はちょっと妥協して、reader の機能は残す価値があると判断しました。なぜなら、logInfo のような単純な関数に何かを追加で渡すのは、かなりの苦痛だからです。Yesod の核となっている型は HandlerT で、getHomeR :: HandlerT App IO Html のように使われます。内部では、HandlerT は以下のようになっています:

簡単な質問をしましょう: HandlerT は本当にトランスフォーマーである必要があるのでしょうか?

なぜシンプルにこんな風に書かないのでしょうか:

m という型引数を IO という具体的なものに変えただけです。どんな場所でもハンドラはベースモナドとして IO を持つ、という前提がすでにあるので、汎用性が無くなるわけではありません。

しかしこの結果得られるものは:

  • 少し分かりやすいエラーメッセージ
  • より少ない型制約。思い浮かぶのは MonadUnliftIO m みたいな
  • 内部で、型族周りの汚い部分をかなりシンプルにできる。

ヘルパー型シノニムを導入すれば、多くの後方互換性を得ることができます:

さらに、Template Haskell によって生成された Handler という型シノニムを使っているなら、新しいバージョンの Yesod は正しいものを生成してくれるでしょう。全体として、これはほんの少しの改善です。この変更によって得られる利益と破壊のコストを天秤にかける必要があります。ただ、まだ折衷案が残っています。

サブサイトを扱う (ええ、トランスフォーマーです)

私は 2回嘘をつきました: さっきのブランチはトランスフォーマーを使っています。そして HandlerTHandlerFor よりも一般的です。いずれの場合もサブサイトをどうにかする必要がありますが、これは歴史的に苦痛を伴う作業です (使う分にはひどすぎることもないです)。実は、今日 HandlerT が存在する唯一の理由に、サブサイトを綺麗に層に分けるやり方で実装しようとした、というものがあります (失敗しましたが)。Yesod を長く使っている人は GHandler という前回のアプローチを覚えているかもしれません。そして、サブサイトを書いたことがあって defaultLayout を使う時に起こる地獄を知っている人は、現在の状況は良くないということに同意してくれると思います。

なので、問題を全て解決するため: サブサイトを書く時、ほとんど全てが普通の handler のコードを書くのと同じです。以下の点が違いますが:

  • getYesod を呼ぶ時、マスターサイトの app データを受け取る (例えば、スキャフォールドサイトの App)。サブサイトのデータを入手する方法も必要になります (例えば、yesod-staticStatic という値)。
  • getCurrentRoute を呼ぶと、マスターサイトのルートを返してくれます。例えば、yesod-auth の中にいる時、親サイトの取りうる全てのルートを扱いたくはないでしょう。その代わりに、サブサイト自身のルートを知りたいはずです。
  • URL を生成する時、サブサイトのルートを親サイトのルートに変換する手段が必要になります。

今日の Yesod では、これらの違いを HandlerT の中で提供しています。こうすると、ベースケースの mIO にする時に、やけに複雑になります。その代わりに、新しいブランチでは HandlerFor の上に ReaderT 1層を置き、これら 3つの機能を提供しています。詳しく知りたい方はコードを見てください

何をすべきか?

全体的に、私はこの設計をエレガントで、理解しやすく、コードを綺麗にしてくれるものだと思っています。現実問題、昔のものから大きく離れたわけでも大きく改善されたわけでもありませんし、私はトランスフォーマーを無くすような変更の道半ばで進めなくなっています。

近い将来、Yesod には破壊的変更が行われますが、必ずしもこの変更を含む必要はありません。もしこの変更が追加されないのなら、破壊的変更は上で言及した、かなりマイナーなものになるでしょう。この変更が好ましいという一般のコンセンサスが得られたなら、同時に追加した方がいいでしょうね。