Haskellで作るCLI

matsubara0507氏:「HaskellでCLI」というタイトルでお話しします。とくに自己紹介スライドとかはないんですけど、一応僕は職業Haskellerではなく、趣味でHaskellをたしなむ程度の人間なので、あまり難しいことは聞かないでください。

(会場笑)

まだ僕は新卒でして。ここ半年ぐらいはHaskellとぜんぜん違う言語を使っていて、本当に趣味でちょこっとずつしかやってないから、最近のことはよくわからないので、とくに最近のことは聞かないでください(笑)。

本題。今回はみなさん、「これからHaskell触ってみたいな」という人が多いと思っているんですが、一方でHaskell自体が初めてのプログラミング言語だという人はほぼいないと思うんですけど、仮にHaskellを新しくプログラミング言語として学んだ場合、みなさんは何をしますか?

たぶんみなさん、コマンドラインを作ると思うんですよ。いきなりGUIを試すという猛者がいるかもしれないんですけど、ひととおり構文を学んだら、普通は簡単なターミナルのコマンドを叩いて、引数もらって、「あ、なんかいいぞ」みたいなことをやると思うんですよね。

今回はそれについてのお話をしたいと思います。とはいえ、あんまり入門書には書いてない、少しマニアックな話が多いんですけど、したいと思います。

1つ目が、「コマンドの話する」って言ってるんだから、「コマンドライン引数をHaskellでどうやって扱うか?」っていう話をします。

Haskellでコマンドライン引数を扱うのは、この3つ以外にも実はたくさんあるんですけど、僕がだいたい使うのはこの3つなので、この3つを紹介します。

1つ目が“getArgs”っていう標準のライブラリにある関数です。

Haskell自体をあんまり書いたことがない人が多いかもしれないんですけど。Haskellは“main”っていう関数からコマンドとか実行ファイルを作ると実行されるんですけど、そのなかで“getArgs”っていう関数を呼びます。

そうすると、左側にある“args”っていうところに、できたコマンドの名前の後に続くようなコマンドライン引数のところが、全部入ってきます。String型っていう文字列型の……、この“[]”は線形リストです。厳密にはぜんぜん違うんですけど、他の言語で言う「配列」とか、そういう認識だと思ってください。それが入ってきます。

“head args”ってしてるのは、1つ目を取ってきて、「Hi, 〇〇」みたいに出力するっていうのをやってます。たぶん入門書とかだと、まず“getLine”をして、標準入力を受け取るみたいにやると思うんですけど、それと同じような感覚で扱えます。

ただ、これは単純にコマンドライン以降の文字列を空白区切りで引っ張ってくるだけなんですよ。実際、リッチなコマンドを作りたいと思ったときは、「もう少しオプションのところをいい感じに扱いたいな」っていう野心が芽生えてくると思うんですけど、そうなってくると、さっきのではきついと。

そのためのものとして、標準で用意されているものに“GetOpt”というものがあります。

これは“getArgs”で得た文字列を与えて、いい感じに型にしてくれます。Haskell、なんとなく今日1日の話を聞いていて、みんな、「いい感じに型にして扱いたいんだな」っていうのがわかってると思うんですけど、そんな感じのことをしてくれます。

この1引数目の“ArgOrder”っていうのは、オプションでもなんでもないものをどうやって扱うかっていうだけで。2引数目の“OptDescr”っていうやつが、いい感じの型にしてくれるやつですね。この“a”って書いてあるところが、そのいい感じにしてほしい型を明示的に与えます。3引数目が、さっきの“getArgs”のやつですね。

返り値としては、3つ組のタプルで返ってくるんですけど、実際のパースできた型と、オプションの型と、それ以外とエラーで返ってきます。細かい説明は割愛します。

実際の使用例としては、この“data Flag”ってやつがいい感じにしてほしい型ですね。

よくある“Verbose”とか“Version”とかっていうのを定義してます。一番下の“options”っていうのが、そのパースの仕方を簡単に定義しています。なんとなく“v”と“verbose”で“Verbose”になって、“V”と“version”で“Version”になるみたいな感じです。それで、さっき“getArgs”で取ってきたものを、さっきの“GetOpt”関数に与えている、というような感じです。

optparse-applicative

こっからだんだん難しくなってくるんですけど、もっとリッチなことをたくさんしたいと。例えば、サブコマンドを使ったり、もっとリッチなことをしたいっていうために、optparse-applicativeというのがあります。

これは午前中でも使われた、stackって呼ばれるビルドツールでも使われているものです。なので、困ったらそれを見にいくといいと思います。それらはリッチなんですけど使い方が難しいので、派生したものがたくさんあります。

どうやって使うかっていうと、細かいところはいいんですけど、さっき同様、“data Sample”っていう引数っぽい型を定義して……。それはそれぞれ、“hello”だったら“--hello”みたいな感じですね。 “verbose”は“--verbose”と、小文字の“-v”ですみたいな、そういう感じです。

ちょっとおまけの話なんですけど、optparse-applicativeを使うとサブコマンドを扱うことができます。サブコマンドっていうのは、例えば“stack new”とかのときの“new”の部分ですね。

例として、この型は、なんとなくToDoタスクを扱えそうな適当な型なんですけど、“NewCmd”でタスクのカードを作って、“AddCmd”でタスクの本文を足して、みたいな、そういうすごい雑なやつです。

そのサブコマンドのパーサーをoptparse-applicativeで書くとこんなふうになって。

これが“hoge new”とか“hoge add”とかで、それぞれ実行されるんですけど、これ何が問題かっていうと、さっきaiyaくんが一生懸命説明してくれた“Semigroup”の“<>”の結合を使っているんですけど、これって、さっきのこの4つについて、「ちゃんと全部定義してあるか?」という網羅性をチェックすることはできないんですよ。

だって、3があったときに、1+2だったのか、0+3だったのかって、3からはわからないじゃないですか。それと一緒で、“Semigroup”からは、もともとどういったものが結合されていたかっていうのはチェックできない。Haskellerの基本的な精神として、型でなんでもやりたいという貪欲な精神があるので、その網羅性をなんとかして型でチェックしたいじゃないですか。たぶんそのうち、みんなもそうなると思うんですけど。

(会場笑)

そのために、extensibleっていう、とてもイカすパッケージを使います。

extensibleっていうのは、とくに今回細かい話はしないんですけど、例えば入門書とかを読んで、「Haskellのレコードってイマイチじゃん。クソじゃん」みたいに思ってきたら、「Haskell、extensible」で検索してくると、それらが解決できるようになると思います。

作者が日本人で今日も来てらっしゃるので、最悪「ぜんぜんわからんぞ、このエラーメッセージ」ってなったら、それをHaskell-jpに貼るといいと思います。

さらに、extensibleはなぜかwikiがあるんですよ。攻略wikiっていうね。

(会場笑)

あまり検索しても出てこないんですけど、困ったらこれを見にいくといいと思います。

それとさっきとまったく一緒ですね。一緒ですよね? わかりますよね? なんとなく気持ちはわかりますよね? これがこうなると。

これは、サブコマンドで書くとこんなふうになります。

そしてこれは、ちゃんと“new”と“add”と“done”と“list”が定義されてるかをチェックしてくれます。これ、“list”がなかったら、エラーメッセージとしてコンパイルが通らなくなります。詳しくは記事があるので、見てください。

これは、「この“variantFrom”って何?」っていうところを書いてあるだけで、興味ある人は読んでください。ただ、これも随分前に書いたやつなんで、「もっといい書き方があるんだよ」みたいなまさかりは、Twitterでボソッと言ってください。

Alt.Prelude

2つ目の話になります。このあたりからは「もうぜんぜんCLI関係ないじゃん」っていう感じかもしれないんですけど、わかってる人には。「Alt.Prelude」という話をします。

これは何かっていうと、まずそもそもPreludeって何かっていうと、Haskellの標準ライブラリとしてbaseと呼ばれるものがあります。これはHaskell……、というかGHCですね。ごめんなさい。僕さっきから「Haskell」とか言ってても、GHCのこと指してます(笑)。

baseという標準ライブラリ、標準パッケージというものがあって。Preludeっていうモジュールがあるんですけど、それは勝手にインポートされるような関数群です。例えばさっきの (++) であるとか、IO型とか、Stringとか、型とかも全部そこに入ってるんですけど、そういったものです。

逆に、GHC拡張として、NoImplicitPreludeっていうプラグマっていうものを使えば、「勝手にインポートするなよ」みたいなこともできます。Haskellあるあるとして、だいたい「Preludeって微妙じゃん」ってなってくるタイミングがときどきあるんですよ。

(会場笑)

型でなんでもしたいっていうのに、部分関数である“head”が標準であるとか、僕もさっき使ってたから人のこと言えないんですけど、そういう問題があります。

そのために標準ライブラリのオルタナティブとして、Alt.Prelude系のライブラリがいくつか存在します。1回、HaskellのSlackでも話題になったはずなんですが。

これ、読んでもらえればわかるとおりなんですけど、「結局何がいいかなんてHaskellerでもわかってない」という結論なんですけど、これはなんやかんや話して結論ついてないです(笑)。

RIOライブラリ

今回紹介するのは、このなかでもRIOと呼ばれるやつで、僕が愛用してるからというだけなんですけど、「本当にそれが一番いいか?」って言われると、たぶんそんなこともないので、みなさんいろいろ使って、自分で良し悪しを判断してください。すいません(笑)。

これは何かっていうと、stack……。ちょくちょく出てきている、午前も使ったビルドツールですね。stackを開発してるチームが開発している、Alt.Preludeです。実際、最近このRIOに変わったのかな、たぶん変わってるはず。彼らのベストプラクティスを詰め込んでいます。

例えば大域環境というか、大域な変数を扱いたいときとか、あと、Haskellってくると、だいたい「ログできないんでしょ?」って言われるロガーとか、あとエラーハンドリングのあたりとかというベストプラクティスをまとめたものです。

さらによく使うライブラリが、もうすでにラッパーとして組み込まれています。このあたりは「どうせみんなよく使う」というか、Haskellのビルド、cabalとかstackを使うとこれらが全部インポートされるみたいなやつが、もうすでに入っています。

例えばRIOの使用例、これは実際に最近作ったやつのコマンドの一部なんですけど。

これは何かっていうと、MediumっていうQiitaの海外版みたいなやつがあるじゃないですか。あれって残念なことにマークダウンが使えなくて、マークダウンに汚染された人間にとっては、ちょっとそれ、扱いにくいんですよね。

なんですけど、なぜかMediumのAPIからはマークダウンを送信できるという謎機能があって。

(会場笑)

12月から始まるみんな大好きアドベントカレンダーに向けて、どうしてもMediumに投稿しなきゃいけないんだけど、「マークダウンを使いたい」という要望のためにHaskellで書いたツールの一部です。

なんで、“MEDIUM_TOKEN”とかって書いてあるんですけど、この上の“run”っていうところがRIOのボイラーテンプレートみたいなもので、こんなふうに書くと、この下の“RIO Env ()”みたいなところでは、自由にロガーとか大域変数とかにアクセスができるという、謎な状態になってます、細かくはとくに話さないんですけど。だいたいこの“Env”にいろんなものが詰め込まれていて。

例えば“logDebug “Run cmd : call me api””とかって書いてあるみたいに、これをしとくと、ロガーがデバッグ時に表示してくれる、みたいな。で、“API get me token”っていうのは、普通のIOですね。これでMediumのMe APIを叩くぐらいのイメージなんですけど、これは単純にユーザーの情報を取ってくるだけのAPIですね。“logDebug”とかをしてくれる。“logInfo”で“Hi hoge !!”を出力してくれるみたいな感じです。

実際の出力例なんですけど、試しに……。

ここで“me”とかって打つと、さっきのやつが出てきますね。

それで、“verbose option”……、“v”でいけるのか、こんなふうにデバッグが出てきて。

これはRIOのデバッグの仕様そのままなので、わざわざ僕が何かしたわけでもないです。そんな感じです。

Stack Template

最後のテーマなんですけど、「Stack Template」っていう話をします。

「Stackとは何ぞや?」っていう話は、たぶんもういいと思うんですけど、Haskellのビルドツールで、ここ5、6年前ぐらいに出たやつですかね。それまでは、検索すればたぶんいっぱい出てくる「Cabal Hell」という地獄に悩まされていたと思うんですが、これのおかげでだいぶ初心者の導入のしやすさが上がったかと思います。

処理系自体は、Haskellとか言ってるけどGHCです。プロジェクト内の各パッケージのバージョン管理とかをしてくれます。他にもDockerをいい感じで扱ってくれたりとか、いろんな機能があるんですけど、RubyのBundleとか、Scalaの sbtと同じだと思ってくれて差し支えないと思います。

だいたいこの“stack new sample”で、一番右側“hoge-template”っていうのは、“new”したときに生成されるファイルのテンプレートを与えています。“sample”なのに、“sample-project”に“cd”してる。まあ、いいや。ミスです。それで、……“stack build”とかしてあげるとビルドが走って、“stack exec – application”とかってすると、できあがった実行ファイルを実行できる、みたいな感じです。

何回も何回も“stack new”するとわかってくると思うんですけど、“stack new”の後にやることってだいたい一緒で、推しパッケージをとりあえずDependencyに書いて……(笑)。

(会場笑)

それで自分のところにそのGitHubのアカウントを足して……、みたいなことをやるんですよね。そういった何回も同じことをやるのを解決するために、オレオレテンプレートを作ることができます。これは最初からできてたんですけど。

例えばこういうふうなやつですね。

Haskellって固有拡張子がだいぶ好きなんですけど、hsfilesっていう、テンプレートでこういうふうに書くと、どう見てもHaskell専用の拡張子で。

例えば“package.yaml”っていうのは、Stackでのパッケージの指定の仕方なんですけど。オレオレ。大好きなDependencyというか、依存パッケージをこういうふうにあらかじめ書いておくとか、どうせインポートするんだから、最初のmainのところにもう書いておくとか、みたいなことができるようになります。

いままではそれをローカルに置いて、それを参照するぐらいしかできなかったんですけど、最近のアップデートで、GitHubとか、GitLabとか、BitBucketとかに置いたものが取ってこれるようになりました。こんな感じですね“stack new github :自分のGitHub名”に、そこに置いてあるhsfilesを指定してあげます。

作り方の詳しいところはこの記事を参照してもらえばいいんですけど。これは固定なんですけど、自分のところにstack tplsって呼ばれる、このリポジトリ名で1個処理を作って、なんちゃらhsfilesをトップのところに置いてあげると、さっきのやり方でアクセスできるようになります。

2tack-tpls

最後におまけなんですけど、それらのテンプレートって、みなさんが使ってくれればすごい大量にできるんですけど、一括で参照する方法がないので、それをしてくれるようなCLIを、いままで紹介したような手法で作りました。

これは……、こんなふうにやると……。取ってきてくれるっていう。

半月ぐらい前に実行した結果と、出てる量が変わんない気がするんですけど(笑)、こんなふうに取ってきてくれます。

例えばここに書いてあるやつをやると……。

中身を取ってきてくれるので、試しに見たりとかもできます。

ちなみに、このstack tplsの裏側ではGitHubのGraphQLを使ってるんですけど、GraphQL……。すごくいいなと思ったんですけど、Haskellのいい感じのクライアントがないので、誰か作ってほしいなというのがあります(笑)。

(会場笑)

1週間ぐらい悩んだんですけど、型がいい感じになるようなのができなかったので、ちょっと誰か型のプロの人、がんばってください(笑)。

まとめ

まとめです。

コマンド引数の取得方法、今回紹介した以上にもっといいのがあるっていうの、たぶんTwitterに出てると思うんですけど、たくさんあります。単純なものからリッチなものまであるので、自由に使ってください。

あと、標準ライブラリ使ってて、「Prelude微妙じゃん」って思ったら、Alt.Preludeもたくさんあるので、みなさん探してみてください。あと何回も何回もHaskell使うようになってくると、オレオレプラクティスができるので、そしたらテンプレート化して、stack tplsとかを使ってみてください。という話でした。以上です。

(会場拍手)

司会者:すいません。また時間をミスってしまって、すでに若干切れてる状態なんですが、質問のある方、誰かいらっしゃいましたらください。

……いいかな? じゃあ、時間も押しているので、次に切り替えたいと思います。もう一度拍手をお願いします。ありがとうございました。

(会場拍手)