Haskell で +channel 使って Vim とおはなしする

今年に入った辺りから Vim に極めて活発にパッチが入り始め，1 月に [`smile`](https://github.com/vim/vim/commit/86e179dbe75010e9545e1a2fcc92a15d57bf27fd)が実装された時にはまだ7.4.1005だったのが本記事を書いている時点での最新のパッチはすでに7.4.1980となっています．

この間には実に様々な機能が入り，すこし数えるだけでも[一応のパッケージ機構](https://github.com/vim/vim/commit/91715873d19a1859c08eeded7848113596e2f2bd)，以前から需要のあった [true color support のあるターミナルへの対応](https://github.com/vim/vim/commit/8a633e3427b47286869aa4b96f2bfc1fe65b25cd)，[函数の部分適用めいたものへの対応](https://github.com/vim/vim/commit/1735bc988c546cc962c5f94792815b4d7cb79710)[^partial]，などが実装されました．なかでも [1月](http://vim-jp.org/vimmagazine/2016/01/31/vimmagazine.html)  から [2月](http://vim-jp.org/vimmagazine/2016/02/29/vimmagazine.html) にかけて [timer](https://github.com/vim/vim/commit/975b5271eed4fa0500c24a8f37be0b1797cb9db7), job や channel の機構が導入されたのは多くの人に大きな喜びと期待をもって迎えられました．

* [Big Sky :: Vim にchannel(ソケット通信機能)が付いた。](http://mattn.kaoriya.net/software/vim/20160129114716.htm)

<del> [`:h channel`](https://github.com/vim/vim/blob/361c8f0e517e41f1f1d34dae328044406fde80ac/runtime/doc/channel.txt) にはこうあり，</del> (16 Jul 2016 追記: 本日 DRAFT 表記が[とれました！](https://github.com/vim/vim/commit/269f595f9eef584937e7eae70fde68cdd7da5bcf#diff-46964248691e139346525e6db91829b7L1))

```
DRAFT  DRAFT  DRAFT  DRAFT  DRAFT  DRAFT  DRAFT  DRAFT  DRAFT  DRAFT
```

実装直後は[関数名に変更が入ったり](https://github.com/vim/vim/commit/7823a3bd2eed6ff9e544d201de96710bd5344aaf)と，<del>まだ安定して使うにはどうかという感じがありますが，とはいえもう入ってから4ヶ月ほど経過しています．</del>ちょっとくらい触ってみましょう．この記事は Vimmer と Haskeller とその他の方々からの指摘を期待して書かれている部分もあります．

題材はとりあえず i)非同期の旨味が少しはあって ii) (僕の Vim script 力がないから) `:echom` だけで意味があるようなもの…というわけで安直に降雨の状況を取得することにします．以前 twitter の bot[^bot] を書いた時にも使った，<span style="margin:15px 15px 15px 15px"><a href="http://developer.yahoo.co.jp/about">Web Services by Yahoo! JAPAN</a></span> の中の[気象情報API](http://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/weather.html)と[Yahoo! ジオコーダAPI](http://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/geocoder.html)を組み合わせてみます．

（簡単な，そしていくつか絶対改善できるやろって点のある）成果物は[こちら](https://github.com/lesguillemets/rainfall-vim-hs)．`stack install` して `rtp+=rainfall-vim-hs/vim` で使えはします．`:RainfallStart` して `:Rainfall 左京区` とかしてください． 

# 簡単な機構

## Vim からの話し方

とりあえず `channel-demo` に書いてあるのをおおよそなぞってみることにします．

```vim:vim
let channel = ch_open('localhost:8765')
```

これで channel を開く（見たらわかる）．やりとりのモードは以下の4つがあって，

> <pre>
>   Over the socket and pipes these protocols are available:
>
>   RAW    nothing known, Vim cannot tell where a message ends
>   NL     every message ends in a NL (newline) character
>   JSON   JSON encoding |json_encode()|
>   JS     JavaScript style JSON-like encoding |js_encode()|
> </pre>

`ch_open` では何もしないと `JSON` モードになるので以降これの話をします．

Vim からデータを送る函数はいくつかありますが，[`:h channel-use`](https://github.com/vim/vim/blob/v7.4.1980/runtime/doc/channel.txt#L217) をみるとこんな感じです:

* データを synchronously 送って結果を得る[^chevalexpr]:
  ```vim
  let response = ch_evalexpr(channel, {expr})
  ```
* 結果は無視して非同期に送る[^chsendexpr]:
  ```vim
  call ch_sendexpr(channel, {expr})
  ```
* 結果を handler に渡す:
  ```vim
  call ch_sendexpr(channel, {expr}, {'callback': Handler})
  ```
* 他に自分で全部面倒を見る [`ch_sendraw`](https://github.com/vim/vim/blob/v7.4.1980/runtime/doc/eval.txt#L2972) などもあります．

Vim はこれらをIDとして振った番号と一緒に送ります．すなわち，もし `"hello"` という string を送ろうとした場合，実際には(たとえば) `[3, "hello"]` というリストが送られることになります．


## Vim への話し方

Vim は先ほど振られたIDでどれへのお返事かを判別します．従って `[3, "panzer"]` に対して返答するなら `[3, "vor"]` といった形式でよろしい．こちらから話しかけたいときは最初を`0`にして `[0, "pon!"]`と言うふうにします．

さらに，handler をすっ飛ばして直接コマンドを投げることもできます．[`:h channel-commands`](https://github.com/vim/vim/blob/v7.4.1980/runtime/doc/channel.txt#L270) に一覧されていますが，例えば `["ex","echo 'ankou'"]` というような形式になります．

なお，

```vim
call ch_logfile('channellog', 'w')
```
というふうにすればログを書き残せるので便利．

# Haskell 側を書く

## シンプルなサーバ

Haskell でシンプルなサーバを書くなら[network](http://hackage.haskell.org/package/network-2.6.2.1)の `Network.Socket` を使うのも手ですが，どうやら [conduit-extra](https://hackage.haskell.org/package/network-conduit) (旧称 network-conduit) の [`Data.Conduit.Network`](https://hackage.haskell.org/package/conduit-extra-1.1.13.1/docs/Data-Conduit-Network.html)を使うと楽そうです[^tnkh]．基本的な演算子 (`=$=` とか) はふつうの conduit 由来なので[これ](https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/conduit-overview)とか読んで勉強しましょう[^condover]．

基本的にはこういう感じ

```haskell
import Data.Conduit
import Data.Conduit.Network
import Data.Conduit.Text (encode, decode, utf8)
defaultPort = 4567
main :: IO ()
main = do
    runTCPServer (serverSettings defaultPort "*") $ \ appData ->
        appSource appData $$ decode utf8
                          =$= conduit
                          =$= encode utf8
                          =$= appSink appData

conduit :: ConduitM Text Text IO ()
conduit = undefined
```

たとえば conduit がこういうの

```haskell
conduit = do
          str <- await
          case str of
              Nothing -> return ()
              (Just s) -> do
                  yield . id $ s
                  conduit
```

だとこれは echo サーバになり，`[1,"Duce"]` には `[1,"Duce"]` を返すので， `ch_evalexpr` 等からみても echo になります[^echo]．
この段階でもう基本的には vim とお喋りする道具は揃った！！ので(たとえば `Text -> Text` とか `Text -> m Text` を書けばよい) 是非遊んでみましょう．

## 中身を育てる
ここからはあんまり関係ないのでサクッと書いてしまいます．結論だけいうと，

* 簡単にリクエスト[作って](https://github.com/lesguillemets/rainfall-vim-hs/blob/8e51c3a966f96452e6e28b3da686cbbe135b2a79/src/YOLP/Weather/Query.hs#L45)[投げる](https://github.com/lesguillemets/rainfall-vim-hs/blob/8e51c3a966f96452e6e28b3da686cbbe135b2a79/src/YOLP/Weather.hs#L13)には [http-conduit](https://hackage.haskell.org/package/http-conduit) が楽[^httpconduit]．
* json のパーズには [aeson](https://hackage.haskell.org/package/aeson) を[^fieldLabelModifier]
* データ型を定義するまでもない[とき](https://github.com/lesguillemets/rainfall-vim-hs/blob/8e51c3a966f96452e6e28b3da686cbbe135b2a79/src/YOLP/Geocoder/Result.hs#L30)は [lens-aeson](https://hackage.haskell.org/package/lens-aeson) がべんり [^lensaeson]
* 話し終わったらプロセス全体が終了して欲しい時は `MVar` とかで[管理](https://github.com/lesguillemets/rainfall-vim-hs/blob/8e51c3a966f96452e6e28b3da686cbbe135b2a79/app/Main.hs#L20)するといいっぽい．

```haskell:Main.hs
main :: IO ()
main = do
    exitWatcher <- newEmptyMVar
    port <- defaultPort
    putStrLn $ "listening on " ++ show port
    forkTCPServer (serverSettings port "*") (run exitWatcher)
    takeMVar exitWatcher *> putStrLn "exit."

run ew appData =
    appSource appData $$ decode utf8
                      =$= conduit ew
                      =$= encode utf8
                      =$= appSink appData

conduit :: MVar () -> ConduitM Text Text IO ()
…
-- 終わりたいところで `putMVar` するほかは上の conduit と同じ
```

全体としては場所を `Text` で受け取る→ジオコーダで経緯度に変換→天気情報APIでその前後の降雨量/予測を取る→前2時間の累計と，後1時間の降り始めとピークの降水量をお伝えという構成になった．


# もう一度 Vim

今度は Vim 側を書く…といっても本当に Vim script 経験が足りず，[Vimプラグインが出来るまで - ぼっち勉強会](http://kannokanno.hatenablog.com/entry/20120403/1333462565)とか `:help` を見ながらというかんじ．スコープとかも不安があるので精進します．

最初にサーバを立ち上げるのをどうしたらいいんだろうってなって，とりあえず `job_start` を使っている．起動後ちゃんとお話ができるようになるまで待ってからチャンネルを開かないといけなくて，ここをとりあえず `ch_read` で読むことでやってるのだけど，絶対もっとまっとうなやり方あるよなあ…[`:h job-options`](https://github.com/vim/vim/blob/v7.4.1980/runtime/doc/channel.txt#L541)とか読みながら10分ほど試してうまく行かず一旦休憩中です．

```vim:autoload/rainfallVimHs.vim
function! rainfallVimHs#start()
    let l:port = get(g:, 'rainfallVimHs_port', 5678)
    let l:job = job_start(['rainfall-vim-hs-exe', '--port=' . l:port])
    " FIXME : how to wait
    call ch_read(l:job)
    let s:channel = ch_open('localhost:' . l:port)
    if (ch_status(s:channel) == "open")
        echom "Started. Powered by : Web Services by Yahoo! JAPAN http://developer.yahoo.co.jp/about"
    endif
endfunction
```

get のところだけかっこいいのは [mattnさんの記事](http://mattn.kaoriya.net/software/vim/20151127181201.htm)のおかげ．

実際にお喋りするところはシンプルでいい．

```vim:autoload/rainfallVimHs.vim
function! rainfallVimHs#handle(channel,msg)
    echom a:msg
endfunction

function! rainfallVimHs#send(loc)
    call ch_sendexpr(s:channel, a:loc, {'callback' : "rainfallVimHs#handle" })
endfunction
```

`sendexpr` で帰ってきた値を `echom` してるだけ．今回は `echo` すべき文字列は haskell 側で作ったのですが，ここで辞書をわたして vim 側でメッセージを作ることももちろん可能で，その場合は `handle` が長くなります．

# まとめ
雰囲気さえつかめればお喋りじたいはそんなに難しくないです．Haskell からお喋りできるのはやはりハッピー．みんながお好きないろんな言語で試せればいいとおもいます．バッファの書き換え[^tr]とか，`out-io` とかと組み合わせると夢が広がりますね．あるいは薄い wrapper を書いておくと幸せ人口がより増えるかも知れません．

とりあえず今は動く実例を増やすだけでもなんかの意義があるかと思って書きましたが，もっときちんと書かれたコードや解説記事の露出が増えるのを願っています．また今回のコードについては，Haskeller と Vimmer 双方からの厳しいご指摘を希望しつつ待機しています．

[^partial]: このあとしばらく[いろいろ大変](http://vim-jp.org/blog/2016/03/23/take-care-of-patch-1577.html)でしたが…
[^bot]: というより自分のアカウントに載せるサイボーグ機能でした
[^chevalexpr]: [`:h ch_evalexpr`](https://github.com/vim/vim/blob/v7.4.1980/runtime/doc/eval.txt#L2850)
[^chsendexpr]: [`:h ch_sendexpr`](https://github.com/vim/vim/blob/v7.4.1980/runtime/doc/eval.txt#L2963)
[^tnkh]: 古くていくつも関数が変わっていますが翻訳記事の [ConduitとHaskellでネットワークプロキシサーバを作る - 純粋関数空間](http://tanakh.jp/posts/2012-07-01-conduit-0.5.html) とかは雰囲気をつかむのに良い．あるいは [Building an Async Chat Server with Conduit - School of Haskell ](https://www.schoolofhaskell.com/user/joehillen/building-an-async-chat-server-with-conduit) もいい tutorial です．
[^echo]: つまり， ビルドして `./Main` で実行し， vim を立ち上げて `let channel=ch_open('localhost:4567')` し， `echom ch_evalexpr(channel, "Duce!")` すると `"Duce!"` が返ってくるということ．
[^condover]: 読んでないですが[日本語訳](http://qiita.com/alpha22jp/items/0d634b0bb9f4ccb0cd05)も投稿されています．
[^httpconduit]: [Haskellから簡単にWeb APIを叩く方法](http://qiita.com/lotz/items/f8440fa08a62d1c44e1a) などもよくまとまっています
[^fieldLabelModifier]: `fieldLabelModifier` が[便利](http://qiita.com/tattsun58/items/b74dcdf13910fa254f7d)
[^lensaeson]: [HaskellでのJSONパースがこんなに簡単だったとは](http://qiita.com/lotz/items/6305f8460db3419c22e3)．これも基本はlensなので，lensの勉強をすればよいです．lens自身については [A Little Lens Starter Tutorial - School of Haskell](https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial)がよい．
[^tr]: そういえば，[vim-transform](http://qiita.com/t9md/items/4a7c6886b3660829f5d6)とかを参考にできそう？