上田ブログ

生きるフリー素材化への厳しい修行(生きるフリー素材だとは言っていない)

お知らせ: 本買ってくださーい / 

  このエントリーをはてなブックマークに追加 
   

USP Magazine 2015年1月号「Haskell版Open usp Tukubai完成させるぞ企画: Haskellでやってはいかんのか?」

10. Haskell版Open usp Tukubai完成させるぞ企画: Haskellでやってはいかんのか?

産業技術大学院大学・USP研究所・USP友の会 上田隆一

Open usp TukubaiのHaskell版がまるでサグラダ・ファミリア 状態なので,連載しながら開発しようと思い立った上田は, 不幸にも黒塗りの高級車に追突してしまう.後輩をかばい すべての責任を負った上田に対し,車の主,暴力団員谷岡に 言い渡された示談の条件とは....

10.1. はじめに

  [1] [2] [3] .富山の産んだ, 知の阪神 [4] ブラックエンジェル上田です. 前回までシェル芸勉強会の問題をHaskellで解いて自己満足していた本連載ですが, マンネリ化していたので,年が変わって1月号に入ってしまったのをいいことに, 編集部に無断で新しいことをやりたいと思います [5]

 んで,やることは冒頭で谷岡に言い渡されたように, Open usp TukubaiのHaskell版 [6] を完成させる.つまりHaskellでコマンドを作る ということです.Haskell版はわてくしが趣味でコツコツ作っていたのですが, 現在,非常に中途半端(半分くらい作って止まっている)な状態です. こいつを連載のついでに進めたい, という個人的一粒で二度美味しい感じでやってこうと思います.

 サンプルコードを含んだブランチを

に用意しておきますので,参考にしながら Haskellを勉強してみてください. コマンド作りということで, 内容も一段と実戦的になります.

10.2. mapを作る

 ということでハエある最初の未実装コマンドには, map を選びました. map はクロス集計の表を作るコマンドで, 基本的には図1のような挙動を示します. num=2 というのは左から2列をキー扱いするという意味で, map の出力を見るとキーが縦軸, キーの隣のフィールド(AとかBとか書いてある列) が横軸になってデータが出力されています. これ,awkでやろうとすると結構面倒なので, 無いと困るコマンドなのですがまだHaskell版では未実装です. 残念無念.切腹. いや,切腹はやめて実装することにします.

  • 図1: mapの挙動
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
uedambp:~ ueda$ cat data
001 鎌田 A 1
001 鎌田 B 2
002 濱田 A 3
002 濱田 B 4
003 上田 B 1
uedambp:~ ueda$ map num=2 data
* * A B
001 鎌田 1 2
002 濱田 3 4
003 上田 0 1
###ketaコマンドで整形###
uedambp:~ ueda$ map num=2 data | keta
 * * A B
001 鎌田 1 2
002 濱田 3 4
003 上田 0 1

10.3. まずファイルを読み込めるようにする

 コマンドを作るときは,まず cat(1) コマンドに相当する 部分から書いていきます. 書いて行くというよりは,どこかからコピーしていきます. コピーするならライブラリにまとめればいいじゃんという 意見もあると思いますが,コマンドは1個のファイルを コンパイルしたら使えるというのが理想なので, コピーで済ませます.

 そこまで書いたものを図2に示します. 実行してみると図3のような出力になります.

  • 図2: ファイルをcatするmap.1.hs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import System.Environment
import Data.ByteString.Lazy.Char8 as BS

main :: IO ()
main = do args <- getArgs
 case args of
 [num] -> readF "-" >>= BS.putStr
 [num,file] -> readF file >>= BS.putStr

readF :: String -> IO BS.ByteString
readF "-" = BS.getContents
readF f = BS.readFile f
  • 図3: map.1の出力
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
###コンパイル###
uedambp:COMMANDS.HS ueda$ ghc -O2 map.1.hs
[1 of 1] Compiling Main ( map.1.hs, map.1.o )
Linking map.1 ...
###元のデータ###
uedambp:COMMANDS.HS ueda$ cat ~/data
001 鎌田 A 1
001 鎌田 B 2
002 濱田 A 3
002 濱田 B 4
003 上田 B 1
###ファイル名を指定###
uedambp:COMMANDS.HS ueda$ ./map.1 num=1 ~/data
001 鎌田 A 1
001 鎌田 B 2
002 濱田 A 3
002 濱田 B 4
003 上田 B 1
###パイプを使う(その1)###
uedambp:COMMANDS.HS ueda$ cat ~/data | ./map.1 num=1
(出力略)
###パイプを使う(その2)###
uedambp:COMMANDS.HS ueda$ cat ~/data | ./map.1 num=1 -
(出力略)

 さて,図2の解説をしていきます. シェル芸のときの解説と違っていきなり結構難しいですが, 落ち着いて読んでいきましょう. まず,10行目からの readF 関数ですが, これはファイルあるいは標準入力を読んで別の関数に渡す関数です. パターンマッチを使っており, 11, 12行目はそれぞれ標準入力,ファイルから読む場合に対応しています.

 ファイルから読み込んだ文字列は,String型ではなく, これまでも使ってきたByteString型として読み込みます. これまでのシェル芸ならともかく, コマンドだとスピードが求められ, そうなるとString型では手に負えなくなります. 読み込むモジュールは2行目のように Data.ByteString.Lazy.Char8 を選びます.

 10行目を見ると分かるように, readF 関数の出力は IO BS.ByteString となります. BS.ByteString なら分かりやすいのですが, ファイルから読み込んでいるので頭に IO がくっつきます.

 11, 12行目で使っている関数の型を一応,図4に示しておきます. readFile の引数にはこれまで連載中に何回も出て来た FilePath という型が出て来ますが, この図のように :i で型の情報を調べることができます. これを見ると, String型の別名であることが分かります.

  • 図4: getContentsとreadFileの型とFilePathの正体
1
2
3
4
5
6
7
8
9
uedambp:USPMAG ueda$ ghci
(略)
Prelude> import Data.ByteString.Lazy.Char8 as BS
Prelude BS> :t BS.getContents
BS.getContents :: IO ByteString
Prelude BS> :t BS.readFile
BS.readFile :: FilePath -> IO ByteString
Prelude BS> :i FilePath
type FilePath = String -- Defined in `GHC.IO'

 さて, readF が分かったところで [7] , 次に main 関数に移りましょう. まず5行目の左辺の getArgs の説明をします. この関数は引数を読み込むためにあります. 図5に型を示しますが,出力がString型のリストに IOをくっつけたものであることが分かります. 5行目では,このリストに <-args という名前を付けています. で, main 関数は2014年9月号でも出て来た do で書かれているので, 普通の手続き型の言語のように次行に処理の流れが進みます.

  • 図5: getArgsの型
1
2
3
Prelude> import System.Environment
Prelude System.Environment> :t getArgs
getArgs :: IO [String]

 6行目では本連載で始めてとなる「case式」 が出てきました.「case文」ではなく「case式」です. case式では分岐の基準となるもの(ここではargs) のパターンを書いていき,場合分けを表現します. パターンは -> の左側,パターンにマッチした時の処理は右側に書きます. 7行目は args ,つまり引数が一つであるとき(例: map num=1 ) のようなときに適合します. 8行目はファイル名の指定があるときに適合します. この場合分けと, readF 関数のパターンマッチによって, ファイル名が指定されたとき,指定されなかったとき, ファイル名の代わりに「 - 」が指定されたときで, 読み込む先がファイル名になったり標準入力になったりと, 適切な挙動が実現されます.

 引数がそれ以外のパターンの場合は,この例では次のように エラーとなります.

1
2
uedambp:COMMANDS.HS ueda$ ./map.1 hoge hoge hoge
map.1: map.1.hs:(6,11)-(8,52): Non-exhaustive patterns in case

10.4. usageをつける

 次に,コマンドの使い方を説明するusageをつけます.usageは, 引数のパターンにマッチしなかったときに表示することにしましょう. 図6のようにヘルプを付け加えました. ヘルプを出す関数が showUsage で, それを main 関数のcase式で パターンにマッチするときに呼び出しています. パターンの一番下,18行目の _ は,上のパターンが全て マッチしなかったときにマッチする記号です.

  • 図6: map.2.hs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import System.Environment
import Data.ByteString.Lazy.Char8 as BS
import System.IO

showUsage :: IO ()
showUsage = do System.IO.hPutStr stderr (
 "Usage : map <num=<n>> <file> \\n" ++
 "Thu Oct 23 08:52:44 JST 2014\\n" ++
 "Open usp Tukubai (LINUX+FREEBSD+Mac), Haskell ver.\\n")

main :: IO ()
main = do args <- getArgs
 case args of
 ["-h"] -> showUsage
 ["--help"] -> showUsage
 [num] -> readF "-" >>= BS.putStr
 [num,file] -> readF file >>= BS.putStr
 _ -> showUsage

readF :: String -> IO BS.ByteString
readF "-" = BS.getContents
readF f = BS.readFile f

 標準エラー出力に何かを出力するときには, 6行目のように System.IO.hPutStr を使います. この関数を使用するために,3行目で System.IO をインポートしました. 型を見ると次のようになっており, GHC.IO.Handle.Types.Handle に出力先 ( map.2.hs の場合は stderr ) を指定し,その後に出したい文字列をString型で指定します.

1
2
Prelude> :t System.IO.hPutStr
System.IO.hPutStr :: GHC.IO.Handle.Types.Handle -> String -> IO ()

  map.2.hs でちゃんと出力されるか確認しておきましょう. 図7のように出たらOKです.

  • 図7: map.2.hsの動作確認
1
2
3
4
5
6
7
8
9
uedambp:COMMANDS.HS ueda$ ghc -O2 map.2.hs
[1 of 1] Compiling Main ( map.2.hs, map.2.o )
Linking map.2 ...
uedambp:COMMANDS.HS ueda$ ./map.2
Usage : map <num=<n>> <file>
Thu Oct 23 08:52:44 JST 2014
Open usp Tukubai (LINUX+FREEBSD+Mac), Haskell ver.
uedambp:COMMANDS.HS ueda$ ./map.2 -h
(略)

10.5. 終了ステータスの指定

 さて,これで今号は最後にしておきますが, usageを表示したら終了ステータス1を返すようにしておきましょう. コマンドは,終了ステータスで呼び出し元に処理がうまくいったか どうかを返すわけですが,usageを出すのは そのコマンドの本職の処理ではないので,異常を示す1を返します.

 図8に終了ステータスの処理を加えた map.3.hs と,その動作を示します.加えたと言っても,4行目に System.Exit というモジュール, 11行目に exitWith という関数を加えただけです.

  • 図8: map.3.hsのコードと動作確認
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import System.Environment
import Data.ByteString.Lazy.Char8 as BS
import System.IO
import System.Exit

showUsage :: IO ()
showUsage = do System.IO.hPutStr stderr (
 "Usage : map <num=<n>> <file> \\n" ++
 "Thu Oct 23 08:52:44 JST 2014\\n" ++
 "Open usp Tukubai (LINUX+FREEBSD+Mac), Haskell ver.\\n")
 exitWith (ExitFailure 1)
(以下略)
uedambp:COMMANDS.HS ueda$ ghc -O2 map.3.hs
[1 of 1] Compiling Main ( map.3.hs, map.3.o )
Linking map.3 ...
uedambp:COMMANDS.HS ueda$ ./map.3
Usage : map <num=<n>> <file>
Thu Oct 23 08:52:44 JST 2014
Open usp Tukubai (LINUX+FREEBSD+Mac), Haskell ver.
###終了ステータスの確認###
uedambp:COMMANDS.HS ueda$ echo $?
1

  exitWith の型は次の通り.Int型を引数にとるわけではないので, ExitFailure というデータコンストラクタを使っています. 面倒ですが,型のためには面倒を厭わないのがHaskellerというものです [8]

1
2
Prelude> :t System.Exit.exitWith
System.Exit.exitWith :: GHC.IO.Exception.ExitCode -> IO a

10.6. おわりに

 今回はHaskell版のOpen usp Tukubaiを作るにあたっての 基礎の部分である,ファイルの読み込み,引数の読み込み, usageの表示,終了ステータスの操作を扱いました. 次号から,mapの処理を書いて行きましょう.

脚注

[1]こん
[2]
[3]ちわ
[4]編集部注: 「知の巨人」にかけているらしいです.
[5]編集部御中,自由すぎてすんません.
[6]https://github.com/usp-engineers-community/Open-usp-Tukubai/tree/master/COMMANDS.HS
[7]うーん.どうなんでしょう?どれだけの人がついてきているかどうかは,実際にはよく分からなかったり.@ryuichiuedaで公開でどんな感じかお伝えしていただければ,と.
[8]いや・・・面倒です.


Article Info

created: 2015年 5月 4日 月曜日 12:53:27 JST
modified: 2017年 9月 22日 金曜日 23:27:45 JST
views: 633
keywords: