Vim
VimDay 25

Lambda 関数に俺はなる

この記事は Vim Advent Calendar 2017 の25日、最終日の記事です。その1その2、多くの方に参加頂きました。参加頂いた皆様、本当にご苦労様でした。そして完走おめでとうございます。

今年のおさらい

今年は Vim の国際カンファレンス Vimconf2017 も開催され、未だ衰えを知らない Vim の人気を再認識する年になったと思っています。

Vim バージョン 8.0 がリリースされたのが去年の9月の事。約1年くらいですが、最近になってようやく皆さんに認知されてきたのかなとも思います。Vim 8.0 の新機能を今一度おさらいしておくと、大きくは以下の物がありました。

  • チャネルによる非同期 I/O のサポート (Channel)
  • ジョブ (Job)
  • タイマー (Timer)
  • パーシャル (Partial)
  • ラムダ (Lambda)、クロージャ (Closure)
  • パッケージ (Packages)

引用: http://vim-jp.org/blog/2016/09/13/vim8.0-features.html

今考えると、これらが無い頃に Vimmer はいったいどうやって Vim script を書いていたんだろうと思うくらい重要な機能が入ったと思っています。

Lambda は凄い

特に僕が気に入っているのが Lambda です。これは元々、僕が sort 関数の引数で比較演算の指定方法が面倒で、どうしても欲しくて作り始め、さらに kaoriya さんや vim-jp の皆さん、そして
k-takata さんに後押しして貰って入れて貰いました。

例えば辞書の配列を sort するには以下の様に書く必要がありました。

function! s:by_name(lhs, rhs)
  return a:lhs.name < a:rhs.name
endfunction

let foo = [
\ {"name": "alice", "age": 16},
\ {"name": "john", "age": 14},
\ {"name": "bob", "age": 13},
\]
echo sort(foo, function('s:by_name'))

比較処理を書くだけの為にいちいち関数を定義しないといけなかったのです。これが Lambda を使うと以下の様に簡単に書く事が出来るのです。

let foo = [
\ {"name": "alice", "age": 16},
\ {"name": "john", "age": 14},
\ {"name": "bob", "age": 13},
\]
echo sort(foo, {lhs, rhs -> lhs.name < rhs.name})

Lambda は式しか書けません。例えば変数に値を代入したり echo コマンドなど、文は直接書けないのです。ただし方法がない訳ではありません。例えばタイマーを使い、1秒に1回、時刻を表示するのであれば以下の様に execute() 関数を使う事で実現できます。

let s:timer = timer_start(1000, {t ->
\ execute('echo strftime("%c", localtime())', '')},
\ {'repeat': -1})

さらにカウントアップする数字を表示するのであれば以下の様に List を使います。

let s:count = 0
let s:timer = timer_start(1000, {t -> [
\   execute('let s:count += 1'),
\   execute('echo s:count', ''),
\ ]}, {'repeat': -1})

Lambda と execute() 関数があればつまりはなんでも出来る訳です。

Lambda でもやれる

例えば List から Dict を作りたい時、皆さんはどうやっているでしょうか? ['key1', 'value1', 'key2', 'value2', ...] から {'key1': 'value1', 'key2': 'value2', ...} を作るといった場合です。関数を作りますか?いえ、出来るんです。Lambda でも。

let l = [['key1', 'value1'], ['key2', 'value2'], ['key3', 'value3']]
echo call({x,y-> [map(x, 'execute("let y[v:val[0]]=v:val[1]")'),y]}, [l,{}])[1]

call() 関数は第一引数に関数、第二引数に関数への引数を List で渡します。今回であれば対象の List と空の Dictionary {} を渡しています。Lambda 関数の引数 x には List が、y には {} が格納されます。map() 関数は List と Dictionary のどちらでも引数として取れますが、戻り値も同じ型が返ります。つまり List として引数に与えた場合は map() および call() は List を返します。そこで List と空の Dictionary を双方保持する List をコンテナとして引数として与え、call() の戻り値から [1] で Dictionary 側を参照しているのです。

簡単ですね。

Lambda でスパイラル

例えば以下の様に、外側から数字が小さくなりつつ真ん中で終了する図形を描くにはどうすれば良いでしょうか?

99  98  97  96  95  94  93  92  91  90
64  63  62  61  60  59  58  57  56  89
65  36  35  34  33  32  31  30  55  88
66  37  16  15  14  13  12  29  54  87
67  38  17   4   3   2  11  28  53  86
68  39  18   5   0   1  10  27  52  85
69  40  19   6   7   8   9  26  51  84
70  41  20  21  22  23  24  25  50  83
71  42  43  44  45  46  47  48  49  82
72  73  74  75  76  77  78  79  80  81

出典: Challenge - Print Spiral

出来ます!Lambda でも。

echo join(call({n->map(range(n),{i,x->join(map(range(i*n,i*n+n-1),{->printf("%*i ",float2nr(ceil(log10(n*n-1))),call(function({...->a:1(a:1,a:2,a:3,a:4)},[{e,n,x,y->n%2==0?(y==0?n*n-1-x:(x==n-1?n*n-n-y:e(e,n-1,x,y-1))):(y==n-1?n*n-n+x:(x==0?n*n-n-(n-1)+y:e(e,n-1,x-1,y)))}]),[n,v:val%n,v:val/n]))}),' ')})},[10]),"\n")

スパイラル

作り方はそれほど難しくありません。まず上記 dev.to のリンク先にあるコードをそのまま Vim script に移植します。

function! s:e(n,x,y)
  let sq = a:n * a:n
  if a:n%2 == 0
    if a:y == 0
      return sq - 1 - a:x
    endif
    if a:x == a:n-1
      return sq - a:n - a:y
    endif
    return s:e(a:n-1,a:x,a:y-1)
  endif
  if a:y == a:n-1
    return sq - a:n + a:x
  endif
  if a:x == 0
    return sq - a:n - (a:n - 1) + a:y
  endif
  return s:e(a:n-1,a:x-1,a:y)
endfunction

let n = 10
let l = float2nr(ceil(log10(n * n-1)))
for y in range(n)
  for x in range(n)
    echon printf("%*i ", l, s:e(n,x,y))
  endfor
  echo ""
endfor

次に関数を Lambda に、そして if 文を三項演算子に置き換えます。

let s:e = {n,x,y->
\  n%2 == 0 ? (
\    y == 0 ?
\      n * n - 1 - x
\    : (
\      x == n-1 ?
\        n * n - n - y
\      :
\        s:e(n-1,x,y-1)
\    )
\  ) : (
\    y == n-1 ?
\      n * n - n + x
\    : (
\      x == 0 ?
\        n * n - n - (n - 1) + y
\      :
\        s:e(n-1,x-1,y)
\    )
\  )
\}

let n = 10
let l = float2nr(ceil(log10(n * n-1)))
for y in range(n)
  for x in range(n)
    echon printf("%*i ", l, s:e(n,x,y))
  endfor
  echo ""
endfor

ここで1つ問題が発生します。Lambda 関数を格納した変数 s:e は再帰呼び出しを行う必要があります。つまり変数を無くすという事は再帰呼び出しが出来なくなるのです。

そこで登場するのが Vim 8.0 で入った Partial です。

let X = function('s:foo', [1,2,3])

Partial を使うと X() という呼び出しの際に s:foo に引数 1, 2, 3 を渡す事が出来ます。つまり部分引数として Lambda を渡す事で、function() の第一引数で与える関数の引数としてバインドできる事になります。

let s:e = function({...->a:1(a:1,a:2,a:3,a:4)}, [{e,n,x,y->
\  n % 2 == 0 ? (
\    y == 0 ?
\      n * n - 1 - x
\    : (
\      x == n-1 ?
\        n * n - n - y
\      :
\        e(e, n-1, x, y-1)
\    )
\  ) : (
\    y == n-1 ?
\      n * n - n + x
\    : (
\      x == 0 ?
\        n * n - n - (n - 1) + y
\      :
\        e(e, n-1, x-1, y)
\    )
\  )
\}])

let n = 10
let l = float2nr(ceil(log10(n * n-1)))
for y in range(n)
  for x in range(n)
    echon printf("%*i ", l, s:e(n,x,y))
  endfor
  echo ""
endfor

これで s:e が消せます。あとは for 文で回している部分を range と map で実装し直せば、上記の様に1行でスパイラル図形が表示できるという訳です。

また最後の方にある 10 を大きくすれば、お好きな N x N の図形が表示されます。

簡単ですね。

Lambda で中華料理画像うpよろしく

こんな図形はどうでしょうか?

中華料理画像うpよろしく
華@@@@@@@@@@し
料@@@@@@@@@@ろ
理@@@@@@@@@@よ
画@@@@@@@@@@p
像@@@@@@@@@@う
う@@@@@@@@@@像
p@@@@@@@@@@画
よ@@@@@@@@@@理
ろ@@@@@@@@@@料
し@@@@@@@@@@華
くしろよpう像画理料華中

出典: https://anond.hatelabo.jp/20161015194043

出来ます。Lambda かわいいよ。

echo call({s->call({s,u->join([s]+map(range(2,len(u)-1),{i->u[i+1].repeat("@",len(u)-2).u[-i-2]})+[join(reverse(u),'')],"\n")}, [s, split(s,'\zs')])}, ['中華料理画像うpよろしく'])

中華料理画像うpよろしく

簡単ですね。

Lambda でカレンダー

UNIX で cal コマンドを実行すると以下の様に表示されます。

Su Mo Tu We Th Fr Sa
                1  2
 3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31

これは難しいでしょうか?いえ、出来ます。Lambda なら。

echo
\ "Su Mo Tu We Th Fr Sa\n"
\ .join(split(
\  repeat('   ', strftime('%w', localtime() - (strftime('%d', localtime())-1)*60*60*24))
\  .join(map(range(1,
\     call(
\       {y,m->
\         [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m-1] + ((m == 2 && y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 1 : 0)
\       }, [strftime(strftime('%y', localtime())), strftime(strftime('%m', localtime()))]
\     )), {_,x->printf('%02d', x)}), ' '), repeat('.', 21).'\zs'), "\n")

うるう年の計算も入っていて安心ですね。見やすくする為に \ を使って改行していますが、1行にすると以下の様になります。

echo "Su Mo Tu We Th Fr Sa\n".join(split(repeat('   ', strftime('%w', localtime() - (strftime('%d', localtime())-1)*60*60*24)).join(map(range(1,call({y,m->[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m-1] + ((m == 2 && y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 1 : 0)}, [strftime(strftime('%y', localtime())), strftime(strftime('%m', localtime()))])), {_,x->printf('%02d', x)}), ' '), repeat('.', 21).'\zs'), "\n")

実行してみましょう。

カレンダー1

カレンダー2

いちいち Vim を起動するの面倒だという人は、シェルで以下を実行すればいいのです。

$ vim --not-a-term --cmd 'echo "Su Mo Tu We Th Fr Sa\n".join(split(repeat('\''   '\'', strftime('\''%w'\'', localtime() - (strftime('\''%d'\'', localtime())-1)*60*60*24)).join(map(range(1, call({y,m->[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m-1] + ((m == 2 && y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 1 : 0)}, [strftime(strftime('\''%y'\'', localtime())), strftime(strftime('\''%m'\'', localtime()))])), {_,x->printf('\''%02d'\'', x)}), '\'' '\''), repeat('\''.'\'', 21).'\''\zs'\''), "\n")' --cmd q > /dev/null

カレンダー3

簡単ですね。

Lambda があれば強くなれる

この様に、Lambda があれば Vim script は強くなれるのです。綺麗な図形を1行で書いて同僚に見せれば、皆が貴方を見る目も変わるはずです。みなさんもぜひ Lambda やりましょう。いや、して下さい。そして Lambda になりましょう。