haskellプログラムのimportの別名が多くなる問題にはclassy-preludeを使いましょう

classy-preludeというhaskellパッケージの紹介です.

importの別名が多くなってしまう問題

haskellでは多くのデータ構造に対する関数がほぼ同じ意図を持っているのにも関わらず,違うモジュールで違う型で定義されています.

lookup, insert, length, member, updateなどですね.データ構造に対する典型的な関数たちは多く被っています.

例えばlookup関数は単なる関数で,baseのData.Listでは,lookup :: Eq a => a -> [(a, b)] -> Maybe bと定義されています.unordered-containersのData.HashMap.Lazyではlookup :: (Eq k, Hashable k) => k -> HashMap k v -> Maybe vと定義されています.

というわけで,Data.HashMapなどを使うときには,一般的にはimport qualifiedで別名を付ける必要が出てきます.

いつ衝突する関数が増えるかわからないので,常に別名を付けてやるべきだと主張する人もいます.Haskellでのimportの使い方 - Blog :: Meatware

これは2014年の記事で,今はyesodのデフォルトなどでは真逆の方法,別名を付けないという解決方法が行われています.

classy-preludeで解決

抽象的にはlookupmemberは同じことのはずで,データ構造が異なる場合に違う関数を使わなければいけないというのはスマートではありません.

lookupがクラスの関数なら,違うデータ構造でも同じ関数を使えたはずです.

まさにそれを行ってくれるのがclassy-preludeです.

classy-preludeはクラス化された関数群を提供してくれます.例えば,先ほど述べたlookupはclassy-preludeがre-exportするmono-traversableのData.Containersに定義され,その定義元クラスであるIsMapListHashMap両方にinstanceを提供してくれています.classy-preludeのサポートするデータ構造(実際に実装されているのはmono-traversableですが)を使っている限りは,importで別名を付ける必要はありません.classy-preludeがラップしてくれます.

ただ,完全にbaseなどの内容をそのままクラス化できたわけではなく,fromListなどはsetFromListmapFromListに分けられていたりします.

classy-preludeを使う注意点として,headなどの例外を容易に発生させる可能性のある関数はそのまま移植されておらず,NonNullに限られていることが挙げられます.headMay :: MonoFoldable mono => mono -> Maybe (Element mono)readMay :: (Element c ~ Char, MonoFoldable c, Read a) => c -> Maybe aなどのMaybe付きにその能力を去勢された関数があるので,それを使いましょう.headEx :: MonoFoldable mono => mono -> Element monoというそのままの関数が存在しますが,危険なので推奨はしません.

また,classy-preludeはデータ構造への関数だけではなく,putStrLnなどのIO周りの被りが多く存在する関数も1つに統一していて,baseと違ってTextByteStringを使っています.Stringを使っている既存プロジェクトを移行させるときは少し手を加える必要があるかもしれません.

classy-preludeを使うにはもともとのpreludeのimportを省く必要があるので,以下のようにプラグマをつけてimportしましょう.

{-# LANGUAGE NoImplicitPrelude #-}
import           ClassyPrelude

ついでにwhenM :: Monad m => m Bool -> m () -> m ()などの便利な関数も付いてきます.

classy-preludeの問題

  • 多くのデータ構造,多くの機能を提供するため,依存ライブラリがこれを指定するだけで爆発的に多くなります
  • クラス化した代償で型推論が決定しなくなることがあります,その時は型注釈をしてあげましょう
  • エラーがわかりにくくなります
  • 基本的にre-exportしているラッパーなので関数の実装が何処に置いてあるのかわかりにくくなります