Maybeの使い方
それではMaybeがどのように使われているかについて見ていきましょう。
Maybeの使い方の一つは,リストと同様に「答えがあるかどうかわからないものを表現する」ことです。これは前に説明した「実行時エラーの代わりに別の型を使用する」という話にもつながります。つまり,答えがないときに,実行時エラーを引き起こす代わりに,Maybeを使って「答えがあればJustに包んで返し,答えがなければNothingを返す」という処理を行わせるのです。
返り値としてMaybeを使用する関数の例を見てみましょう。Data.List(またはList)には,リスト中に指定した要素がある場合,その要素が最初に出現した場所を示すelemIndexという関数があります。
Prelude Data.List> :t elemIndex elemIndex :: (Eq a) => a -> [a] -> Maybe Int Prelude Data.List> elemIndex 3 [0..5] Just 3 Prelude Data.List> elemIndex 13 [0..5] Nothing Prelude Data.List> elemIndex 1 [3,1,4,1] Just 1
Data.ListにはelemIndicesという関数もあります。これは,リスト中に指定した要素がある場合,そのすべての場所をリストにして返す関数です。
elemIndexでは最初に出現した場所を返すためMaybeを使うのに対し,elemIndicesでは出現したすべての場所をリストにして返す,という形で型の使い分けが行われています。
Prelude Data.List> :t elemIndices elemIndices :: (Eq a) => a -> [a] -> [Int] Prelude Data.List> elemIndices 3 [0..5] [3] Prelude Data.List> elemIndices 13 [0..5] [] Prelude Data.List> elemIndices 1 [3,1,4,1] [1,3]
こうして得られたMaybe型を持つ答えに対する処理は,Maybeで定義されているfmapのように「パターン照合を使ってJustとNothingそれぞれの場合に対する処理を定義した関数」によって行います。これは,前回説明したListの場合と同様です。例えばPreludeのmaybe関数は,第3引数の値がJustである場合には第2引数の関数を適用し,第3引数の値がNothingである場合には第1引数に与えたデフォルトの値をそのまま返します。
Prelude Data.List> :t maybe maybe :: b -> (a -> b) -> Maybe a -> b Prelude Data.List> maybe 12 (\x -> 2*x) $ Just 2 4 Prelude Data.List> maybe 12 (\x -> 2*x) Nothing 12
Maybeのもう一つの使い方は,Maybeをオプションを与えるための引数として利用するというものです。こうした関数は,以下のような形で処理を行うことになります。
- 与えられた値がNothingであれば,オプションは与えられていない。このため,デフォルトの処理をする
- 与えられた値がJust xであれば,xという値がオプションとして与えられている。このため,デフォルトの処理の代わりに,xという値に基づいた処理をする
このような関数の例として,System.ProcessのrunInteractiveProcessという関数があります(現在のところHugsではSystem.Processモジュールをサポートしていないので,以下の例はGHCiでのみ実行できます。参考リンク)。runInteractiveProcessは,実行するファイルの名前とコマンドライン引数を渡し,別のプロセスでそのコマンドを実行する関数です。オプションとして,第3引数に作業ディレクトリを,第4引数に環境変数を与えることができます。runInteractiveProcessの型を見てみましょう。
Prelude System.Process System.IO> :t runInteractiveProcess runInteractiveProcess :: FilePath -> [String] -> Maybe FilePath -> Maybe [(String, String)] -> IO (Handle, Handle, Handle, ProcessHandle)
(,)で区切られて複数の型が列挙されている部分があるのに気付いたと思います。これは「組(tuple,タプル)」と呼ばれるものです。タプルは,二つ以上の複数の値を組にして一つの値にするのに使います(参考リンク)。リストとタプルの違いは,リストが同じ型の値しか持てないのに対し,タプルは異なった型の値を組として持てることです。リストは保持する要素数がいくつであっても同じくリスト型として識別されますが,タプルの場合には要素の数の違うものは異なる型として明確に区別されます。
runInteractiveProcessの第4引数は,環境変数とその値の「対(pair,ペア)」です。runInteractiveProcessの返り値は,stdin,stdout,stderrを示すハンドル(Handle)と実行したプロセスハンドル(ProcessHandle)の組です。ハンドルから実際の入出力に移すためには,hGetContentsなどSystem.IO(またはIO)のハンドルを操作するための関数を使う必要があります(参考リンク)。このため,評価に使用するモジュールにSystem.IOを加える必要があります。
GHCiでは以下のようにして複数のモジュールを評価に使用できます(残念ながらHugsでは同じことはできません。第2回で行ったように,ソース・ファイルに複数のモジュールをインポートするという定義を書いて,そのファイルを読み込む必要があります)。
Prelude System.Process> :m + System.IO Prelude System.IO System.Process>
または
Prelude System.Process> :m System.Process System.IO Prelude System.IO System.Process>
さて,それでは実際にrunInteractiveProcessを使って外部のコマンドを実行してみることにしましょう。「ふつうのHaskellプログラミング」の著者である青木峰郎氏が開発した,Haskellのリファレンス・マニュアルを検索するツール,「href」を使用することにします。なお,hrefで使われているリファレンス・マニュアルは,青木峰郎氏が自分で作成したものです。
最初にコード・ページ(文字コード)を設定します。Windows版のGHC6.6ではコード・ページが28591(Latin-1)に設定されているため,文字がエスケープされてしまいます。Windows版のGHC6.6を使って試す場合には,まずは:!コマンドを使ってコード・ページを変更するためのコマンドを実行してください。:!は,対話処理系内で外部のコマンドを実行するためのものです。runInteractiveProcessを単純化したラッパーと考えればよいと思います。ここではWindows日本語版のデフォルトのコード・ページである932(Microsoft Shift_JIS)に変更します(参考リンク)。
Prelude System.Process System.IO> :! chcp 932 現在のコード ページ: 932
まずはオプションを使用せずに実行してみましょう。普通にコマンドラインから実行するのと同じ結果が出力されます。
Prelude System.Process System.IO> (_,out,_,pid) <- runInteractiveProcess "href" ["putStr"] Nothing Nothing (↑実際には1行) Prelude System.Process System.IO> result <- hGetContents out ~ 略 ~ Prelude System.Process System.IO> putStr result Prelude.putStr putStr :: String -> IO () putStr str 文字列 str を標準出力 (stdout) に出力する。 see also: putStrLn, hPutStr, hPutStrLn -- Hello, World プログラム main = putStr "Hello, World!\n" Prelude System.Process System.IO> getProcessExitCode pid Just ExitSuccess
runInteractiveProcessの結果を取っているところで使っている「_」は「ワイルドカード・パターン(wildcard pattern)」と呼ばれるものです。「_」では束縛は発生しません。これは,以降の部分で値が決して参照されない場合に便利です(参考リンク)。
hGetContentsはハンドルから入力全体を読み込みます。printは改行文字などを解釈せずにエスケープした形で出力するため,ここでは文字を適切に解釈した形で出力してくれるputStrを使っています。
最後に実行しているgetProcessExitCodeはプロセスの終了を待つ関数です。対象となるプロセスが終了すれば終了コード(ExitCode)をJustに包んで返し,終了しなければNothingを返します。対話処理系を使うときにはあまり関係ありませんが,実行ファイルとしてプログラムを作成する場合には,外部のプログラムをきちんと実行させるのに必要な処理になります。getProcessExitCodeを使って最後にあと片付けをする習慣をつけておくとよいでしょう。
Prelude System.Process System.IO> :t getProcessExitCode getProcessExitCode :: ProcessHandle -> IO (Maybe GHC.IOBase.ExitCode)
では,いよいよオプションとして値を与えてやることにします。hrefは,リファレンス・マニュアルが存在するディレクトリを探すためにHREF_DATADIR環境変数を使っています。これをリファレンス・マニュアルが存在しないディレクトリに変更することで,検索できなくなって内容が表示されなくなります。なお,ペアの順番は(環境変数,値)であって,(値,環境変数)ではないことに注意してください。
Prelude System.IO System.Process> (_,out,_,pid) <- runInteractiveProcess "href" ["putStr"] Nothing (Just [("HREF_DATADIR","home")]) (↑実際には1行) Prelude System.IO System.Process> result <- hGetContents out "" Prelude System.IO System.Process> putStr result Prelude System.IO System.Process> getProcessExitCode pid Just (ExitFailure 1)
System.Processには,今回説明した関数のほかに様々な派生形(variation)が存在します。詳しくはライブラリのドキュメントを見てください(参考リンク)。