あどけない話

Internet technologies

HaskellとgetOpt

最近では、何かプログラムを書くときは、Haskell を使うようにしています。Haskellスクリプトを書くと困ることの一つに、コマンドライン・オプションの処理があります。

IOモナド地獄

System モジュールで定義されている getArgs は IO [String] を返します。そこで、型が IO () である main などから以下のように使うことになります。

-- "-c" オプションを調べる
import System
main = do argv <- getArgs
          let cflag = "-c" `elem` argv
          -- ここに何か書く

この方法には2つ問題があります。

  • main で getArgs を使うと、コマンドライン・オプションの処理結果(cflag など)を下位の関数にずっと渡していかないといけない
  • コマンドライン・オプションの処理結果が必要な関数で getArgs を使うと、本来純粋であるべきその関数が IO に侵される

やはり、コマンドライン・オプションの処理結果は、

  • グローバルにアクセスでき
  • IO に侵されてない純粋なデータである

べきだと思います。

禁断の unsafePerformIO

長い間、この問題に悩んでいたのですが、"Tackling the Awkward Squad" を読んで、ようやく解決しました。

unsafePerformIO を使うんですね。

unsafePerformIO は、型が IO a -> a であることから分るように、IO モナドの中身を取り出します。

関数名が示唆する通り危険なので、注意して利用しなければいけません。上記の文献では、以下のようなパターンに使うとよいと書かれています。

  • 設定ファルのように実行中一回しか触らないファイルの読み書き
    • たとえば関数 configFileContents の中
  • グローバル*変数*の割り当て
noOfOpenFiles :: IORef Int 
noOfOpenFiles = unsafePerformIO (newIORef 0)
trace :: String -> a -> a 
trace s x = unsafePerformIO (putStrLn s >> return x)

コマンドライン・オプションは、最初のパターンに当てはまりますから使ってよいでしょう。

すなわち、

unsafePerformIO getArgs

で、[String] が得られる訳です。

サンプル

Haskell には、標準で System.Console.GetOpt というモジュールがありますので、これを使った例を示します。

以下は、C コンパイラの一般的なコマンドライン・オプションである "-c" と "-o FILE" をチェックする例です。

ParseArgs を import すると、解析済みのコマンドライン・オプションが options に、ファイル引数が files に定義されます。

必要な場所でそれぞれ

  • optCompile options
    • 型は Bool
  • optOutput options
    • 型は Maybe FilePath

とチェックして下さい。

不適切なコマンドライン・オプションを指定すると、usage も表示します。v^^)

module ParseArgs (progName, files, options, Options(..)) where

import System
import System.IO.Unsafe
import System.Console.GetOpt

progName :: String
progName = unsafePerformIO getProgName

options :: Options
files :: [String]
(options,files) = parseArgs argspec (unsafePerformIO getArgs)

parseArgs :: [OptDescr (Options -> Options)] -> [String] -> (Options, [String])
parseArgs spec argv
    = case getOpt Permute spec argv of
        (o,n,[]  ) -> (foldl (flip id) defaultOptions o, n)
        (_,_,errs) -> error (concat errs ++ usageInfo usage argspec)

----------------------------------------------------------------
-- Edit from here
----------------------------------------------------------------

usage :: String
usage = "Usage: " ++ progName ++ " [options] [file]"

data Options   = Options { optCompile :: Bool
                         , optOutput :: Maybe FilePath
                         } deriving Show

defaultOptions :: Options
defaultOptions = Options { optCompile = False
                         , optOutput = Nothing
                         }

argspec :: [OptDescr (Options -> Options)]
argspec = [ Option ['c'] ["compile"]
            (NoArg (\opts -> opts { optCompile = True }))
            "compile but not link"
          , Option ['o'] ["output"]
            (ReqArg (\d opts -> opts { optOutput = Just d }) "FILE")
            "output file"
          ]