久々にHaskellでプログラムを書いて学んだこと (3): コーディングスタイルなど

コードフォーマット

最近はGo言語を書く機会もあり、そこでgo fmtいいなと思ったので、Haskellでもコードフォーマッタを導入することにした。

とりあえずstylish-haskellを使うことにした。Emacsで使うので、設定ファイルは自分のEmacs設定レポジトリに入れている。

やはりimportリストを勝手に整理してくれるのはありがたい。自分は基本的にnon-qualified wildcard importを使わず、各シンボルをexplicit importするタイプなのでimport listがすぐにぐちゃぐちゃになる。

とりあえずstylish-haskellにしてみたが、ormoluなんかでもよかったかもしれない。

Custom preludeっぽいモジュール

前述のように、自分は基本的にwildcard importはしないようにしている。つまり、基本的にimportは

import           Hoge (a,b,c)
import qualified Foo  as F

上記の2パターンのどちらかになる。wildcard importをやりだすとシンボルの出自が分からなくなり、書いた本人ですらコードが理解しにくくなるからだ。まあ、IDEをちゃんと使えばそんな心配いらなくなるのかもしれないが。

とはいえ、さすがにモジュールの数が多くなってくるとimport listを書くだけで一苦労になってくる。特に、どんなコードでも必ず使うようなシンボルを繰り返しimportに書くのはさすがに気が滅入ってくる。

ということで、そういったよく使うシンボルについてはまとめて一つのモジュールでimportしておいて、プロジェクトの他のコードはそのモジュールをimportするようにした。例えば、

module MyApp.Base
    ( module X
    ) where

import  Control.Applicative          as X (empty)
import  Control.Exception.Safe       as X (MonadCatch, MonadThrow,
                                           catch, handle, throw,
                                           throwIO, throwString)
import  Control.Monad                as X (forM_, forever, void, when)
import  Control.Monad.Logger         as X (MonadLogger (..))
import  Control.Monad.Reader         as X (MonadReader (..), ReaderT (..), runReaderT)
import  Control.Monad.Trans          as X (MonadIO (..), MonadTrans (..))
import  Control.Monad.Trans.Resource as X (MonadUnliftIO, MonadResource)
import  Crypto.Random                as X (MonadRandom (..))
import  Data.Aeson                   as X (FromJSON (..), ToJSON (..))
import  Data.ByteString              as X (ByteString)
import  Data.HashMap.Strict          as X (HashMap)
import  Data.Monoid                  as X (Monoid (..))
import  Data.Proxy                   as X (Proxy (..))
import  Data.Semigroup               as X (Semigroup (..))
import  Data.Text                    as X (Text)
import  Data.Traversable             as X (Traversable (..))
import  GHC.Exts                     as X (IsList (..))
import  GHC.Generics                 as X (Generic)
import  GHC.Records                  as X (HasField (..))

といったモジュールを書いておく。このモジュールではre-exportのテクニックを使う。

利用する側のモジュールは単にMyApp.Baseから普通にシンボルをimportする。

module MyApp.Foo

import MyApp.Base (MonadThrow, MonadIO, MonadReader, FromJSON, ToJSON, Generic)

依然としてexplicit importをするのでimport listは長くなるが、「あのtypeclassってどのモジュールにあったっけ?」と悩まずにガンガン書ける分、気分的には楽になる。

このやり方はrioclassy-preludeといったcustom preludeのアプローチに似ているが、個人的にはPreludeを置き換えるほどのこともないのではと考えている。うかつにPreludeを置き換えると、やはりシンボルの出自がよく分からなくなるのが怖い。

MyApp.Base相当のモジュールはプロジェクトの特質にあわせてプロジェクトごとに育てていけばいいように思う(チームで開発をするとなるともっと統制が必要かもしれないが)。何をBaseに突っ込むかは悩みどころだが、explicit importをする前提なら大抵のシンボルを放り込んでもそんなに問題ないだろう。ただ、将来的に分離する可能性がある依存パッケージのシンボルはあまりホイホイ突っ込むべきではないだろう。