13
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-07-02

今年に入った辺りから Vim に極めて活発にパッチが入り始め,1 月に smileが実装された時にはまだ7.4.1005だったのが本記事を書いている時点での最新のパッチはすでに7.4.1980となっています.

この間には実に様々な機能が入り,すこし数えるだけでも一応のパッケージ機構,以前から需要のあった true color support のあるターミナルへの対応函数の部分適用めいたものへの対応1,などが実装されました.なかでも 1月 から 2月 にかけて timer, job や channel の機構が導入されたのは多くの人に大きな喜びと期待をもって迎えられました.

:h channel にはこうあり, (16 Jul 2016 追記: 本日 DRAFT 表記がとれました!)

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 の中の気象情報APIYahoo! ジオコーダAPIを組み合わせてみます.

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

簡単な機構

Vim からの話し方

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

vim
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 でシンプルなサーバを書くならnetworkNetwork.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 のパーズには aeson9
  • データ型を定義するまでもないときlens-aeson がべんり 10
  • 話し終わったらプロセス全体が終了して欲しい時は MVar とかで管理するといいっぽい.
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プラグインが出来るまで - ぼっち勉強会とか :help を見ながらというかんじ.スコープとかも不安があるので精進します.

最初にサーバを立ち上げるのをどうしたらいいんだろうってなって,とりあえず job_start を使っている.起動後ちゃんとお話ができるようになるまで待ってからチャンネルを開かないといけなくて,ここをとりあえず ch_read で読むことでやってるのだけど,絶対もっとまっとうなやり方あるよなあ…:h job-optionsとか読みながら10分ほど試してうまく行かず一旦休憩中です.

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さんの記事のおかげ.

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

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 からお喋りできるのはやはりハッピー.みんながお好きないろんな言語で試せればいいとおもいます.バッファの書き換え11とか,out-io とかと組み合わせると夢が広がりますね.あるいは薄い wrapper を書いておくと幸せ人口がより増えるかも知れません.

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

  1. このあとしばらくいろいろ大変でしたが…

  2. というより自分のアカウントに載せるサイボーグ機能でした

  3. :h ch_evalexpr

  4. :h ch_sendexpr

  5. 古くていくつも関数が変わっていますが翻訳記事の ConduitとHaskellでネットワークプロキシサーバを作る - 純粋関数空間 とかは雰囲気をつかむのに良い.あるいは Building an Async Chat Server with Conduit - School of Haskell もいい tutorial です.

  6. 読んでないですが日本語訳も投稿されています.

  7. つまり, ビルドして ./Main で実行し, vim を立ち上げて let channel=ch_open('localhost:4567') し, echom ch_evalexpr(channel, "Duce!") すると "Duce!" が返ってくるということ.

  8. Haskellから簡単にWeb APIを叩く方法 などもよくまとまっています

  9. fieldLabelModifier便利

  10. HaskellでのJSONパースがこんなに簡単だったとは.これも基本はlensなので,lensの勉強をすればよいです.lens自身については A Little Lens Starter Tutorial - School of Haskellがよい.

  11. そういえば,vim-transformとかを参考にできそう?

13
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?