Vim script で flatmap
しょーもない話ですが最近になって Vim script を書き始めたので。
全く知らなかったのですが、 Vim 7.0 あたりから map
関数や filter
関数が実装されていた模様。
また、 Vim 8.0 からはラムダ式が、 8.2 からは UFCS っぽいもの1が導入されたそうです。
map
や filter
があるなら 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] })
てな感じで、ほぼほぼ想定通りに動きました。
map
や filter
が辞書を引数に取り得るので、今回はそれに倣い辞書の場合も想定してみました。
ただし、特にメリットが分からないので map
filter
とは違い新たにオブジェクトを作り直しています。
ローカルスコープの関数を定義することができないので、ローカルレベルで辞書を作成して中身に関数を定義することで疑似的にローカル関数を定義するハックを利用しています。
-
Unified Function Call Syntax の略。
func(arg1, arg2, arg3)
をarg1->func(arg2, arg3)
のように、関数の第一引数のメソッドであるかのように関数呼び出しを書けます。これによってメソッドチェーンの如く数珠繋ぎにフローを書くことができ、可読性が高まります。 Dlang での名称。 Wikipedia(en) には記事がありますが、その他の言語では UFCS が正式な名称ではなさそうなので「っぽいもの」としました。↩ - このことは Vim のマニュアルにも書かれています。↩
scoop cleanup * はいいぞ
Windows を開発機にしているユーザーの100%が導入している1と言われる Scoop ですが、インストールされているアプリケーションのアップデートが溜まってくると割とストレージを圧迫します。
そんなときは scoop cleanup *
として過去のバージョンを消してしまうと効果があります。
これで
これが
こうなる。
なお、私の場合は入れているアプリケーションは以下です。
なお、 scoop cache rm *
も効果があります。私はやりませんが。
いや別にデメリットがあるわけじゃないんですけどね。
余談ですが、前にいた会社では Scoop 経由で他にもたくさん入れていたのでこれ以上に効果がありました。
-
私調べ (N=1)↩
nim / rust / pony の速度比較
※ 最初に申し上げておきますが、とても浅はかな比較です。
Rust は言わずもがな、「最も愛されているプログラミング言語」4年連続1位1の言語です。
データの生存期間を型で管理することで、GCなしにメモリセーフなプログラミングを可能にしてますね。
トレイトやマクロなどもあり、表現力も豊富で開発も盛んなので注目ですね。
実行速度もちょくちょく C++ に勝ったり負けたりと、非常に高速なことで知られます。
Nim は柔軟な書き方ができる言語で、最近お気に入りです。
ついこの間、めでたく v1.0 を迎えて安定期に入りました🎉
遅延リストのリテラルがないのが残念です。マクロはあるのでその気になれば自力実装はできそうですが、現時点では私には Nim 力が足らず無理です。
Nim で書かれたコードは一度 C のコードに変換されてから2、C のコンパイラでさらにネイティブコードにコンパイルされます。
Pony は昨日3初めて知った言語ですが、割と前からあるみたいですね。
言語構文レベルでアクターモデルを採用しているのが、たぶん最大の特徴です。
Rust に似て変数の所有権のようなもの4を型に持たせることで、アクターモデルでありながらデータのコピーを極力減らすことができ、高速かつ安全なプログラミングを可能にしているようです。
補完にまで対応した IDE やエディタ拡張が見当らないので、それがネックですね…。
まだセルフホストには至っていない模様。
環境
$ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.3 LTS (Bionic Beaver)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 18.04.3 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic
$ rustc --version rustc 1.39.0 (4560ea788 2019-11-04)
$ nim --version Nim Compiler Version 1.0.2 [Linux: amd64] Compiled at 2019-10-22 Copyright (c) 2006-2019 by Andreas Rumpf git hash: 193b3c66bbeffafaebff166d24b9866f1eaaac0e active boot switches: -d:release
$ ponyc --version 0.33.0-98c36095 [release] compiled with: llvm 7.0.1 -- cc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0 Defaults: pic=true
比較
なるべく公平になるようにコードを書いているつもりです。
FizzBuzz
コード
Rust
fn fizz_buzz(n: i32) { let s = match (n % 3, n % 5) { (0, 0) => "FizzBuzz".to_string(), (0, _) => "Fizz".to_string(), (_, 0) => "Buzz".to_string(), _ => n.to_string(), }; println!("{}", s); } fn main() { for x in 1..=100 { fizz_buzz(x); } }
Nim
proc fizzBuzz(n: int) = let (x, y) = (n mod 3, n mod 5) let s = if (x, y) == (0, 0): "FizzBuzz" elif x == 0: "Fizz" elif y == 0: "Buzz" else: $n echo s for x in 1..100: fizzBuzz x
Pony
use "collections" actor FizzBuzz let _env: Env new create(env': Env) => _env = env' be fizz_buzz(x: I32) => let s = match (x % 3, x % 5) | (0, 0) => "FizzBuzz" | (0, _) => "Fizz" | (_, 0) => "Buzz" else x.string() end _env.out.print(s) actor Main new create(env: Env) => let fb = FizzBuzz(env) for x in Range[I32](1, 101) do fb.fizz_buzz(x) end
コンパイル速度
Rust
$ time -p rustc -Copt-level=3 -Clto -Cpanic=abort fizzbuzz.rs real 2.23 user 1.96 sys 0.39
Nim
$ time -p nim c -d:release --opt:speed --hints:off fizzbuzz CC: stdlib_io.nim CC: stdlib_system.nim CC: fizzbuzz.nim real 2.03 user 1.70 sys 0.48
Pony
$ time -p ponyc --verbose=0 real 2.84 user 2.03 sys 0.81
Rust | Nim | Pony | |
---|---|---|---|
real | 2.23 | 2.03 | 2.84 |
user | 1.96 | 1.64 | 2.12 |
sys | 0.39 | 0.54 | 0.70 |
コンパイル速度はそこまで差が出ないようです。
※ ただし、Nim はコンパイル結果をキャッシュするので、同じプロジェクトであれば2度目以降のコンパイルが非常に高速になります。 --forceBuild
/ -f
オプションを付けるかキャッシュを削除すると1度目と同じ条件になります。今回は毎回キャッシュを削除しました。
※ Rust は最適化オプションをいくつか外すことで、成果物のファイルサイズを犠牲(3倍近く)にかなり高速(4分の1以下)になりました。
ファイルサイズ
Rust
$ ls -l fizzbuzz -rwxrwxrwx 1 htsign htsign 928400 11月 21 21:58 fizzbuzz*
Nim
$ ls -l fizzbuzz -rwxrwxrwx 1 htsign htsign 88864 11月 21 21:59 fizzbuzz*
Pony
$ ls -l fizzbuzz -rwxrwxrwx 1 htsign htsign 163176 11月 21 21:59 fizzbuzz*
Rust | Nim | Pony |
---|---|---|
928,400 bytes (907KB) | 88,864 bytes (87KB) | 163,176 bytes (159KB) |
※ KB 表記の端数は四捨五入
Rust が圧倒的にデカいですね。
Rust のバイナリがデカい理由はデバッグシンボルが残っているからかと思われます。最適化オプション有効にしてるのに…。
ここではコマンド単体で完結することを前提とするのでこれを最終結果とします。
実行速度
Rust
$ time -p ./fizzbuzz > /dev/null real 0.01 user 0.00 sys 0.01
Nim
$ time -p ./fizzbuzz > /dev/null real 0.00 user 0.00 sys 0.00
Pony
$ time -p ./fizzbuzz > /dev/null real 0.02 user 0.00 sys 0.01
Rust | Nim | Pony | |
---|---|---|---|
real | 0.01 | 0.00 | 0.02 |
user | 0.00 | 0.00 | 0.00 |
sys | 0.01 | 0.00 | 0.01 |
まぁ当然ですが、負荷が軽すぎてほとんど差が出ないですね…。
45番目のフィボナッチ数
※ 45番目なのは計算時間的にちょうどよさそうなので。
コード
Rust
fn fibonacci(n: i64) -> i64 { match n { 1 | 2 => 1, _ => fibonacci(n - 2) + fibonacci(n - 1), } } fn main() { println!("{}", fibonacci(45)) }
Nim
func fibonacci(n: int64): int64 = case n of 1, 2: 1.int64 else: fibonacci(n - 2) + fibonacci(n - 1) echo fibonacci(45)
Pony
primitive Fibonacci fun fib(n: I64): I64 => match n | 1 | 2 => 1 else fib(n - 2) + fib(n - 1) end actor Main new create(env: Env) => env.out.print(Fibonacci.fib(45).string())
コンパイル速度
※ 実行コマンドは FizzBuzz のとほとんど変わらないので割愛
Rust | Nim | Pony | |
---|---|---|---|
real | 2.20 | 2.00 | 1.19 |
user | 1.75 | 1.64 | 0.89 |
sys | 0.45 | 0.48 | 0.29 |
なぜか Pony が FizzBuzz の場合と比べてちょっと速くなりました。
ファイルサイズ
Rust | Nim | Pony |
---|---|---|
924,024 bytes (902KB) | 84,288 bytes (82KB) | 158,696 bytes (155KB) |
こちらも相変わらず Rust がダントツにデカいです。
実行速度
Rust | Nim | Pony | |
---|---|---|---|
real | 2.88 | 3.45 | 3.52 |
user | 2.87 | 3.45 | 3.48 |
sys | 0.01 | 0.00 | 0.03 |
さっきと比べて少し差が出ましたね。
Rust、やはり速い。
並列性を上げて計算すれば Pony も速くなったりするのでしょうか。
以上、Pony はまだまだ発展途上ですし、Rust もバージョン上がるごとに最適化が進んだりするので、本当にガバガバ比較です。
結局のところ好みで選べばいいんじゃないかと思います。
並列計算が目的なら Erlang や Elixir などの選択肢もあります。が、あれはプロセス単位での並列なのでちょっと事情が違うかも。
-
https://insights.stackoverflow.com/survey/2019#technology-_-most-loved-dreaded-and-wanted-languages↩
-
いわゆるトランスパイル↩
-
投稿時間が日を跨いでしまったので…。↩
-
Pony では Reference Capabilities と呼ばれます。↩
Promise を再帰させるとどうなるか
ただの興味本位です。
以下のコードを各環境で動かしていきます。
const p = n => new Promise(resolve => { console.log(n); resolve(p(n + 1)); }); p(1);
なるべく公平を期すため、ブラウザの場合は about:blank
にて実行します。
環境
PC1:
種別 | 詳細 |
---|---|
OS | Windows 10 |
CPU | AMD Rizen 5 1600 |
MEM | DDR4 PC4-24000 16GB x2 (空き 20000MB程度) |
PC2:
種別 | 詳細 |
---|---|
OS | macOS 10.14.6 (Mojave) |
CPU | Intel Core i5 5250U |
MEM | LPDDR3 PC3-12800 4GB (空き1000MB程度) |
結果
環境 | 再帰回数 | 備考 |
---|---|---|
Edge 44 | 1000 | 20回置きにウェイトが入る。終了時は特に出力なし |
Safari | 3674 | コンソールにはエラーが表示されず、 ブラウザページにポップアップで Maximum call stack size exceeded. と表示され終了 |
Firefox 69 (PC1) | 582 | too much recursion の例外で終了 |
Firefox 69 (PC2) | 2946 | too much recursion の例外で終了 |
Chrome 77 (PC1) | 1934 | RangeError: Maximum call stack size exceeded の例外で終了 |
Chrome 77 (PC2) | 1929 | RangeError: Maximum call stack size exceeded の例外で終了 |
node 10.16.3 (PC1) | 1818 | エラーを吐かずにクラッシュして終了 |
node 10.16.3 (PC2) | 1847 | RangeError: Maximum call stack size exceeded の例外で終了 |
node 12.11.1 (PC1) | 1806 | エラーを吐かずにクラッシュして終了 |
node 12.11.1 (PC2) | 1835 | RangeError: Maximum call stack size exceeded の例外で終了 |
この規則性のない結果から、おそらく Promise における再帰については厳密な仕様はなく実装依存であることが推定されますが、仕様書のどの辺りを見ればよいのか分からず…。
同じバージョンでも環境によって結果が異なるので、メモリ等の状態によっても変わるのかなと思いますが、ブラウザのソースコード読む気力もなく、「とりあえず Promise の再帰はなるべく避けよう」という辺りで落ち着けておきます。