htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

Vim script で flatmap

しょーもない話ですが最近になって Vim script を書き始めたので。

全く知らなかったのですが、 Vim 7.0 あたりから map 関数や filter 関数が実装されていた模様。
また、 Vim 8.0 からはラムダ式が、 8.2 からは UFCS っぽいもの1が導入されたそうです。

mapfilter があるなら flatmap もあっていいだろうと。
※ なお、 map filter は新しくリストや辞書を作るのではなく、与えられたオブジェクトそのものを変化させるので、他の言語のノリで使うとハマります。 copy を予め使うといい感じです。2

さて、実装はこんな感じです。

function! FlatMap(obj, body) abort
    let l:privates = {}
    function l:privates.for_list() abort closure
        let l:new_list = []
        for l:x in a:obj
            let l:new_list += a:body(l:x)
        endfor
        return l:new_list
    endfunction
    function l:privates.for_dict() abort closure
        let l:new_dict = {}
        for l:item in values(a:obj)
            for [l:key, l:val] in items(l:item)
                let l:new_dict[l:key] = a:body(l:val)
            endfor
        endfor
        return l:new_dict
    endfunction

    return get({
        \ v:t_list: l:privates.for_list,
        \ v:t_dict: l:privates.for_dict,
    \ }, type(a:obj), { -> v:null })()
endfunction

これで…

" [1, 1, 2, 2, 3, 3]
echo [1, 2, 3]->FlatMap({ x -> [x, x] })

" [1, 2, 3, 2, 4, 6, 3, 6, 9]
echo [
    \ { 'inner': [1, 2, 3] },
    \ { 'inner': [2, 4, 6] },
    \ { 'inner': [3, 6, 9] },
\ ]->FlatMap({ x -> x.inner })

" {'1': '(10)', '2', '(20)', 'bbb': '(BBB)', 'aaa': '(AAA)'}
echo { 'a': { 'aaa': 'AAA', 'bbb': 'BBB' }, '1': { '1': 10, '2': 20 } }
    \ ->FlatMap({ x -> printf('(%s)', x) })

" v:null
echo (100)->FlatMap({ x -> [x, x] })

てな感じで、ほぼほぼ想定通りに動きました。

mapfilter が辞書を引数に取り得るので、今回はそれに倣い辞書の場合も想定してみました。
ただし、特にメリットが分からないので map filter とは違い新たにオブジェクトを作り直しています。

ローカルスコープの関数を定義することができないので、ローカルレベルで辞書を作成して中身に関数を定義することで疑似的にローカル関数を定義するハックを利用しています。


  1. Unified Function Call Syntax の略。 func(arg1, arg2, arg3)arg1->func(arg2, arg3) のように、関数の第一引数のメソッドであるかのように関数呼び出しを書けます。これによってメソッドチェーンの如く数珠繋ぎにフローを書くことができ、可読性が高まります。 Dlang での名称。 Wikipedia(en) には記事がありますが、その他の言語では UFCS が正式な名称ではなさそうなので「っぽいもの」としました。
  2. このことは Vim のマニュアルにも書かれています