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で解決
抽象的にはlookup
やmember
は同じことのはずで,
データ構造が異なる場合に違う関数を使わなければいけないというのはスマートではありません.
lookup
がクラスの関数なら,
違うデータ構造でも同じ関数を使えたはずです.
まさにそれを行ってくれるのがclassy-preludeです.
classy-preludeはクラス化された関数群を提供してくれます.
例えば,
先ほど述べたlookup
はclassy-preludeがre-exportするmono-traversableのData.Containersに定義され,
その定義元クラスであるIsMap
はList
とHashMap
両方にinstance
を提供してくれています.
classy-preludeのサポートするデータ構造(実際に実装されているのはmono-traversableですが)を使っている限りは,
import
で別名を付ける必要はありません.
classy-preludeがラップしてくれます.
ただ,
完全にbaseなどの内容をそのままクラス化できたわけではなく,
fromList
などはsetFromList
とmapFromList
に分けられていたりします.
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と違ってText
とByteString
を使っています.
String
を使っている既存プロジェクトを移行させるときは少し手を加える必要があるかもしれません.
classy-preludeを使うにはもともとのpreludeのimport
を省く必要があるので,
以下のようにプラグマをつけてimport
しましょう.
{-# LANGUAGE NoImplicitPrelude #-}
import ClassyPrelude
ついでにwhenM :: Monad m => m Bool -> m () -> m ()
などの便利な関数も付いてきます.
classy-preludeの問題
- 多くのデータ構造, 多くの機能を提供するため, 依存ライブラリがこれを指定するだけで爆発的に多くなります
- クラス化した代償で型推論が決定しなくなることがあります, その時は型注釈をしてあげましょう
- エラーがわかりにくくなります
- 基本的にre-exportしているラッパーなので関数の実装が何処に置いてあるのかわかりにくくなります