修飾名を付ける

基本的に、Haskell でモジュールを import する際には、ちゃんと修飾名を付けるべきです。

これは名前の衝突を防ぐためです。

例えば以下のコードはコンパイルエラーになります。 putStrLnData.TextData.ByteString の両方に存在しているからです。

import Data.Text
import Data.ByteString

main = putStrLn ""

これが衝突しなかったら自由に import していいのかというと、これもやめておいた方がいいでしょう。

というのも、依存パッケージをバージョンアップした時に関数が増えた場合、その関数が他のパッケージの名前を重複する場合があるからです。

module Module.Oreore (oreore) where
oreore = undefined
module Module.Moremore (moremore) where
moremore = undefined

というモジュールがあり、以下の様に利用していたとします。

import Module.Oreore
import Module.Moremore

main = oreore

この後、Module.Moremore モジュールが以下の様に更新されてしまうと…。

module Module.Moremore (moremore, oreore) where
moremore = undefined
oreore = undefined
import Module.Oreore
import Module.Moremore

main = oreore -- コンパイルエラー!

oreore という名前が重複してしまい、コンパイルエラーになります。

この現象のひどいところは、元のソースコードを一切行っていないにも関わらず発生する可能性があることです。 依存するパッケージが、何気ないつもりで関数を一個追加しただけで、コンパイルが通らなくなる可能性があるのです。

このような悲劇を防止するためにも、修飾名を付けるか、あるいは関数名を明示的に指定するべきです。

import qualified Module.Oreore as O
import qualified Module.Moremore as M

main = O.oreore

-- あるいは以下の様にする
import Module.Oreore (oreore)
import Module.Moremore (moremore)

main = oreore

修飾名のルール

自分が作っている Yesod のプロジェクト(例えば melpon.org)では、可能な限り修飾名を統一しています。 これは、ファイルによって異なる修飾名にされると、非常に読みにくいからです。

import qualified Data.Text as T
import qualified Control.Monad as M
import qualified Data.Text as Text
import qualified Data.Map as M

Data.Text の修飾名が T だったり Text だったりと、統一されていません。 また、M という修飾名が Data.Map だったり Control.Monad だったりと統一されていません。

ファイルによって修飾名を変えられたコードを読むのは非常に大変です。 これはプロジェクト全体で統一してやるべきです。

自分の場合は、以下の様な ImportRule.hs を用意し、それを目的のファイルにコピペしています。

{-# OPTIONS_GHC -w #-}
module ImportRule () where

import qualified Control.Monad                          as Monad
import qualified Control.Monad.Logger                   as MonadLogger
import qualified Control.Applicative                    as Applicative
import qualified Control.Exception                      as Exc
import qualified Control.Concurrent                     as Concurrent
import qualified Control.Concurrent.Chan                as Chan

import qualified Data.Aeson                             as Aeson
import qualified Data.Aeson.Types                       as AesonTypes
import qualified Data.Bits                              as Bits
import qualified Data.ByteString                        as BS
import qualified Data.ByteString.Char8                  as BSC
import qualified Data.ByteString.Lazy                   as BSL
import qualified Data.Char                              as Char
import qualified Data.Conduit                           as Conduit
import qualified Data.Conduit.Binary                    as ConduitB
import qualified Data.Conduit.List                      as ConduitL
import qualified Data.Default                           as Default
import qualified Data.Function                          as Func
import qualified Data.IORef                             as IORef
import qualified Data.List                              as List
import qualified Data.Maybe                             as Maybe
import qualified Data.Text                              as T
import qualified Data.Text.IO                           as TIO
import qualified Data.Text.Encoding                     as TE
import qualified Data.Time                              as Time
import qualified Data.Word                              as Word
import qualified Data.Yaml                              as Yaml

import qualified Language.Haskell.TH                    as TH
import qualified Language.Haskell.TH.Syntax             as THS

import qualified Network.HTTP.Conduit                   as HConduit
import qualified Network.Wai.Logger                     as WaiLogger
import qualified Network.Wai.Middleware.RequestLogger   as RequestLogger
import qualified Network.Wai.Handler.Warp               as Warp

import qualified System.Directory                       as Directory
import qualified System.Exit                            as Exit
import qualified System.IO                              as I
import qualified System.Locale                          as Locale
import qualified System.Log.FastLogger                  as FastLogger
import qualified System.Mem                             as Mem
import qualified System.Environment                     as Environment

import qualified Text.Hamlet                            as Hamlet
import qualified Text.Hamlet.RT                         as HamletRT
import qualified Text.Jasmine                           as Jasmine
import qualified Text.Shakespeare.Text                  as Text

import qualified Yesod                                  as Y
import qualified Yesod.AtomFeed                         as YAtomFeed
import qualified Yesod.Core.Types                       as YCoreTypes
import qualified Yesod.Default.Config                   as YDConfig
import qualified Yesod.Default.Handlers                 as YDHandlers
import qualified Yesod.Default.Main                     as YDMain
import qualified Yesod.Default.Util                     as YDUtil
import qualified Yesod.Feed                             as YFeed
import qualified Yesod.RssFeed                          as YRssFeed
import qualified Yesod.Static                           as YStatic

これは別に .hs ファイルにする必要は無く、単なるテキストファイルに書いておいても構いません。 .hs にしているのは、vim でシンタックスハイライトした表示にするのが楽だからというだけです。

ひとまず、こうすることで、プロジェクト全体で統一した名前になり、コードが読みやすくなりました。

例えば以下のコードは Yesod が自動生成したコードです。

import Yesod
import Yesod.Default.Config
import Yesod.Core.Types
import Network.Wai.Middleware.RequestLogger
    ( mkRequestLogger, outputFormat, OutputFormat (..), IPAddrSource (..), destination
    )
import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
import Data.Default

import Foundation
import Settings
import Settings.Development

makeApplication :: AppConfig DefaultEnv Extra -> IO Application
makeApplication conf = do
    foundation <- makeFoundation conf

    -- Initialize the logging middleware
    logWare <- mkRequestLogger def
        { outputFormat =
            if development
                then Detailed True
                else Apache FromSocket
        , destination = RequestLogger.Logger $ loggerSet $ appLogger foundation
        }

    -- Create the WAI application and apply middlewares
    app <- toWaiAppPlain foundation
    return $ logWare app

これが、ルールに従って書くと、以下の様になります。

import qualified Yesod                                  as Y
import qualified Yesod.Default.Config                   as YDConfig
import qualified Yesod.Core.Types                       as YCoreTypes
import qualified Network.Wai.Middleware.RequestLogger   as RequestLogger
import qualified Data.Default                           as Default

import Foundation (App(appLogger))
import Settings (Extra)
import Settings.Development (development)

makeApplication :: YDConfig.AppConfig YDConfig.DefaultEnv Extra -> IO Y.Application
makeApplication conf = do
    foundation <- makeFoundation conf

    -- Initialize the logging middleware
    logWare <- RequestLogger.mkRequestLogger Default.def
        { RequestLogger.outputFormat =
            if development
                then RequestLogger.Detailed True
                else RequestLogger.Apache RequestLogger.FromSocket
        , RequestLogger.destination = RequestLogger.Logger $ YCoreTypes.loggerSet $ appLogger foundation
        }

    -- Create the WAI application and apply middlewares
    app <- Y.toWaiAppPlain foundation
    return $ logWare app

自分としては、これで大分読みやすくなったかなと思います。

デメリットは、新しいモジュールを追加する場合、ImportRule.hs に記述してから目的のファイルに記述することになり、手間が増えることです。 正直、これぐらい Haskell の機能として用意して欲しいところですが…。 無いものは仕方がないので、ひとまずこの方法でやっています。