record update
Haskellのrecordを使っているとこういう事をよくやる。
data Test = T { a :: Int, b :: String } updateA :: Test -> (Int -> Int) -> Test updateA t f = t { a = f (a t) }
Test型のデータのaをf関数を使って更新したい。
例えばほら、Stateにはmodify関数があるわけじゃないですか。
modify :: (s -> s) -> State s ()
という感じで、sを更新する関数を引数にとる。実際とはちょっと違うけど。でもレコードでもこういう感じの事がしたい。
具体的にはこう。
update name f rec = rec { name = f (name rec) }
recordのフィールドラベルまで引数として渡したいわけです。これコンパイルできないんですけどね。
さてどうしたものだろうかと思っていたところ、2010年にHaskell-cafeで同じ事を言っている人がいらっしゃいました。
[Haskell-cafe] record update
どうやらよくあるどうしようもない話題みたいです。最初の人がやりたいことと大体同じで、例としてState.modifyを挙げるところまで同じというかまさに私もsomeStateTransformみたいな事が頻出するコードを書いていてどうにかならんもんかと思って調べ始めたんですけど。
Jonathan Geddesさんの例:
someStateTransform :: State MyRecord () someStateTransform = do modify $ \record->record{field1 = (++"!") $ field1 record} ...
これは書きたくないよねぇ。
で、このHaskell-cafeのrecord updateスレは結構盛り上がってて、いろんな案が出てきたんですが決定打が出ないまま終わってしまいます。話自体は結構盛り上がってて、いろんなライブラリが紹介されててそれはそれで面白かったんですが。
しょうがない、やるか。
-- \f r -> r { label = f (label r) } upd :: Name -> ExpQ upd label = do f <- newName "func" r <- newName "rec" lamE [varP f, varP r] (recUpdE (varE r) [(,) label <$> [|$(varE f) ($(varE label) $(varE r))|]] )
これを使ってこう。
data Test = T { a :: Int, b :: String } deriving (Show) main :: IO () main = do let test = T 1 "abc" print test -- => T {a = 1, b = "abc"} print ($(upd 'a) succ test) -- => T {a = 2, b = "abc"}
TemplateHaskellで作ればいいじゃないという。いいんだけどなんか、微妙に微妙。
exercises/record/Main.hs at master · yunomu/exercises · GitHub
そしてやっぱり似たような事を考える人はいるのでした。
mkEditor - Data.SemanticEditors
この場合は式じゃなくて関数定義を作ってますけど。
どがんかならんでしょうか。
jhcをビルドする
環境:Mac OS X 10.8.2, GHC 7.4.2
jhcはだいたい下記の記事に書いてあるとおりなんですが(autoreconfやaclocalが無い時はautoconfとautomakeをMacPortsとかでインストールするとたぶん大丈夫)、いろいろ引っかかりましたので。
簡約!λカ娘(4)の紹介とjhcのすゝめ - Metasepi
darcsは公式サイトから落としてくる。
Darcs - Download Darcs
バイナリで配布されているので、解凍してパスの通ったディレクトリに置くだけ。
で、いざやってみると、
% darcs get http://repetae.net/repos/jhc % cd jhc % autoreconf -i % ./configure % make (略) DrIFT src/C/FFI.hs -o drift_processed/C/FFI.hs make: DrIFT: No such file or directory make: *** [drift_processed/C/FFI.hs] Error 1
という感じになる。DrIFTとはなんぞや。
cabalで探すとそれらしいのが見つかる。
% cabal list DrIFT [jhc] * DrIFT-cabalized Synopsis: Program to derive type class instances Default available version: 2.2.3.2 Installed versions: [ Unknown ] Homepage: http://repetae.net/computer/haskell/DrIFT/ License: BSD3 * pugs-DrIFT Synopsis: DrIFT with pugs-specific rules. Default available version: 2.2.3.20120717 Installed versions: 2.2.3.20120717 Homepage: http://pugscode.org/ License: BSD3
よくわからないけどDrIFT-cabalizedはインストールできないし、pugs-DrIFTは違うものっぽい。
けどもサイトの方にソースアーカイブがおいてあるので、それを取ってきて自分でビルドすればいいじゃないということになる。
http://repetae.net/computer/haskell/DrIFT/drop/
% wget http://repetae.net/computer/haskell/DrIFT/drop/DrIFT-2.2.3.tar.gz % tar xvzf DrIFT-2.2.3.tar.gz % cd DrIFT-2.2.3 % make Making all in src make all-am /usr/bin/ghc -i. -i. -hidir . -odir . -o DrIFT --make ./DrIFT.hs DrIFT.hs:20:18: Could not find module `System' It is a member of the hidden package `haskell98-2.0.0.1'. Use -v to see a list of the files searched for. make[2]: *** [DrIFT] Error 1 make[1]: *** [all] Error 2 make: *** [all-recursive] Error 1
うえええ
どうもhaskell98に依存してるっぽくて、cabalにあったDrIFT-cabalizedがインストールできないのもここ関係のもよう。
仕方ないのでcabalファイルを書く。こんなものでよかろう。
DrIFT.cabal
name: DrIFT version: 2.2.3 build-type: Simple cabal-version: >=1.8 executable DrIFT main-is: DrIFT.hs build-depends: haskell98 hs-source-dirs: src
これを使ってビルドする。
% cabal-dev build Building DrIFT-0.1.0.0... Preprocessing executable 'DrIFT' for DrIFT-0.1.0.0... [ 6 of 23] Compiling GenUtil ( src/GenUtil.hs, dist/build/DrIFT/DrIFT-tmp/GenUtil.o ) src/GenUtil.hs:486:18: Could not deduce (Show a) arising from a use of `st' from the context (Integral a) bound by the type signature for showDuration :: Integral a => a -> String at src/GenUtil.hs:(486,1)-(491,28) Possible fix: add (Show a) to the context of the type signature for showDuration :: Integral a => a -> String In the first argument of `(++)', namely `st "d" dayI' In the expression: st "d" dayI ++ st "h" hourI ++ st "m" minI ++ show secI ++ "s" In an equation for `showDuration': showDuration x = st "d" dayI ++ st "h" hourI ++ st "m" minI ++ show secI ++ "s" where (dayI, hourI) = divMod hourI' 24 (hourI', minI) = divMod minI' 60 (minI', secI) = divMod x 60 st _ 0 = "" st c n = show n ++ c
これは普通にコンパイルエラーを直してしまう。。
src/GenUtil.hs:486行目のshowDuration関数のシグネチャでクラス制約を付け足せばよい。
- show Duration :: Integral a => a -> String + show Duration :: (Integral a, Show a) => a -> String
あとは普通にビルドできます(たぶん)。
% cabal-dev build ... Linking dist/build/DrIFT/DrIFT
出来上がったものをパスが通ったディレクトリに置けばOK。
jhcに戻ってビルドする。
% make ... pandoc jhc_man.mkd -s -f markdown -t man -s -o jhc.1 make[2]: pandoc: No such file or directory make[2]: *** [man] Error 1 rm jhc_man.mkd options.mkd make[1]: *** [jhc.1] Error 2 make: *** [all] Error 2
おおぅpandocさん……。
あとはどうにかしてpandocをインストールして、sudo make installで終わりです。
Haskellで特定のモジュールのコンパイルが遅すぎる
1つのモジュールのコンパイルに時間がかかりすぎて、5分間何も出力がなかったとしてビルド失敗扱いされるの、つらいものがある
— 就活用アカウントさん (@eagletmt) 2013年1月24日
という話があって。具体的にはaws-sdk(https://github.com/worksap-ate/aws-sdk)の開発中の話で、
aws-sdkではTravis CI(https://travis-ci.org/)を使ってビルドだけのテストをしているんですが、そのテストっていうかビルドが頻繁に失敗するようになってしまった。
原因はAWS.EC2.Typesのコンパイルで、このモジュールのコンパイルだけで5分以上かかる。その間何も画面出力などはされなくて、Travisでは何も出力がされないまま5分経過するとビルドプロセスが殺されてしまうという。
これはいけませんというか、いや別に普通にTravis以外ではビルドできてるからいいといやいいんだけど、これでTravisを切り捨てるのもかっこ悪いしねぇどうしようかと。
それとは別なのかもしれないけど32bit版LinuxだとAWS.EC2.Typesのコンパイル中にghcがsegmentation faultで落ちるという問題もあって、いい加減このモジュールどうにかした方がいいんじゃないかと思っていました。
当時のソースはこんな感じ。
aws-sdk/AWS/EC2/Types.hs at v0.9.2.1 · worksap-ate/aws-sdk · GitHub
データ定義が140個並んでいて、全部がEq, Show, Readのインスタンスになってて、あとその中でFromTextクラスのインスタンス化してる型が、TemplateHaskellを使うものが64個、TemplateHaskellを使わないものが8個という感じ。72個。
これ一見TemplateHaskellが重いのかなぁと思うんですけど、TemplateHaskellが絡むモジュールだけを分割しても大して変わらないし、モジュールを大雑把に2つに分けても結局その根本のAWS.EC2.Typesのコンパイル時間はあまりかわらないし、なんなんだこれと思って、exportするシンボルの数が多いとか最適化とかそういうのかなぁでもデータ定義しか無いし、
travisは要するに5分間標準出力に何も書き込まれなかったからコンパイラのプロセスを殺すということのようなので、コンパイル時に小話を出力するスレッドを作るとかそういうアレでなんとかなるんじゃないか
— Yusuke Nomuraさん (@yunomu11) 2013年1月25日
などとバカな話も考えはじめていたんですが、@yunomu11 AWS/EC2/の中にTypesってディレクトリを作ってその中に分散させるというのはどうですか?
— あむさん (@amkkun) 2013年1月25日
との提案を受けたので、1日かけてもいいのでよろしくって言って彼にやってもらいました。私が最初にやったテキトウな分割と違って、きちんとデータ間の依存関係を調べて分割してくれました。すげえ、ありがとうございます。
結果: aws-sdk/AWS/EC2/Types at f43a8b589db1271c9183339873a2115e61a7d9ce · worksap-ate/aws-sdk · GitHub
diff: Split EC2/Types. · f43a8b5 · worksap-ate/aws-sdk · GitHub
で、これをやった結果、ファイルが19個増えたものの、AWS.EC2.Typesのコンパイルが速くなるってもんじゃなくて、コンパイル時間が全体で60%ほど短くなりました。いや短くなりすぎだろうという気もしますが、短くなったものは仕方ないじゃないか。
ともあれ成果出てよかった。
よく考えてみるとHaskellのデータ構造ってC言語みたいな単純なメモリマップとは違って、コードに近いんですよね多分。いやghcが吐くコードがどんなものか知りませんけど。
まあそれが関係あるのかどうかはわかりませんけど、データ間の依存関係の計算に時間がかかってたんじゃないかなぁと。@amkkunが分割してくれた結果、ある程度の依存関係はモジュール分割の段階で解決してしまっているので、それで劇的に速くなったんじゃないかなぁ。同一モジュール間でデータの関係とか調べるのかな、そうだとしたら140個は多すぎるよな。
そういう感じでした。
そもそもexportが重いとかだったらAWS.EC2もかなり重くなってしかるべきなので、その予想はさすがに無かったかな。
ということで、シンボルが多すぎる時は気をつけましょうというか、適切にインタフェースを定義したモジュールに分割しましょうというお話でした。あとこれ関係で、できるだけexportするシンボルはきちんと列挙しておいた方がいいかもしれないとも思った。
32bit Linuxについては検証するの忘れてたからまた今度。
Haskell忘年会でAWS SDK for Haskellについて喋った
去る12月16日に行われたHaskell忘年会なるイベントでしゃべってきました。
忘年会もなにも勉強会とかにはあんましで出かけないのでほとんどの方ははじめましてという感じ? いや知ってる人も割といましたね。同僚とか同僚とか。
その時の発表スライドです。
要は作りましたというお話です。作りました。
Hackageにも登録してあります。
http://hackage.haskell.org/package/aws-sdk
ソースはGitHubにあります。
worksap-ate/aws-sdk · GitHub
まあそんなような感じです。
聞く側としては話題のFreeモナドの話がちょっとおもしろそうな感じでした。「そのライブラリもFreeで書いたらテスト楽になるんじゃないの?」って言われて、確かに面白そうな気もするけどバリバリConduit依存なのでちょい厳しいのかもしれません。いやFreeは全然使ったことないのでなんとも言えませんけど。
あとはmasterqさんにいっしょにOSカーネル書こうぜーって絡まれてたのが面白かった。
ビデオメモリ直書きで画面表示とか、泣きながらstop or rebootデバッグ(無限ループを仕込んで、何も起きなければそこまでは到達している、再起動がかかったらそれ以前でバグっているという画面が使えない時のprintfデバッグ的な技。タイムスライスで結局死んだりする)をした日々とか、やりたいようなやりたくないような感じだけどやると結構楽しいんですよねーっていう話をしていました。
忘年会なんで、終始割とゆるく楽しむ感じで、面白かったです。
少しはテストを楽しくやる(QuickCheck)
テストって別にやりたくないわけじゃないっていうかやりたいんですけど、なんかつい後回しになってしまうというか。
でも例えば外部システムとの連携部分だったりすると私は割と真面目にテストを書くんですが、普段はなんかなんとなく面倒臭い。面倒くさいと面倒くさくないの境界線は何なんだ。
外部システム連携みたいな入出力系のテストがそれほど苦じゃないのは、どっちにしろ動作確認で動かすからなんですよね。
一方でそれ以外の部分、いわゆる計算というか、モジュール間のデータのやり取りだったりデータ変換だったりとか、そういう部分のテストがなんで面倒くさいかといったら、それは足し算をテストする時の事を考えるとだいたい想像つくと思いますけど、
@Test void testPlus() { assertEqual(plus(1, 1), 2); assertEqual(plus(1, -1), 0); assertEqual(plus(2, 3), 5); assertEqual(plus(-5, 4), -1); ...
そこで浮かんでくる感情というのは「わかりきってるじゃんそんなこと」だったり、「a+b=cのa,b,cをどのくらい用意すればいいのかわからない」だったり、「いや並べておけばいいんだけどそんな単純作業面倒くさい」だったりで、一言で言うと面白くないわけです。面白くない。
これなんとか面白くならないものか。
……という観点で出会ったわけでは全然ないんですけど、ちょっとテストを書く用事ができて、QuickCheckを使ってみたらこれが割と面白かったのです。
QuickCheckというのは話には聞いたことがあったんですけど、要は関数の性質を検査するものらしいと。
性質とはなんぞや。
例えば、リストをソートする関数があった場合、実装がバブルソートだろうがクイックソートだろうが、それ以外の何か高速なアルゴリズムで実装されていようが、同じ入力に対する出力結果は同じです。こういう時に性質が同じと言う。
例えば、リストの順序を逆にする関数があった場合、任意のリストに対してその関数を2回適用すると元に戻るという性質がある。
例えば、ある数値を文字列に変換してそれをさらに数値に変換すると元の数値に戻ってほしい。
そういうものですよね。
そういう時にQuickCheckではこう書くらしい。といいつつこれコンパイルすらもしてないから動くのかはわからないけども。
import Test.Hspec import Test.Hspec.QuickCheck (prop) import Test.QuickCheck spec :: Spec spec = do describe "sort test" $ do prop "int" (test_sort :: [Int] -> Bool) prop "string" (test_sort :: [String] -> Bool) describe "reverse test" $ do prop "int" (test_reverse :: [Int] -> Bool) prop "char" (test_reverse :: [Char] -> Bool) describe "transform test" $ do prop "int to string" test_int_str -- バブルソートとクイックソートは同じ結果になる test_sort :: Ord a => [a] -> Bool test_sort xs = bubble_sort xs == quick_sort xs -- リストの逆順の逆順は元と同じになる test_reverse :: [a] -> Bool test_reverse xs = xs == reverse (reverse xs) -- 数値を文字列に変換して、それを数値に変換すると元に戻る test_int_str :: Int -> Bool test_int_str i = i == parseInt (toString i)
ここのtest_sort, test_reverse, test_int_strで、さっき挙げた例と同じ事をやっています。
この3つの関数は全部何らかの入力を受けて、関数の性質が正しければTrueを返すようにしています。
じゃあこの「何らかの入力」というのは何なのかというと、任意の[Int]だったり[String]だったりIntだったり、それはprop関数が値の型から自動的にランダムな値を選んで適当に突っ込んでくれるらしい。
どういう仕組なのかというと、これ。
Test.QuickCheck.Arbitrary
Test.QuickCheckをimportするとIntとか[Int]とかCharとかそのあたりがあらかたArbitraryのインスタンスになって、arbitrary関数を呼び出すと、その型のランダムな値が取り出せるようになる。このランダムな値を使ってpropが適当に入力を生成してテストしてくれるというわけらしい。
生成する型インスタンスの情報が必要なので、sortとかreverseでは型注釈を書かないといけなかったわけです。
運悪く間違えてテストが通ってしまうとかありそうな気がしないでもないけど、まあ自動テストして何度も回していればいつか気づくでしょうとか、そういう感じでしょうか。そこら辺はよくわかりませんが。
で、自分で作ったデータ型に関してもArbitraryのインスタンスにしてっていうか、あの辺のモジュールにあるGenを生成するchooseとかelementsだとかいう関数を使ってarbitraryを実装してあげれば、任意の値を使った性質のテストが書けるという。
例は私が書いたものじゃないんですけど。山本先生のIPアドレス表現関係のライブラリです。
iproute/test/RouteTableSpec.hs at readaddr · yunomu/iproute · GitHub
自作のIPv4とかIPv6のデータをArbitraryのインスタンスにして、それぞれのCIDRもvalidなものを自動生成するようなコードが上の方に固まっています。
これに対して私がPull Requestを送った時に書いた異常系のテストがこっち。
iproute/test/IPReadSpec.hs at readaddr · yunomu/iproute · GitHub
InvalidIPv4とかそういうデータを定義して、異常な入力値を作っている。ここでは例えばIPv4のCIDRのマスクビット数が0-32以外の値である場合を作っています。文字とか突っ込んでもよかったなぁ。
これらを使ったテストを下の方に書いてます。今回はread関数をテストしたかったんですけど、SafeのreadMayを使ってテストするのもアリかなと思ってこんな感じにしてみましたっていうか、readMayでも問題なく使えるようにするのが目的だったんですが。
こういうのを使って正常な値を自動生成するとか異常な値を自動生成するとかいう作業は割と楽しかったりする。というか楽しかったのです。
どうやってテストできるだろうとか正常値をどう表現しようとか考えたりするのは大変だけど楽しいですよね。コピペは楽しくない。
参考というか、実際は参照してなかったりするけども。
Haskellの単体テスト最前線 - あどけない話
あと全然関係ないけどPull Requestする時に参考になった。
お気軽 pull request - Quickhack Diary
readを使って文字列をRead aに変換しようとする
最初はreadTextみたいな関数を作ろうとしていました。
readの、StringじゃなくてTextを取る版。
readText :: Read a => Text -> a
で、最初はこんな感じで実装しようとしていた。
module Main where import Data.Text (Text) import qualified Data.Text as T readText :: Read a => Text -> a readText = read . T.unpack
unpackしてできたStringをreadに食わせれば終わりじゃないか。
と思っていたんですが、
ghciで試してみると
Main> :set -XOverloadedStrings Main> :m Data.Text Main Data.Text> readText "193" :: Int 193 Main Data.Text> readText "abide" :: Text "*** Exception: Prelude.read: no parse
TextをTextに変換しようとすると失敗する。
「TextってReadのインスタンスだよね? なんで?」って一人で大騒ぎしていました。
というかよくよく考えて試してみると、
Prelude> read "841" :: Int 841 Prelude> read "abc" :: String "*** Exception: Prelude.read: no parse
Stringでもunpackとかしなくても普通に失敗する。Intはうまくいくのに。
ヒントはもう出ていて、`"*** Exception:`のところの最初のダブルクォーテーション、文字列をshowした時に出るやつで、文字列はshowしたりする時はダブルクォーテーションで囲まれる。
同様に、readに渡す文字列は文字列の文字列表現の形をとっている必要があって、
つまり
Prelude> read "\"abc\"" :: String "abc"
文字列は文字列らしくダブルクォーテーションで囲めばよろしい。
文字列だと思ってparseしようとしたのにダブルクォーテーションが無かったから困ったねって事になっていたようです。
ということで、最初のreadTextの実装を変更してみる。unpackじゃなくてshowしてやればいいじゃないか。
readText :: Read a => Text -> a readText = read . show
showしてreadってなんかなんじゃそりゃって感じですけどね。
でもこれはこれで問題があって、
Main> readText "186" :: Int *** Exception: Prelude.read: no parse
今度は数字が読めなくなっている。全然駄目だ。
showしたことで文字列がダブルクォーテーションで囲まれてしまって、数字としてreadできなくなったということで。
そういう時だけは(read . unpack)でいいんだけど。
で、最終的にSafeモジュールのreadMayを使ってこうなった。
import Data.Text (Text) import qualified Data.Text as T import Safe (readMay) import Data.Maybe (fromMaybe) readText :: Read a => Text -> a readText t = fromMaybe (read $ T.unpack t) . readMay . show $ t
文字列系に変換するパターンの方が圧倒的に多かったのでこういう感じに。文字列以外に変換するパターンが多かったら、showとunpackの位置が逆になるんじゃないでしょうか。
ということで出来上がったのはreadではなくて、なんか、汎用のデータ変換関数? 結局readとは少し違うものができてしまいましたし、最初から目指していたのはこれでした。
この変換関数を作成中のプロダクトの基盤部分に突っ込んでみた結果、それはそれで「型推論できないぞ」って怒られまくったんですが、それはまた別の話。
readを使いこなすのって難しいですよねぇ。というかあんまし使いたくないけども。
Haskellでコマンドラインパーサを使う2
前回とは違うライブラリを使ってみます。
前回: Haskellでコマンドラインパーサを使う - yunomuのブログ
前回の記事ではcmdargsを使ったんですが、あれはあれで面倒くさいというか、あらかじめ定義したrecord型の通りにしかデータを取ることができないのがたまに面倒だったりとか。
第二引数があった時は第三引数が必須とか、逆に要らないとか、そういうのが面倒くさかったりします。
具体的にはgitみたいなのを作るのが面倒くさい。
ということで他になんかないものかと思って使ってみたのがparseargsというライブラリです。
ArgとかArgsとか似たような型が色々あってちょとややこしいですけど、要するにparseArgsIOに[Arg a]の配列を渡してあげればとりあえず使えそう。で、Arg aというのがコマンドライン引数の型とかオプション文字列とかdescriptionとかを持ったいわゆる「設定」で、"a"はハッシュのキーみたいなもののようだ。
なのでさしあたりaはIntということにするとこうなる。
module Main where import System.Console.ParseArgs options :: [Arg Int] options = [Arg 1 Nothing Nothing (argDataRequired "abc" ArgtypeInt) "abc value"] main :: IO () main = do a <- parseArgsIO ArgsComplete options putStrLn $ "progName: " ++ argsProgName a print (getArg a 1 :: Maybe Int)
これを実行するとこうなる。
% ./args args: missing required argument <abc> usage: args <abc> <abc> abc value
DataArg(Argの4つ目の引数)を"argDataRequired"で作って渡したので、この引数はRequiredになっている。引数の名前が<abc>になってるのも、argDataRequiredに渡した文字列から取られているみたいですっていうか、マニュアルにそう書いてます。
で、なんか適当な引数を渡してみる。
% ./args text progName: args args: argument text to abc is not an int usage: args <abc> <abc> abc value
これは"text"って文字列を渡しているんですが、やっぱりargDataRequiredに渡してるArgtypeがArgtypeIntになっているので、Intとしてパースできる値を渡してあげないとエラーになる。
% ./args 1234 progName: args Just 1234
Intとしてパースできる文字列を入れてあげるとこのようにうまくいきます。
ここでちょっと面白いのは、"text"って文字列を渡した時の動きで。これよく見ると、putStrLnで出力した文字列は先に出てるんですよね。
つまりこの「"text"はintじゃないからだめだよ」っていう感じのエラーメッセージは、parseArgsIOを実行した時ではなくて、そこで取ってきた値を使おうとした時、つまりgetArgした時に出力されているということなんです。まあこれは単にparseArgsIOの中でerror関数とか使って適当に戻り値を設定しているせいなんだと思いますけど。いや調べてませんけど。
まあこうしておいた方が、「この値が不正な時にどうしよう〜」っていう微妙な需要に対応できる可能性があって良いのかもしれません。どうやってやるのかわかりませんけど。
それはそうと、ArgtypeIntについて、パースできる型はInt, Integer, Float, Double, Stringとあるので、まあ十分でしょう。
Argの一つ目の引数については、最初あたりで書いたようにハッシュのキーみたいになっていて
print (getArg a 1 :: Maybe Int)
のところの"1"で参照しています。もうちょっとわかりやすい値にした方がいいのかもしれません。
Argの2つ目と3つ目の引数はそれぞれショートオプションとロングオプションの指定になっていて、
Arg 2 (Just 'f') (Just "flag") Nothing "flag"
みたいにすると、"-f"とか"--flag"を読み取ることができる。
この例の場合、4つ目の引数の、さっきargDataRequiredとかになってた部分がNothingになっていますけど、ここで書いたようにNothingにすると、gccでいうところの"-c"みたいに単体で何らかの意味のあるオプションになります。Just DataArgにすると"-o name"みたいなパラメータがついてるオプションになる。
そしてオプションが存在するかどうかはgotArgで調べられるので、
main = do (略) print $ gotArg a 2
とすると、"-f"がついていればTrueだし、ついてなければFalseになります。普通にgotArgは引数の存在確認として使えます。
argDataRequiredの代わりにargDataOptionalを使えば必須じゃない引数になりますし、argDataDefaultedを使えば引数が無かった時のデフォルト値を設定することもできる。
argsUsageでヘルプの表示もできる。
あとparseArgsIOに渡してるArgsCompleteは、だいたい何も考えない時はArgsCompleteでいいです。
というのをひと通りやってみたのがGitHubに上げてるこちらになります。
exercises/parseargs/Main.hs at master · yunomu/exercises · GitHub
前回紹介したcmdargsとの違いは、細かい分岐がやりやすいところかなぁ。いやrecordを読むかgetArgを読むか程度の違いしか無い気もしますけど。Requiredな引数を作るのがparseargsの方が簡単なような、うーん。
ヘルプを充実させたいならやっぱりcmdargsの方が強力だった気もしますが、私はどっちかというと今回のparseargsの方が好きかもなぁ。
という感じでした。