htsign's blog

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

InoReader 向けに、はてブ integration を追加する UserScript

こんな感じになります。

f:id:htsign:20200511130635p:plain

「動けばいい」をモットーにクソ雑実装。
暇すぎて他にすることなくなったらリファクタリングする可能性が微粒子レベルで存在します。

gist.github.com

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 のマニュアルにも書かれています

scoop cleanup * はいいぞ

Windows を開発機にしているユーザーの100%が導入している1と言われる Scoop ですが、インストールされているアプリケーションのアップデートが溜まってくると割とストレージを圧迫します。

そんなときは scoop cleanup * として過去のバージョンを消してしまうと効果があります。

これで

f:id:htsign:20200127211048p:plain

これが

f:id:htsign:20200127211240p:plain

こうなる。

f:id:htsign:20200127211253p:plain

なお、私の場合は入れているアプリケーションは以下です。

f:id:htsign:20200127211636p:plain

なお、 scoop cache rm * も効果があります。私はやりませんが。
いや別にデメリットがあるわけじゃないんですけどね。

余談ですが、前にいた会社では Scoop 経由で他にもたくさん入れていたのでこれ以上に効果がありました。


  1. 私調べ (N=1)

nim / rust / pony の速度比較

※ 最初に申し上げておきますが、とても浅はかな比較です。

Rust は言わずもがな、「最も愛されているプログラミング言語」4年連続1位1の言語です。
データの生存期間を型で管理することで、GCなしにメモリセーフなプログラミングを可能にしてますね。
トレイトやマクロなどもあり、表現力も豊富で開発も盛んなので注目ですね。
実行速度もちょくちょく C++ に勝ったり負けたりと、非常に高速なことで知られます。

Nim は柔軟な書き方ができる言語で、最近お気に入りです。
ついこの間、めでたく v1.0 を迎えて安定期に入りました🎉
遅延リストのリテラルがないのが残念です。マクロはあるのでその気になれば自力実装はできそうですが、現時点では私には Nim 力が足らず無理です。
Nim で書かれたコードは一度 C のコードに変換されてから2、C のコンパイラでさらにネイティブコードにコンパイルされます。

Pony は昨日3初めて知った言語ですが、割と前からあるみたいですね。
言語構文レベルでアクターモデルを採用しているのが、たぶん最大の特徴です。
Rust に似て変数の所有権のようなもの4を型に持たせることで、アクターモデルでありながらデータのコピーを極力減らすことができ、高速かつ安全なプログラミングを可能にしているようです。
補完にまで対応した IDE やエディタ拡張が見当らないので、それがネックですね…。
まだセルフホストには至っていない模様。

環境

WSL Ubuntu on Windows 10

$ 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 などの選択肢もあります。が、あれはプロセス単位での並列なのでちょっと事情が違うかも。


  1. https://insights.stackoverflow.com/survey/2019#technology-_-most-loved-dreaded-and-wanted-languages

  2. いわゆるトランスパイル

  3. 投稿時間が日を跨いでしまったので…。

  4. 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 の再帰はなるべく避けよう」という辺りで落ち着けておきます。