今年に入った辺りから Vim に極めて活発にパッチが入り始め,1 月に smile
が実装された時にはまだ7.4.1005だったのが本記事を書いている時点での最新のパッチはすでに7.4.1980となっています.
この間には実に様々な機能が入り,すこし数えるだけでも一応のパッケージ機構,以前から需要のあった true color support のあるターミナルへの対応,函数の部分適用めいたものへの対応1,などが実装されました.なかでも 1月 から 2月 にかけて timer, job や channel の機構が導入されたのは多くの人に大きな喜びと期待をもって迎えられました.
(16 Jul 2016 追記: 本日 DRAFT 表記がとれました!):h channel
にはこうあり,
DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT DRAFT
実装直後は関数名に変更が入ったりと,まだ安定して使うにはどうかという感じがありますが,とはいえもう入ってから4ヶ月ほど経過しています.ちょっとくらい触ってみましょう.この記事は Vimmer と Haskeller とその他の方々からの指摘を期待して書かれている部分もあります.
題材はとりあえず i)非同期の旨味が少しはあって ii) (僕の Vim script 力がないから) :echom
だけで意味があるようなもの…というわけで安直に降雨の状況を取得することにします.以前 twitter の bot2 を書いた時にも使った,Web Services by Yahoo! JAPAN の中の気象情報APIとYahoo! ジオコーダAPIを組み合わせてみます.
(簡単な,そしていくつか絶対改善できるやろって点のある)成果物はこちら.stack install
して rtp+=rainfall-vim-hs/vim
で使えはします.:RainfallStart
して :Rainfall 左京区
とかしてください.
簡単な機構
Vim からの話し方
とりあえず channel-demo
に書いてあるのをおおよそなぞってみることにします.
let channel = ch_open('localhost:8765')
これで channel を開く(見たらわかる).やりとりのモードは以下の4つがあって,
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()|
ch_open
では何もしないと JSON
モードになるので以降これの話をします.
Vim からデータを送る函数はいくつかありますが,:h channel-use
をみるとこんな感じです:
- データを synchronously 送って結果を得る3:
let response = ch_evalexpr(channel, {expr})
- 結果は無視して非同期に送る4:
call ch_sendexpr(channel, {expr})
- 結果を handler に渡す:
call ch_sendexpr(channel, {expr}, {'callback': Handler})
- 他に自分で全部面倒を見る
ch_sendraw
などもあります.
Vim はこれらをIDとして振った番号と一緒に送ります.すなわち,もし "hello"
という string を送ろうとした場合,実際には(たとえば) [3, "hello"]
というリストが送られることになります.
Vim への話し方
Vim は先ほど振られたIDでどれへのお返事かを判別します.従って [3, "panzer"]
に対して返答するなら [3, "vor"]
といった形式でよろしい.こちらから話しかけたいときは最初を0
にして [0, "pon!"]
と言うふうにします.
さらに,handler をすっ飛ばして直接コマンドを投げることもできます.:h channel-commands
に一覧されていますが,例えば ["ex","echo 'ankou'"]
というような形式になります.
なお,
call ch_logfile('channellog', 'w')
というふうにすればログを書き残せるので便利.
Haskell 側を書く
シンプルなサーバ
Haskell でシンプルなサーバを書くならnetworkの Network.Socket
を使うのも手ですが,どうやら conduit-extra (旧称 network-conduit) の Data.Conduit.Network
を使うと楽そうです5.基本的な演算子 (=$=
とか) はふつうの conduit 由来なのでこれとか読んで勉強しましょう6.
基本的にはこういう感じ
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 がこういうの
conduit = do
str <- await
case str of
Nothing -> return ()
(Just s) -> do
yield . id $ s
conduit
だとこれは echo サーバになり,[1,"Duce"]
には [1,"Duce"]
を返すので, ch_evalexpr
等からみても echo になります7.
この段階でもう基本的には vim とお喋りする道具は揃った!!ので(たとえば Text -> Text
とか Text -> m Text
を書けばよい) 是非遊んでみましょう.
中身を育てる
ここからはあんまり関係ないのでサクッと書いてしまいます.結論だけいうと,
- 簡単にリクエスト作って投げるには http-conduit が楽8.
- json のパーズには aeson を9
- データ型を定義するまでもないときは lens-aeson がべんり 10
- 話し終わったらプロセス全体が終了して欲しい時は
MVar
とかで管理するといいっぽい.
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プラグインが出来るまで - ぼっち勉強会とか :help
を見ながらというかんじ.スコープとかも不安があるので精進します.
最初にサーバを立ち上げるのをどうしたらいいんだろうってなって,とりあえず job_start
を使っている.起動後ちゃんとお話ができるようになるまで待ってからチャンネルを開かないといけなくて,ここをとりあえず ch_read
で読むことでやってるのだけど,絶対もっとまっとうなやり方あるよなあ…:h job-options
とか読みながら10分ほど試してうまく行かず一旦休憩中です.
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さんの記事のおかげ.
実際にお喋りするところはシンプルでいい.
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 からお喋りできるのはやはりハッピー.みんながお好きないろんな言語で試せればいいとおもいます.バッファの書き換え11とか,out-io
とかと組み合わせると夢が広がりますね.あるいは薄い wrapper を書いておくと幸せ人口がより増えるかも知れません.
とりあえず今は動く実例を増やすだけでもなんかの意義があるかと思って書きましたが,もっときちんと書かれたコードや解説記事の露出が増えるのを願っています.また今回のコードについては,Haskeller と Vimmer 双方からの厳しいご指摘を希望しつつ待機しています.
-
というより自分のアカウントに載せるサイボーグ機能でした ↩
-
古くていくつも関数が変わっていますが翻訳記事の ConduitとHaskellでネットワークプロキシサーバを作る - 純粋関数空間 とかは雰囲気をつかむのに良い.あるいは Building an Async Chat Server with Conduit - School of Haskell もいい tutorial です. ↩
-
つまり, ビルドして
./Main
で実行し, vim を立ち上げてlet channel=ch_open('localhost:4567')
し,echom ch_evalexpr(channel, "Duce!")
すると"Duce!"
が返ってくるということ. ↩ -
Haskellから簡単にWeb APIを叩く方法 などもよくまとまっています ↩
-
HaskellでのJSONパースがこんなに簡単だったとは.これも基本はlensなので,lensの勉強をすればよいです.lens自身については A Little Lens Starter Tutorial - School of Haskellがよい. ↩
-
そういえば,vim-transformとかを参考にできそう? ↩