Unhandled Rejection の考え方

はじめに

twitter 上で議論になっていたネタを本人の許可を得て記載しています。

実はこの話は会社の中でも一回議論になったネタなんですよね。僕も microtask と呼ばれる Promise キューイングの仕組みとイベントループでタスクをハンドリングする仕組みの両方が組み合わさった時に Unhandled Rejection が起きる理由がわかりにくくなるなーと思っています。誤解していた様子でしたが、それもうなずけます。僕も誤解してました。今回はその理由を補足しながら解説してみます。

Unhandled Rejection

Promise には Pending, Fulfilled, Rejection の3状態があります。Pending は処理中の状態を示し、 Fulfilled と Rejection は Promise が成功したか失敗したかという2つの状態を指しています。 Rejection だった時に catch して処理が行われたのであれば、 Rejection はハンドリングされたものとなります。誰も catch しなかったら Rejection はハンドリングされなかった状態になります。この状態を Unhandled Rejection と呼びます。簡略化のために下記のコード例を記載します。

main()

async function main() {
    await fail(); // Unhandled Rejection
    console.log('Successfully handled')
}

async function fail() {
    throw new Error('failed')
}

4行目で throw されてる例外を誰も catch していないので、 Unhandled Rejection 扱いになります。try-catch.catch などの方法で catch すれば Unhandled Rejection 扱いではなくなります。

じゃあこのときはどうなるのか

// https://gist.github.com/shqld/be21d1e82cec4dccfc933477f9b60356

main()

async function main() {
    const tasks = []

    tasks.push(fail())

    // Commenting out this line makes it work
    await new Promise((resolve) => setTimeout(resolve, 0))

    tasks.push(fail())

    await Promise.allSettled(tasks)

    console.log('Successfully handled')
}

async function fail() {
    throw new Error('failed')
}

該当のスクリプトですね。この時も Unhandled Rejection になります。 main 関数の3行目の await new Promise((resolve) => setTimeout(resolve, 0)) で起きます。一見うまくいきそうに見えるから不思議ですね。しかもこのコードをそのまま Chrome 等で実行してみると特にエラーも何も出ずに成功したかのように見えます。

しかし、 Node.js/deno のときは Unhandled Rejection で process が落ちます。

Error: failed
    at fail (/private/tmp/t3.js:10:11)
    at main (/private/tmp/t3.js:5:11)
    at Object.<anonymous> (/private/tmp/t3.js:2:1)
    at Module._compile (node:internal/modules/cjs/loader:1097:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1149:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:17:47

ただし、 Chrome であったとしても、 ログのレベルを verbose に上げた状態で上のコードを実行すると Unhandled Rejection が発生していることが確認できます。

ログレベルで verbose まで上げておく
ログレベルで verbose まで上げておく

Unhandled Rejection エラーになる (Chrome)
Unhandled Rejection エラーになる (Chrome)

一旦スクリプトを細かく分割して整理しつつ、中でどんな事が起きてるのかを解説します。

もう少し細かく

Unhandled Rejection が起きるといいましたが、一応条件があります。それは Promise が内部的に利用している microtask キューと呼ばれるキューが空になったタイミング で Rejection 状態で handle されてない Promise があった時です。 また、 microtask キューは "queue" と名付けられているだけあって、 FIFO (First In First Out) な動きをします。つまり、queue にタスクが積まれるのですが、それは先頭から消費されていきます。

function main() {
  Promise.resolve().then(
    () => console.log("promise first") // キューに積まれる
  ).then(
    () => console.log("promise second")  // キューに積まれる
  );
  console.log("show first"); // 同期的に実行される
}

main();
// show first
// promise first
// promise second

microtask queue の動き
microtask queue の動き

microtask キューが空になったタイミングで Rejection 状態になると Unhandled Rejection になります。

function main() {
  Promise.resolve().then(
    () => { throw new Error("failed") } // rejection 状態になる
 ).then(
    () => console.log("promise second")  // rejection 状態なので then は通らず、ここは queue に積まれない
  ); // taskqueue が空になった状態で Rejection のままになる。このままだと Unhandled Rejection になる
  console.log("show first"); // 同期的に実行される
}

main();

Unhandled Rejection 状態
Unhandled Rejection 状態

元のスクリプトを見てみる

この状況で元のスクリプトを確認してみましょう。まずは setTimeout の Promise が無い状態だとどうなるかを考えてみます。

main()

async function main() {
    const tasks = []

    tasks.push(fail())

    // Commenting out this line makes it work
    // await new Promise((resolve) => setTimeout(resolve, 0))

    tasks.push(fail())

    await Promise.allSettled(tasks)

    console.log('Successfully handled')
}

async function fail() {
    throw new Error('failed')
}

この状態だと、 Promise.allSettled 関数が throw したエラーを内部的に catch してくれます。 Promise.allSettled はすべての Promise のタスクが Fulfilled にせよ Rejection にせよどの状態になったとしても終了するまでハンドルする関数です。この動きをさせるために内部的には Promise の例外をキャッチしてくれています。なので、この状況だと Promise は Unhandled Rejection にはなりません。普通に終了します。

Promise.allSettled により、Unhandled Rejection にならない
Promise.allSettled により、Unhandled Rejection にならない

ただ逆に最初の tasks.push(fail()) で止めたらどうなるでしょうか。

main()

async function main() {
    const tasks = []

    tasks.push(fail())
}

async function fail() {
    throw new Error('failed')
}

この場合は Unhandled Rejection になります。 microtasks のキューが空になった状態で fail() 内で Error をスローしてそのままだからです。

fail() が Throw してるので Unhandled Rejection
fail() が Throw してるので Unhandled Rejection

さて、 もう一回もとに戻してみましょう。今度は該当の setTimeout を入れてみます。

main()

async function main() {
    const tasks = []

    tasks.push(fail())
    
    await new Promise((resolve) => setTimeout(resolve, 0))

    tasks.push(fail())

    await Promise.allSettled(tasks)

    console.log('Successfully handled')
}

async function fail() {
    throw new Error('failed')
}

さらにもう少し説明を追加します。

Node.jsのイベントループモデルと一緒に解説します。下記のように EventLoop でのタスク管理と Promise でのタスク管理 (図で microtask queue と書いたところ)に分かれています。

EventLoop とタスク管理
EventLoop とタスク管理

緑色の箱で示したフェーズと青い箱で示した nexttick queue / microtask queue が存在しています。各フェーズにもコールバックを貯めておくキューが存在します。それぞれ赤い線で nexttick queue や microtask queue を適宜呼び出し、フェーズが終了した段階で microtask queue に積んだり、 nexttick queue に積んだりします。

上のコード例で言うと、 await new Promise((resolve) => setTimeout(resolve, 0)) の時に setTimeout をしているので、このタイミングで、 EventLoop の Timer のコールバックにタスクが積まれます。この時点で一旦 EventLoop 側に Task が移ります。

setTimeoutしたタイミングで Timer に処理が移るも、そのタイミングで microtask queue が空であると判定され、rejection したままになる
setTimeoutしたタイミングで Timer に処理が移るも、そのタイミングで microtask queue が空であると判定され、rejection したままになる

この際に microtask queue は一旦空になります。空になったタイミングで、 Handle されていない1つ目の fail() 関数が Error を Throw しているため、 Unhandled Rejection 扱いになります。

さぁ、わかりにくいですよね。非常にトリッキーです。Promise の処理が行われた後に 「EventLoop 側の処理機構に移る」という言い方をしていますが、この際に別なフェーズに処理が移ってしまいます。中の青い箱で示したキューから緑のフェーズに処理が移るタイミングで中の Promise のキューがあれば即座に実行されます。その結果、 fail() 内部でエラーが throw されているので Unhandled Rejection 扱いになってしまいます。ちなみに nexttick queue があればそちらを優先します。

setTimeout の内部が呼ばれた時点での microtask の状況
setTimeout の内部が呼ばれた時点での microtask の状況

同じ task の中で同期的に実行していれば (Timer の処理がなければ) Promise の Unhandled Rejection な状況にはなりません。同じタスク内の最後で allSettled などで catch してくれていれば、意図通りに動きます。しかし、その途中で setTimeout, setImmediate などで Timer の処理を動かしたり、 fs でファイルを操作したり、 http リクエストしたりするとその段階で EventLoop の各フェーズに処理が移ります。この時点で一回 microtask queue を実行します。この時に Rejection 状態になってしまっていると、 Unhandled Rejection 扱いになります。

わかりにくくしているポイント

実は Node.js / deno も Chrome もどちらも Unhandled Rejection に一回なっています。ただ Chrome の場合は一回 warn として処理された上で、次の tick に行った場合にそこでちゃんとハンドルされているのであれば、 verbose ログに出すだけで、エラーになりません。

一方で Node.js/deno は一回でも Unhandled Rejection になった場合はエラー扱いになり、落ちます。この動きの差はわかりにくいものの、仕様通りの挙動になっています。

Promise の ECMAScript 上での仕様は Unhandled Rejection 状況になったことを Platform 側に伝えるところまでが明記されています。ただ伝わった後どう Unhandled Rejection を処理するかは Platform 側で自由に決めて良いことになっています。 Chrome のようにクライアントサイドのブラウザであれば、Promise が例外だったとしても warn 扱いで留めた上で、次のイベントループで処理されていればログレベルを下げて表示する、という対策もありでしょう。逆に Node.js/deno はサーバサイドで未処理の例外が発生した状況のままなにも通知せずに動いてしまうとリソースの開放漏れがあったり、例外が発生してレスポンスを返せていないのに正常に動作してるかのように見えるといった、プロセスが落ちるよりもさらにひどい状況になり得ます。そのためプラットフォームごとに動きが異なる、という仕様になっています。非常にややこしいですね。。。

ちなみに Node.js の場合、デフォルトでその動きになっているものの、変更することも可能です。 --unhandled-rejections という起動オプションに対していくつかの設定が用意されています。

  • none UnhandledRejection が起きたことを警告はするが、特に詳細は表示しない。 Chrome とほぼ同じ扱い
  • warn Unhandled Rejection が起きたことを詳細に警告ログに残す
  • throw デフォルトの挙動、エラー扱いとして unhandledRejection イベントを process に通知、何もハンドラがなかったら uncaught exception と同じ扱いとして、エラーにする。
  • strict ハンドラの有無に関わらず、uncaught exception と同じ扱いとして、エラーにする。
  • warn-with-error-code Unhandled Rejection が起きたことを詳細に警告ログに残しつつ、終了時に exit code を 1 にする(異常終了扱いする)。

これらの扱いをうまく使えば Chrome と同じ挙動にすることも可能です。ただし、Node.js/deno といったプラットフォームが提供しているデフォルトの挙動は安全サイドに倒したものになっており、これを意図を考慮しないで変更することは個人的にはオススメしません。プラットフォームのデフォルトの動きがなぜそうなっているのかを把握した上で変更するのであれば、良いと思います。

まとめ

Unhandled Rejection の状況について解説しました。 Timer や fs といった EventLoop の機構と Promise の機構が混在しわかりにくくしている上に、Chrome などのブラウザは Unhandled Rejection なエラーを verbose でしか出していない所が、余計わかりにくくしています。ただ、それにはちゃんとした理由があるという所まで分かっていただけると幸いです。

個人的には Unhandled Rejection 状態になった時の Promise の仕様がプラットフォーム依存になっているところがもう少し周囲が把握できているといいなとは思います。 Chrome と Node/deno だけではなく、 Firefox とかも動きが違うので、本当に難しくなっています。 Node.js に関しては複数の動作パターンを用意していますが、 Unhandled Rejection 時のデフォルトの挙動をどうするべきか議論が白熱したため、このようにオプションできりかえられるようになっています。

今一度 Unhandled Rejection をプラットフォームがどう実装しているのかは確認してみると良いと思います。

参考資料

2021年振り返り

はじめに

今年も書きました。毎年書くことで去年との差分を知れるので、良いですね。来年もちゃんと書きます。

yosuke-furukawa.hatenablog.com

会社

去年は "マネジメントとシニアソフトウェアエンジニア" で4年目になったんですが、今年はそれに経営的なロールまでついて 5 年目突入しました。 大変でしたが、やりがいがある仕事につけて楽しいです。

qiita.com

この手の経営とマネジメントとシニアソフトウェアエンジニア的な話をするタイミングももらえてよかったです。

社内アドベントカレンダーもどちらも活況でした。

adventar.org

qiita.com

優秀なメンバーに恵まれてるなとつくづく感じます。

また、ニジボックス会社内で研修として JavaScript/TypeScript/Next.js 研修をメンバーと一緒にやることができました。

www.wantedly.com

www.wantedly.com

こういう研修が巡り巡って、リクルートの社内研修にも活かされていたりします。

recruit-tech.co.jp

そういえば研修ではブラウザの話も同様にしました。

speakerdeck.com

社内イベント

R-ISUCON 2021 を開催しました。

blog.recruit.co.jp

こういったイベントは完全にオンラインに変わりましたね。オフラインでやってた時のワイワイ感をいかにオンラインでも出すかという所にフォーカスされてきている気がします。 一応このイベントではオンライン飲み会用のケータリングサービスである nonpi を使って食事を配ったりしました。

イベント

JSConf.jp 2021

JSConf.jp 2021 を開催しました。

jsconf.jp

一年越しの開催で、だいぶ難しい形になってしまいましたが、非常に活況だったかなと思っています。 すべてのトークYoutube にスタッフの方が分割してあげてくれています。内容を後から確認できるところも含めて非常に面白いですね。

www.youtube.com

また、発表は StreamYard 、聴講は Youtube Live と Spatial Chat という二段構えで実施していました。もう少し Spatial Chat 側を盛り上げたかったなと思いましたが、また来年やる時にはちゃんと調整します。 一方で非常にオンラインは後片付け等が少なく、スタッフの負担も軽いので、これをどうするかは今後要検討ですね。まだ探り探りですが、来年もオンラインのがいいかなーと個人的には思っています。

東京 Node 学園

3回ほどオンラインで開校しました。

Node学園 35時限目 オンライントライアル - connpass

Node学園 36時限目 オンライン - connpass

Node学園 37時限目 オンライン - connpass

来年もちゃんとやります。オンラインになることでこちらの負担がだいぶ軽くなりましたね。一方でずっと続けてしまうと逆にリアルタイムで見なくていいやと思う人も増えそうなのでそれはそれで避けたい気もしていて、複雑です。

登壇系

Engineer Career Study

スペシャリストになる覚悟を話しました。だいぶ偉そうなことを書いていますが、何度もあの後色んな所で話す機会をいただき、参考になったというフィードバックをもらえてよかったです。

speakerdeck.com

Developer eXperience Day

フロントエンドエンジニアの開発体験について話しました。この時のトークも面白かったですね。他にビッグネームな人たちがたくさんいたことを覚えています。

speakerdeck.com

Node 学園 37 時限目

WHATWG Stream の話をしました。何気に Node.js の発表久々だったかも。

speakerdeck.com

Fastly Yamagoya

性能に関する考え方の話をしました。尊敬するエンジニアが Fastly には多いので、ちょっと張り切りました。

speakerdeck.com

この時に頂いたパーカーがマジですごかった(裏起毛で温かい、この時期外にパーカーだけで出てもなんとかなる)。重宝しています。

Chrome Advisory Board

Chrome 諮問委員会というボードのメンバーなのですが、そこで英語で JSConf.jp の話やどうやってコミュニティを運営しているかを発表させていただきました。

speakerdeck.com

英語で登壇したのですが、割と良いフィードバックをもらえたので、ものすごくありがたい機会でした。割と時間がなかったのですが、英語毎日勉強してたおかげで資料を作る時間はそこまでかからなかったです。

技術顧問勉強会系

この他にも技術顧問で勉強会を開いているのですが、以下の奴が良いフィードバックをもらえました。

レビューの仕方

speakerdeck.com

フロントエンドテストプラクティス

speakerdeck.com

書籍を書く

Web+DB Press vol.123 Next.js 特集

gihyo.jp

Next.js 特集を記述することができました。 @takepepe さんと共著しました

Node.js

CodeGrid

今年も CodeGrid で記事を書きました。

www.codegrid.net

www.codegrid.net

ブログ

急遽アドベントカレンダーを追加して3つくらいエントリーを書きました。問題提起系を1エントリ、あまり知られていない新機能系が2エントリ。

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

Rust

新しい言語を覚えようということで Rust を覚えました。楽しいのでやり続けます。なんか別なことにも使ってみたいですね。

yosuke-furukawa.hatenablog.com

競技プログラミング

Advent Of Code を完走したのと LeetCode を 800 問以上解きました。

LeetCode の戦績
LeetCode の戦績

Advent Of Code 2021
Advent Of Code 2021

yosuke-furukawa.hatenablog.com

英語

DMM英会話マスターレベルになりました。Happy almost New Year!! みたいなことを今日は言い合って終わりました。喋れるようになると色々とネタが出てくるので楽しいです。もっと使いたいですね。

DMM 英会話マスターレベル
DMM 英会話マスターレベル

ELSA Speak もやってました。

プログラミング

なんか競技プログラミングで毎日コードを書いてましたね。

github all green
github all green

とはいえ、もう少しなんか実のある事もやりたいんですよねー。できてないけど、来年はもう少しこの辺りをちゃんとアウトプットします。

まとめ

  • 会社でエンジニアコミュニティづくりをやってます
  • 登壇も頑張った、だいたい毎月一個くらいはなんかやってた気がする
  • 書籍も書いた。 Web DB Press は非常に良かった
  • Node.js 系の記事 5 件位書いた。
  • 新しく Rust を学んだ。
  • 競技プログラミングで LeetCode 800問解いたのと AoC 完走した
  • 英語で DMM 英会話マスターレベルになった。

振り返り

去年目標にしてできたこと: - 新しい言語の習得 - ブログその他のアウトプット

できなかったこと: - 新しい言語でなにか作る

来年の目標

来年の目標: - なにかライブラリやアプリを作る - 人に教える系の何かをちゃんとやりたい - もう少しブログのアウトプットを増やす

最後に

今年も一年間インプットもアウトプットも走り抜けた気がしています。今は少しゆっくりしたいかなーと思っています。 もう少ししたら新年です。個人的には新年になる前の数時間がものすごく好きで、まったり過ごそうと思っています。家族や友人とゆっくり過ごして新年リフレッシュしたいですね。 来年もまたよろしくお願いいたします。

実力なのか運なのか

最近 SNS を見ていて、「人に努力をしろ」と勧めにくくなった、「もっと努力した自分を称えるべきでは」といった内容の投稿を見かけました。 この他にも「運も実力の内」、と言ったり、「運は実力によってカバーできる」といった言説があります。

いくつか本を読んで自分の中で思ったことをまとめて書こうかなと思います。

イメージ

何かしら成功する際に、自分の実力と運が両方必要になるケースがあります。例えばプロのソフトウェアエンジニアになるとして、そもそも実力が足りなかったらなれませんし、一定以上の規模の会社で働こうと思った場合は面接があったりするので、面接官との相性だったり、たまたま出てきたコーディング試験が得意な問題だったといったような事があるでしょう。他にもプロ野球選手になるというケースでも実力はもちろん、たまたまスカウトが見に来た時に運良く活躍するケースもあるでしょう。こういう人のことを「持ってる人」と言ったりしますよね。これは「特別な運を持ってる人」という意味で使われるケースが多い気がしています。

つまり何かしら成功する時に「運」と「実力」の両方が必要だという感覚はほとんどの人が持っていると思います。

運と実力
運と実力

ここで実力というのが足りない時に実力を伸ばし、目標に向かっていくのが「努力」と呼ばれる行動です。 逆に「運」というのはこちらが調整できないモノを指している事が多いです。

努力のイメージ
努力のイメージ

そういう「運」と「実力」を対比した上で、上のイメージで言えば、運の要素に頼らないくらいの実力を身につければ関係ない、という話が「運は実力に寄ってカバーできる」という言説です。

運と実力のイメージは正しいかどうか

さて、実力と運という対比でものを見ていると上のイメージは理解しやすいのですが、実力と努力と運というのはそう簡単なイメージで片付けられるものでしょうか。 最初から実力を強く身につけている人は少なく、何かしら努力して手に入れてきていると思います。ただ努力するという機会に恵まれているという事は、そもそも「こちらが調整できないモノ」である事が多いと思います。家庭環境が裕福だった事によって私立受験する人は多いと思いますが、家庭環境が裕福という事が既に大きいアドバンテージになっています。これは最初から調整できるものではありません。同じようにソフトウェアエンジニアに多い、「家に最初からパソコンがあった人」というのもそういう機会に恵まれた人だと思っています。

つまり、努力と実力と運といった時に結局努力できる機会も「運」によってもたらされているものなのです。

実力部分も運から来ている
実力部分も運から来ている

そういう事を教えてくれた本が、「実力も運のうち、能力主義は正義か」という本です。

www.amazon.co.jp

この本の中では努力を誇ることを「エリートの傲慢」と痛烈に批判します。著者は現代社会の能力偏重な状況を良いと思っていないわけです。本には「そういった能力偏重主義は不平等さを加速させる」という話が出てきます。ただ間違っちゃいけないのは「努力を誇ること」を批判しているのであって「努力そのもの」を批判しているわけではないです。

なんで努力を誇ってはいけないのか

「努力を誇る」というのは成功者の特権のように扱われています。努力したこと自身が大変だったということは理解しますが、実際には努力している影で支えている人たちが居ます。また努力できる機会にすら恵まれなかった人たちも居ます。上述した本の中には「そういったエリートの傲慢さが分断を産み出している」という非難をしています、これが昨今のコロナのワクチンクーデターや brexit 、トランプ政権誕生といった話につなげています。

この本の中で著者は「社会的に評価される仕事の能力を身に着けて発揮すること」は否定していませんし、むしろ推奨しています。一方で、「努力を称えて努力した人としなかった人に優劣をつけること」は否定されています。

人はスタート地点からして不平等であり、その中で実力をつけながら努力することはどうあれ必要です。ただ努力を称えることは能力主義に繋がります。能力主義は「不平等の存在はもはや是正できないため、不平等を正当化する」事で成長を刺激します。ただこれが行き過ぎてしまうと不平等による格差が生まれ、分断が生まれてしまう事を著者は危惧しており、それを社会的に変えていかなければいけないという話に展開しています。

この本自体は割とそういった社会に対してのテーマを語っているため、個人個人でやれることは少ないのですが、一つあるとすると「努力をしたことを誇る」という事をやめて、「周囲の人達へのサポートに感謝し、運が良かっただけである」という自覚をする事ができることなんじゃないかと思いました。

運の利益率

もう一つ本を紹介します。ビジョナリー・カンパニーZEROという本の中に、企業や経営者がどうやって成長していくかという話があります。

www.amazon.co.jp

この本の中で「運の利益率」という話が登場します。「偉大な企業の中で多かれ少なかれ運に恵まれた企業はあったものの、 "運だけ" で成長したという企業は存在しなかった」という話が出てきます。言い換えると「運という要素をどう活かしたか」が経営ひいては企業の成長に欠かせない要素であることを挙げています。

この事を運の利益率という言葉で語っていました。これは個人においても同じことが言えるのではないでしょうか。要は恵まれた運をどうやって活かすか、運の利益率を挙げていくには結局努力は必要なのです。 これらの本は努力を否定しているのではなく、むしろ推奨していて、ただし分断を産まないようにしないといけないのです。

まとめ

運か実力かといった議論が若干的を外した感じでお互い議論しているのを見てきました。言いたいことは結局シンプルで、努力はしないといけないけど、成功したとしても運に恵まれたことを感謝するべきという話ですね。 昨今の親ガチャなどにも通じる話かもしれません。親が良かった・悪かったというのが運の要素によって左右されていることをガチャというソーシャルゲームの要素で表した皮肉な意見ですが、分断が産まれている現状においてはこういった話も大いに有り得そうに思えます。

自分は「努力は努力できる機会に恵まれた人の権利であり、最大限に利用できる範囲で利用する。そのおかげで成功したとしても自分の努力のおかげとはみなさず、運の要素によってもたらされたものである」という考え方を持っていこうと思っています。

この一年やったこと、継続していること (Rust とか 英語とか)

前回エントリを踏襲し、さらに一年間どんな事をやったか、という話を書こうかなと。

yosuke-furukawa.hatenablog.com

一年間やったことを振り返ると、英語と競技プログラミング、その過程で Rust をやっていました。

競技プログラミング

LeetCode

この一年間でトータルで解いた問題が806問になりました。これまで JavaScript で解いてたやつを Rust で解き直したりしてたので、解答数はそこまで増えてないのですが、 Rust の勉強と割り切っていたので、目標としては良かったかなと思います。

github.com

LeetCode の戦績
LeetCode の戦績

一年間ずっとやったことで、金バッジをもらいました。

https://assets.leetcode.com/static_assets/others/2021-annual-badge.gif
LeetCode Annual Badge

ただ競技プログラマーとしてなにか成長したかと言うと、前回よりは解けるものの、「解けない問題が解けるようになった」というより「安定して解ける問題が解けるようになった」っていう感じですね。Rust を覚えたりしたものの、解けない問題は相変わらず言語を変えようと解けないですね。 ただ Rust は JS と比較すると既に BinaryHeap の実装があったり、 binary search を実装したものがあったりするので、この関数使っちゃえばおしまい、みたいな利点がありますね。

Rust を覚えるためにやったこと

exercism.io っていうところで問題を50問くらい解きました。

このツイートに書いたとおりなんですが、 exercism は問題を解くだけじゃなくて、解いた後メンターが見てくれます。その上で、性能が遅いとか、もっと match 式を使えとか、こういう関数あるからこれでやれとか教えてくれます。 approve がもらえると嬉しいです。無くても解ければ次の問題にいけます。 main track と sub track があって、 main track は最後に小さい Stack ベースの言語書かされます。めっちゃ楽しい。

後は地道に LeetCode 解いたり、プログラミング Rust を読んだりしていました。第2版がそろそろ出るということなので、買い直そうと思います。

Advent Of Code 2021

今年も完走しました。前回エントリの通りなので詳細はそちらで。

yosuke-furukawa.hatenablog.com

英語

今年は DMM 英会話と途中から ELSA Speak に手を出してやり始めました。

DMM 英会話

20000分以上受けたので、マスターレベルになりました。

DMM 英会話マスターレベル
DMM 英会話マスターレベル

この一個上にレジェンドレベルというのがあるのですが、 30000 分以上受けないといけないです。一旦そこまで目指してやってみようかなーと思っています。レジェンド以上は今のところ無いみたいですね。 英会話は勉強って言うより楽しく話せる機会としてやっていて、特にニュースを読んで議論するのが楽しいですね。先日はサービス残業を当たり前のようにする人を「Presenteeism」と呼ぶらしいのですが、 Presenteeism の人は日本に多いのか、クリエイターの人はなぜ Presenteeism になりやすいのかについて議論しました。また他の日にはアフガニスタン情勢がアメリカ軍が引き上げたことで変わったことについて議論しました。だいぶセンシティブな話題を議論しています。

ELSA Speak

ELSA Speak は発音矯正アプリなのですが、これまた楽しくてやってます。自分で実際に発音して、それがネイティブにどれだけ近いかどうかで点数を測ってくれます。アメリカ英語の発音とイギリス英語の発音もその他の英語の発音にも対応していて、それらのどれに近いかを認識して、近い方で点数を出してくれます。

ネイティブに近ければ近いほど点数が高いのですが、今の所発音は 95% まで来ました。

ただ実は自分がネイティブの発音に近いからこの点数というわけではないんです。何度も何度も繰り返して良い点数が出るまでやってるので、この点数なのです。とにかく反復して何度も発音して、コツを掴んでもう一回やって、、、というのの繰り返しですね。発音した後88%以上だと合格みたいな独自ルールでやっていると割と高くなりました。 やり続けているおかげか、まだまだ発音も完璧ではないですが、少しずつネイティブっぽい発音に近づけています。

読書

今年は途中で割と読書も心がけてました。というのも DMM 英会話で雑談するにしても知識がある程度共有されていないと有効な話し合いにならないのと、家事の合間に軽くサクッとインプットできる趣味を考えていたら読書が最強だったという事、後は最近やることが増えてきて、自分の知識が単純に足りないなと思ったことがきっかけでした(特に経営面)。

大体毎月1, 2冊は読んでたと思います。

こんな感じで幅広く読んでました。ただもう少し思ったのはちゃんと読んだ後、 Twitter で放流するだけじゃなくて、ブログとかでまとめておかないと忘れてしまうなと思ったので、ちょっとどっかでまとめようかなと思います。

数学

数学のインプットはむしろ減ってしまったかもしれません。というのも、朝に英語やる事で数学を解く時間が減ってしまったことが原因ですね。まぁこの辺はなにか別なことにリソースを割いたら、別なことが犠牲になっているだけなので、どっかで濃度を調整すればまたやれるようになるかなと思います。一応毎日購読してる数学の Youtuber のチャンネルは見れるときに見ています。

www.youtube.com

www.youtube.com

さて次どうしよう

去年やるという目標を立てていた Rust の勉強はできたのですが、 Rust で何か面白いアプリを作ったり、ライブラリを作っているわけではないので、もうちょい実用的なものを作ってみようかなと思います。フロントエンドエンジニアにも Rust が必要になるケースが増えてきており、なんかその辺りでできることないかなーと思っています。英語に関しては一旦スラスラとは話せないものの、ある程度まで話せるようになってきているので、後は繰り返しなのかなーと思ってますが、実は文法とかあやふやだったり、語彙ももう少し身に着けないとなと思っています。話すこと、聞くことはできても、読むこと書くことが苦手になりつつあるので、もう少しちゃんとできるようになりたいですね。

読書や数学と言った新しいインプットも継続的に行いつつ、来年はアウトプット方面をもう少し頑張りたいです。

Advent Of Code 2021 完答した

Advent Of Code 2021 に参加して、今年も毎日コード書ききりました。。。大変だった。。。

Advent Of Code 2021
Advent Of Code 2021

Advent Of Code とは

adventofcode.com

Advent Calendar 形式で一日ずつ問題が出てくるので、それを毎日ひたすら25日まで解かせるというやつです。去年もやったんですけど、ただただ大変で、後半は問題出てきたとしても気持ちが折れかけたりします。ただ解けると楽しいんで、やってしまうという中毒性のある遊びですね。

ちなみに去年のやつです。

yosuke-furukawa.hatenablog.com

今年こそは全問何も見ずにちゃんと自分で考えて解くぞと思ってたんですけど、 day23 で気持ちが折れてしまって、解答を見てしまいました。自分との勝負ではあるものの、年末くらいはゆっくりしたいっていう気持ちに勝てませんでした。。。

今回は Rust で参戦

去年は JS だったんですけど、 Rust 勉強するかーって言って一年間 Rust で LeetCode やったり、 exercism で勉強したんで、集大成として Rust で参戦しました。

github.com

まだまだ Rust っぽい書き方ができるようになったとは言えないんですが、普通にコードは書けるようになりました。もうちょい実用的な処理を書いてみたいですね。来年はなんかライブラリとかアプリとか作ってみようかな。

思い出に残っている問題

思い出に残った問題を上げるなら以下の通り。ネタバレがあるので、これから自分で解きたい人は見ないでください。

Day 4: Giant Squid

ビンゴゲームをイカと一緒に解くっていう問題。数字が記載されているので交互にビンゴを完成させていって、勝ったらスコアが加算されるので、総スコアを答えよ、という問題。意味不明なお題で面白かったです。イカゲームと掛けたのかなこれは。。。

Day 13: Transparent Origami

折り紙を一定の座標軸で折り曲げていって、最後に出てきた画像に書いてあるアルファベットを答えさせる問題。これまで見たこと無い感じの面白い問題でしたね。

Day 14: Extended Polymerization

拡張された重合反応?っていう日本語訳になるかもだけど、要は化学元素記号みたいなものが反応されることで別な記号に生まれ変わる、その記号が反応し合うことで最終的にどういう文字列になるかという問題。

バカ正直に最初解いてて、途中から短く解けるアイデアを使ったら一発で解けてよかった。

Day 15: Chiton

よくあるスタートからエンドまでの迷路を一番効率よく辿れるパスを当てるっていう問題。 A* みたいなアルゴリズムで解こうか迷ったけど、結局 BinaryHeap を使った優先度キューみたいなもので解いてしまった。

この辺りまでは楽しいんですよね。前半の15日目くらいまでは。。。(ずっと前半やってたい)。

Day 16: Packet Decoder

なんでかよくわからないけど、エルフが出てきてバイナリーで話し始めたという所から独自のバイナリをデコードさせるっていう問題。とにかく仕様が複雑で、パケットの中にもサブパケットと呼ばれる入れ子構造になったパケットをデコードさせるので、再帰呼び出しで解く必要があり、また複雑なことにいくつかパケットのタイプが分かれてて、特別な処理が必要なパケットもあったりします。最後に leading zero で zero 値埋めがされているところとかも現実っぽい感じになっていて、とにかく「めんどくさい」問題でした。後半戦にはいったな、っていうことを思わせる問題でしたね。

Day 19: Beacon Scanner

三次元座標上にあるビーコンがあり、センサーがそれを検知するんですが、センサーの向きは教えてもらえず、ビーコンが見つかった三次元座標は渡されるものの、どの向きで発見した座標なのかはわからない、という問題です。書いただけじゃ何言ってるかわからないですね。三次元座標の原点の向きがバラバラになっているところで、すべてのセンサーが発見したビーコンの数を当てよという問題。頭の中でずっと座標をぐるぐる回転させてて、それでも全くどういうパターンが有るのかは分からず、色々座標を書いてパターンを見つけ、、、というほんと大変な問題だった。。。

Day 22: Reactor Reboot

原子炉を再起動させるために特定のパターンでボタンを押さないといけないが、こちらも三次元平面上にあるボタンをオン・オフして最後にオンになっているボタンの数を数えるという問題。書いただけじゃよくわからない問題パート2ですね。問題自体はめちゃくちゃ大変というわけではないものの、アルゴリズム力が求められるような問題でした。

Day 23: Amphipod

ここで一回力が尽きます。 Day 23 も問題自体はシンプルです。いくつかの微生物がいて、そいつらがパズルのように動くので、動いたコストの最小値を求めよ、という問題です。パズルを解ければ OK なんですが、もう day 22 で一回力尽きてたので、やる気がどうしても起きず、一回答えを見た上で解きました。気が向いたら自分でも解いてみます。

Day 24: Arithmetic Logic Unit

僕一番好きなやつですね。 Arithmetic Logic Unit というアセンブリ言語のような言語があるので、仕様どおりに計算を進めた上で、 4 つの processing unit がどのような値になるかをシミュレートするやつです。入力から出力を出すのではなく、出力から入力を推測しろという問題なので、全パターンを試そうとすると 9 ^ 14 の入力パターンを試す必要があり、そのままやると終わらないです。

自分はキャッシュを使って簡単なやり方で解いてしまったのですが、終わった後に色々見たら、 アセンブリ言語JIT を作った猛者がいたりと、めちゃくちゃクリエイティビティなやり方で解いてる人が居て面白かったです。

https://github.com/cemeyer/advent-of-code-2021/blob/master/day24.rs#L164-L304

まとめ

去年 すぎゃーんに言われて始めた Advent Of Code だったけど、今年も継続してやることができました。しかも去年やるって言ってた Rust を学んだ上でそれで挑戦し、解くことができたのは、嬉しかったです。

来年は全部ちゃんと解けるようになりたいなーと思いつつも、健康や家族を優先にし、その中でやれる範囲でやれるようにしたいと思います。

Node.js の assert の小話

Node.js Advent Calendar の4日目の記事 です。

 Node.js の assert は結構歴史が深いです。あまり直接使ってる人は少ないかもしれません。使うとしたら test で使ったりするケースでしょうか。 それも最近は jest に生えてる便利ライブラリを使うほうが多いのかもしれないですね。 unassert なんかで開発中に埋め込んでいるケースもあるかもしれません。このようにたまに使うこともあると思うので、覚えておくと良いでしょう。

assert には 4 年ほど前から strict assertion mode というのが追加されています。

nodejs.org

今日はそんな小話を。

require("assert") は直接使ってはいけなかった。

もう昔の話ですが、 require("assert") が deprecated になっていた時期がありました。知らない人も多いんじゃないかと思います。 なんで deprecated だったかというと、 一部の関数が意図しない動きをしていたからです。

const assert = require("assert");
assert.deepEqual(/a/gi, new Date()); // 例外が上がらない (v9.0 時点での話。v10.0 以降だと例外が上がる)
assert.deepEqual('+00000000', false); // 例外が上がらない (最新でも例外が上がらない)

deepEqual は中身のプロパティが Loosely に同じかどうかを見ているだけなので、 == とかと同じく、 厳密な型チェックの同一性を確認しない働きをします。

この他にも assert.fail がよくわからない動きをします。

assert.fail("should not be reached") // Throw AssertionError should not be reached

assert.fail は fail するという関数です。本来的な使い方で言うと、 test でここに来ちゃったら fail ですよっていうときにメッセージを出す目的で使います。

ただ2つの引数を渡すと第1引数を actual 、第2引数をexpected として Error にメッセージを詰めて throw してくれます(そんな機能いらない)。

assert.fail("a", "b") // Throw AssertionError a != b
assert.fail("a", "a") // Throw AssertionError a != a

ちなみに assert.fail に引数を2つ以上渡すことは 今でも deprecated になっています。

https://nodejs.org/dist/latest-v17.x/docs/api/assert.html#assertfailactual-expected-message-operator-stackstartfn

そんなこんなで、いくつかの関数が意図しない動きをするので昔は deprecated でした。assert モジュールを使うことは 今は deprecated ではありません。

strict method が出てくる。

この状況を改善するべく、 strictEqual と deepStrictEqual というメソッドが出てきます。これは前述した型が一致しているかまで見る assert method になっています。普通にこっちを使うほうが良いです。

assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]); // OK 3 と '3' は == では一致するため

assert.deepStrictEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]); // Throw Assert Error

ただ細かい注意点としては、 deepStrictEqual は厳密等価演算子である === と同じではありません。 どちらかというと、 同値かどうかを表しています。 ECMAScript 的に言うと Same Value かどうかなので、 Object.is() の deep 比較と似ています。 そのため、 NaN 同士の比較も true になります。

assert.deepStrictEqual(NaN, NaN); // OK NaN と NaN は === で違うが、 Object.is() では true のため。
assert.deepStrictEqual(+0, -0); // Throw Assert Error +0 と -0 は Object.is() では false のため。

(それじゃあ deepSameValue のが良いのではないか?と思って、一応 issue で提案したんですが、まぁ微妙な議論になりそうだったので、引き下がりました。)

strict assertion mode が出てくる。

こんな感じで deepStrictEqual が出てきてメデタシメデタシではあったのですが、 deepStrictEqual というメソッドが微妙に長くて使いにくいです。 あと、 strictDeepEqual だっけ? deepStrictEqual だっけ?ってどっちがどっちかわからなくなったりします。そもそも deepEqual 微妙だよねっていう話もあり、最近では "assert/strict" の方を使って、 deepEqual でも deepStrictEqual と同じ動きになるように変更されています。

import assert from "node:assert/strict";

assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]);

+ actual - expected ... Lines skipped

  [
    [
      [
        1,
        2,
+       3
-       '3'
      ]
...
    4,
    5
  ]
    at file:///private/tmp/test/strict.mjs:3:8
    at ModuleJob.run (node:internal/modules/esm/module_job:195:25)
    at async Promise.all (index 0)
    at async ESMLoader.import (node:internal/modules/esm/loader:337:24)
    at async loadESM (node:internal/process/esm_loader:88:5)
    at async handleMainPromise (node:internal/modules/run_main:65:12) {
  generatedMessage: true,
  code: 'ERR_ASSERTION',
  actual: [ [ [ 1, 2, 3 ] ], 4, 5 ],
  expected: [ [ [ 1, 2, '3' ] ], 4, 5 ],
  operator: 'deepStrictEqual'
}

まぁ正直どっち使っても良いんですが、 assert/strict にして deepEqual つかうか、 assert にして deepStrictEqual 使うかという選択肢が増えています。

まとめ

assert に strict assertion mode ができたよっていう話でした。

Node.js コアモジュールの import/require には `node` schemeがつけられる

Node.js アドベントカレンダーの 3 日目の記事です。空きを埋める形で始めました。

qiita.com

www.codegrid.net

CodeGrid でも書かせていただきましたが、 Node.js で ES Module / CommonJS を使ってコアライブラリのロードをする際、 node から始まる scheme を付けることが可能になっています。

nodejs.org

// ESM
import fs from "node:fs/promises";
// CJS
const http = require("node:http");

これにはいくつかのメリットがあります。基本的につけておくことが望ましいです。 今回はメリットをいくつか紹介します。まだこれがデファクト・スタンダードになっている訳ではありませんが、これから付けてもらうように推奨していきたいと思います。

メリット1: Node.js コアモジュールであることが明示される

Node.js のコードを始めてみたときに、このモジュールが Node.js コアから提供されているのかそれとも npm から提供されているのかイマイチよく分からなかった経験ありませんか。 querystring というモジュールがコアからも npm から 3rd party library としても提供されているので、昔誤って npm install querystring してしまうこともありました。これをインストールしたとしてもコアモジュールのほうがロードされるので、余計なものが入ってしまうだけではあるのですが、なるべく避けたい事態ですね。

今回からは node:querystring で初めておくことで、コアモジュールからのロードであることが明示されます。意図しないダウンロードも防げるでしょう。

メリット2: Node.js の将来のコアライブラリが既存のライブラリと被ることを避けられる

http2 モジュールを作るときに最初 require("https").http2 のような形で提供するかどうかを議論になったのですが、これはもともと http2 モジュールが取られていたからで、メジャーバージョンアップで提供する際に require("http2") にする形に落ち着きました。

こういうことは将来的にも起き得る可能性があります。つまり、 Node.js コアモジュールとそれ以外との名前が将来的にかぶってしまうと面倒なことになります。 http2 のときはまだそこまで流行っていないライブラリでしたが、今後流行っているライブラリで起きた場合はエコシステムに影響が生まれます。

そういう事も考慮して node scheme を付けておくことで、 3rd party ライブラリとの差別化を最初から図れるメリットがあります。

細かいところ

これ以外にも細かい所としては、 ES Modules は本来 import 文にかけるのは URL を書く仕様になっています。この node: から始まることで URL valid な文字列をちゃんと記述することができるようになります。今の書き方は Node.js 独自の拡張です。(ただこの独自拡張も import maps などの新しい仕様により、仕様側が Node.js 独自拡張もカバーする形になるかもしれませんが)

さらに細かいところ

require 構文の中で node scheme を付けた場合、 require.cache で中身を差し替えるハック は使えなくなります。これを使って色々 Node.js のコアモジュールを差し替えているモジュールがあった場合、うまく動かなくなる可能性があります。 ES Modules の場合は require.cache を使っておらず、そもそも改変はできないようになっています。

// このように書いた所で、 node:http モジュールは変更できない。
require.cache[require.resolve('node:http')] = function() { console.log("http modified") };

まとめ

node scheme はつけていこう。