パッケージを跨いだ再exportの危険性

この記事は Haskell Advent Calendar 2013 1日目後半の記事です.前半の記事もよろしく.


「ある露出モジュールAで隠蔽モジュールA.Xをimportし,AでA.Xのシンボルを選択的に(あるいはモジュールごと全部のシンボルを)露出させることで,AとA.Xを含むパッケージAから提供するインターフェースをコントロールする」ことができる再exportは,パッケージ内部でのモジュール構成をパッケージ外への公開状態とは切り離すことのできる,とても便利ですばらしい機能だ.

module A ( foo, bar, baz, module A.Y ) where

import A.X ( baz )
import A.Y

foo = ...
bar = ...

ただし,これがパッケージを跨いで行われる場合,その事情は相当異なってきて,結論から言うならば害悪だと思っている.

あるパッケージBがパッケージAのモジュールをimportし,再exportしていたとする.(実際このようなexportは多くのパッケージで見られる)

module B ( foo ) where

import A ( foo )

パッケージBの依存関係には当然パッケージAが入ってくるだろう.そしてパッケージBを使う何かプログラムCを作ったとき,パッケージBがキチンと設計されていれば,CはパッケージBに直接依存する関係は持つが,パッケージAへの依存関係は陽に持たずに済むようになっているはずだ.でなければ,このようなexportを行う意味が無い.

一方,このモジュールBで再度exportされたシンボルは,パッケージAのものであると同時にパッケージBのものでもあるという状態になる.となると,B-1.0とA-1.0の組でビルドしたときと,B-1.0とA-1.1の組でビルドしたときでは,パッケージBから露出しているパッケージAのシンボルの質が異なっているかもしれない.つまり,

  • パッケージBのバージョンは変わっていないのにBから提供されているシンボルの型が変わっているかもしれない
  • パッケージBのバージョンは変わっていないのにBから提供されている型のコンストラクタが増減しているかもしれない
  • パッケージBのバージョンは変わっていないのにBから提供されているクラスのインターフェースが増減しているかもしれない

などなど,という事態が普通にあり得る.通常,パッケージ自身で提供しているシンボルに関し上記のような変更があるのなら,hackageのバージョニングルールに従いバージョン番号の2番目以上を上げなければならないようなケースに該当するだろう.にもかかわらず,そのことが検知されない.「パッケージ提供者がインターフェースの硬さについて保証し,変える場合はパッケージのバージョンを上げる」というバージョニングの基本に対する抜け穴になってしまう.バージョンが上がらないままいろいろ変わってしまう.

型の変更やインターフェースの減少が警戒されるのはバージョニングルール上明らかだが,前述した中で増加まで問題にしている点はすぐにはピンとこないかもしれない.たとえば,開発時に,ある環境でプログラムCをB-1.0に陽に依存させた上で,Bの依存関係的にA-1.1が入ってきた状態で作っているかもしれない.この環境からプログラムCを別の環境,A-1.0があらかじめ入っているような環境に持って行ってビルドすると,Bに対する依存関係は間違っていないにも関わらず,インターフェースが減少したのと同様の状況になり,ビルドできないことがある.そのためA-1.0/1.1の間で単に増加するだけであっても依存関係的にはマズいのだ.

このようなパッケージAとパッケージBの関係は随所に見られるが,近々に遭遇したケースを挙げると,wai-extrawai-logger-0.3.1/0.3.2のペアがある.wai-loggerのNetwork.Wai.Logger.Format.IPAddrSourceをwai-extraはNetwork.Wai.Middleware.RequestLogger.IPAddrSourceとして再exportしている.そして,wai-loggerが0.3.1から0.3.2になったときに,IPAddrSource型のコンストラクタにFromFallbackが追加された*1.その結果wai-extraのバージョンが上がっていないにも関わらず,wai-extraのNetwork.Wai.Middleware.RequestLoggerをimportしておけば,追加したFromFallbackもまた使えるようになってしまった.つまり,FromFallbackをwai-extraにのみ陽に依存させプログラム中で意気込んで使うと,陽に依存していないはずのwai-loggerがまだ0.3.1の環境でビルドできなくなってしまう.たとえwai-extraのバージョンが同じであってもだ.

パッケージBのみに陽に依存させていればよいプログラムCから見たら,パッケージBのバージョンが上がっていないにも関わらず,何故か陽に依存していないパッケージAのバージョンアップを食らってビルドできなくなるという事態に陥る.こうなると泣く泣くプログラムCもパッケージAに依存させねばならず,ならなんのためにわざわざパッケージBがパッケージAのシンボルを再exportしてるのかというトコロに立ち戻ってしまう.

正直,こういったパッケージ跨ぎでの再exportは一切やめて欲しい.もしかすると私が気付いてない有用性があるのかもしれないが,それが一体何であっても前述したデメリットが大き過ぎて余程大きなメリットがあったとしても許容できそうにない.

*1:というかこれ追加するpull reqをしたの私だ