例外を初めて実装した言語

リクルートアドベントカレンダー20日目の記事です。

adventar.org

例外を初めて実装した言語ってなんですかね?

最初にこの疑問を思ったのは、今も忘れない R-ISUCON 2021 というリクルートの社内ISUCONの運営で炎上していた時の話です。 ちなみに R-ISUCON 2021 は劇的な結果で終わっているので、興味のある方は見てみてください。

blog.recruit.co.jp

R-ISUCON 2021 では、 Node.js (TypeScript), Go, Java の3パターンの実装が出てくることが通例になっていまして、今回は Java の実装から Node.js, Go に適用していた時に一緒に実装していたメンバーからの疑問が『例外には色々な議論があるけれど、「例外を初めて実装した言語」ってどういう気持ちで実装したんだろう』という話が挙げられたので、そのネタを持ってきました。

ちなみにここで指している例外というのは、値を return した時の value が既定値以外かどうかでチェックする方法ではなく、専用の機構として例外という考え方を実装したのはどういう言語からで、どのくらいのタイミングからだろうかというのが気になったので調べてみた感じです。 return と if で既定値以外かどうかをチェックする仕組みも例外ハンドリングの一種だと思いますが、そうではなく、専用の仕組みとして例外を実装した言語は何なのかが知りたくなって聞いてみた感じです。

竹迫さんからのコメント:

takesako さんコメント
takesako さんコメント

koichikさんからのコメント:

koichikさんからのコメント
koichikさんからのコメント

実際には諸説あったので、いくつか紹介します。ただおそらく一番古いのは LISP であるという事になりそうです。

StackOverFlow からは PL/I

StackOverFlow からのコメントでは、 PL/I と CLU が挙げられていました。PL/I が 1964 年頃、 CLU が 1973 年頃に実装されていたものと言うことで、だいぶ昔の話ですね。

stackoverflow.com

PL/I は専用の機構として、 try catch 構文で表すものではなく、 SIGNAL という信号とそれを受け取る ON という専用の命令を持っていて、それを使った形で実装されているみたいです。なんか JavaScript のイベント駆動なエラー処理の方法と全く同じですね。

SIGNAL ERROR;

ON ERROR BEGIN;
.
.
END;

実際にはこの辺りに記述があります。

web.archive.org

ON-condition
An occurrence within a PL/I program of a condition that could cause a program interrupt, such as division by zero.

ON条件は PL/I のプログラム内で問題が発生した時にプログラムに割り込む事が可能になる。問題というのは例えば 0 で割った場合などを指す。

という記述がありますね。

例外的な状況に陥った時に割り込み命令のような形でエラーを出し、それによって緊急ハッチのように大域脱出な動きをするのが初期の例外という感じですね。こうなっちゃったらもう潔く死んでしまうという判断をするんでしょうけど、死ぬ前にログを書くなどの処理をしていたのでしょう。

CLU も例外ハンドリングができるようになっていますが、こちらの場合は Java で言うところの検査例外のように例外を補足する時に任意の例外を選んで補足するという when 句を使った書き方が可能になっています。

stack$pop(foo(mystack))
    except
    when empty:
        % handler code
        stream$putl(stderr, "popped empty stack")
    when foo_ex(i: int)
        stream$putl(stderr, "foo exception: " || int$unparse(i))
    when bar, baz (*):
        % ignore exception results, if any, of these
        % bar and baz may have different number and types of exception results
    others:
         % all other exceptions handled here but results are lost
    end
 % flow continues here

なんとなく、こちらの方が後発っぽいですが、最近の流れに親しきものを感じますね。

英語版 wikipedia からは LISP

en.wikipedia.org

Software exception handling developed in Lisp in the 1960s and 1970s.
 This originated in LISP 1.5 (1962), 
where exceptions were caught by the ERRSET keyword, 
which returned NIL in case of an error

1960年代から70年代にかけて、例外処理が開発されてきた。
これはLISP 1.5 (1962年から) が起源であり、
エラーの時に NIL を return する代わりに
 ERRSET キーワードによって例外をキャッチするものとして登場した。

こちらのほうが正確そうですね。 LISP が 1962 年から ERRSET なる機構を用意し、それが return ではない形で処理するための専用の機構ということです。これにはちゃんと続きがあります。

Error raising was introduced in MacLisp in the late 1960s via the ERR keyword. 
This was rapidly used not only for error raising,
 but for non-local control flow, and thus was augmented by two new keywords, 
CATCH and THROW (MacLisp June 1972), reserving ERRSET and ERR for error handling.

エラーを上げることは MacLisp 内に ERR キーワードを使って 1960 年代後半に導入されました。これは急速に利用用途が広がり、
いわゆるエラーを上げるという事だけではなく、ローカルの制御フローにも使われました。これにより2つの新しいキーワードが導入されます。 
CATCH と THROW (1972年) をローカルの制御フロー用のものとして使い、 ERRSET と ERR はそのままエラーを上げる用途として残りました。

おお、、、となると、最初は例外(エラー)を上げることと、 ローカル制御フロー用の例外的な状況、 所謂 Java で言う検査例外的なものはそもそも制御構文からして違ったわけですね。

ちなみに PL/I の SIGNAL / ON の構文は所謂エラーにも検査例外にも使われていたようです。ただ英語版 wikipedia には このような使い方をするのは現代では一般的ではない と言われてますね(JavaScript ...) 。

例外が発明された後

LISP が発明したものが MacLisp に専用構文として使われ、その後 C++ がこの機構を try catch throw を使った形で導入します。 C++ は例外処理の中で利用したリソースを解放するという目的でも利用されます。メモリの解放や open したファイルのクローズ等が必要になるため、大域的に脱出して終わりにするのではなく、 catch に入ってから後始末をやることがあります。ちなみに C++ にはデストラクタと呼ばれるオブジェクトの終了時に呼び出すメソッドがあり、 RAII (Resource Acquisition Is Initialization) という考え方も早くから使われていたのでfinally構文がなくてもリソース解放はできていました。また、 Java には finally 句があったり、 try-with-resource 構文があります。

Java は検査例外という堅牢(だけど、割と面倒な)仕組みを作り、例外処理をきちんとハンドリングさせようとしますが、 C# などの Java の影響を受けた言語にはそれが引き継がれませんでした。 Java の検査例外に対する批判は「結局例外処理を強制させようとしても、殆どのプログラマーが無視したり、そのまま投げたりしてるだけ」という話に繋がります。

この後は Go が多値の return で表現しつつ、エラーは panic で表現するなど、原点回帰していく流れをみせており、今日の流れにつながっています。

最後に

ひょんな雑談からこんな話に繋がりました。みなさんもぜひ一度は英語版の例外処理の項目を見ることをおすすめします。ちなみに Vue.js の話や React の話なんかも少しだけ書いてあります。

en.wikipedia.org

Node.js や deno に Web Standard な API をなんでも取り入れるのが良いことなのかについて

この記事は Node.js Advent Calendar の 11 日目の記事です。

qiita.com

Web API と Node.js

ES2015 以前の Node.js は Web Standard な API の中で足りないものを自分で補う形で進化を続けてきた。 Callback や Event 主体での非同期処理や Common JS な形でロードできる独自のモジュールの仕組みがその筆頭だと思う。ただ逆に Web Standard な API が流行ると今度はそれに追従していかないといけなくなってきた。 ES2015 以後に流行ったものといえば、 Promise 主体での非同期処理であり、 async-await での処理だと思う。また、 ES Modules の台頭もあり、今日では Node.js でも普通に呼び出すことが可能になった。

今ではどちらも Node.js で普通に使える。エコシステムを壊さないようにした結果、 Node.js の ES Modules が普通に使えるようになるには時間がかかったが、いずれにせよ今は使えている。

TC39 だけが Web Standard なグループではない。 WHATWG や WICG 、 W3C などのグループもそれぞれ存在し、それぞれが Web Standard な API を作っている。これらを後追いで Node.js は使えるようにしてきた。 Event Target API, Text Encode / Decode, WHATWG URL, Web Stream, Web Crypto, AbortController などなど、足りないパーツを補う形で作られている。

deno は最初から Web Standard な API をベースに設計されており、割と Node.js よりも既存ブラウザに存在する機能を積極的に持ってきている方だと言える。後発なだけあって、エコシステムに配慮する必要がない分迅速に対応ができている。

Node.js / deno が Web Standard API に追従する状況は現在でも続いている。ただし、最近は若干やりすぎなのではないかというか、本当に必要なのか?と思うようなものまで入っているし、検討されている気がする。

自分の立場を明確にしておくと、「新しい Web API に追従することは良いことだと思うが、不要な API にまで追従する必要はないし、無理矢理ブラウザと同じ API にする必要もない」という立場だ。

新しい Web API が必要か不要かにはいくつかの観点があると思う。筆者は以下のように観点を感じている。

  • 全てのブラウザでコンセンサスが取れていること
  • Node.js / deno の利用者が呼び出した時にどうなるのかが既存の API と比較してイメージしやすいこと
  • 新しい機能が入ることでセキュリティホールが生まれにくいこと

この観点でいくつか考えてみようと思う。

atob / btoa が Node.js / deno に入った。

atob と btoa は 文字列を encode して base64 にしたり、 decode して元に戻す時に使う、binary から ascii (base64) に変換 (btoa) し、 ascii (base64) から binary に戻せる (atob) という API だ。ブラウザでは昔から実装されている。

developer.mozilla.org

ただし、この文字列は名前の通り ascii (base64) にだけしか適用できない。 btoa が binary to ascii (base64) の略語だと知っていれば binary (latin1) な文字列にしか使えないことはわかるが、一方で日本人のように latin1 以外の表現を文字列としてナチュラルに使っているところもあると思う。特に任意の文字列を base64 に変換する API だと誤解して使っていると不用意なバグを埋め込む可能性もある。 encodeURIComponent などで無理矢理日本語を binary (latin1) に変換してから使えば一応使えるが、イディオム的で直感的ではない。

> atob(btoa("こんにちは"))

Uncaught:
DOMException [InvalidCharacterError]: Invalid character

> decodeURIComponent(atob(btoa(encodeURIComponent("こんにちは"))))

'こんにちは'

すでに Node.js にも deno にも実装されている。しかし Node.js は実装した瞬間にこれは使うべきではないと実装者から言われている。

Node.js には Buffer API があるので、これを使わなくても、base64 に変換するような処理は表現することは可能。

const str = "こんにちは";
const base64 = Buffer.from(str, "utf-8").toString("base64");
console.log(Buffer.from(base64, "base64").toString("utf-8"));

こっちのほうが長いので、一見難しそうに見えるかもしれないが、やっていることは simple で utf-8 から base64 に変換している処理であることは掴みやすい。 btoa のような4文字で表現されているAPIは easy な API ではあるものの、ぱっと見てこれが binary to ascii の略で base64 に変換してくれる API だと調べないで分かる人は少ないのではないかと思う。

上述した観点でいうと、「既存のAPIと比較してイメージしにくい」という点と「知らないで使ってしまった時に不用意なバグを埋め込むのではないか」という点でそもそも Node.js には入れなくても良かった API だと思っている

じゃあそもそもなんで使ってほしくない API を Node.js が実装したのか、というと、Web Standard API に合わせるというコアチーム全体の合意ともう一つが atob / btoa を polyfill して作られているライブラリの存在、最後に deno が既に実装しているという競合からの後押しの3つの理由で実装しているのではないかと推測している。

自分の立場で言えば入れなくても良かった API だと思っているものの、多少複雑な思いもある。 Buffer が Node.js に詳しい人は分かっていたとしても、ブラウザ側のフロントエンドエンジニアにとっては atobbtoa の方が身近な存在である可能性はある。 Node.js のユーザーがそちらに傾きつつある現状においてはその方が良いという意見もわからなくはない。一方でブラウザの歴史的なレガシー API をそのまま持ってくることが本当に良いことなのかは慎重に検討したほうが良い気がする。 特に簡単(Easy) な API というのは難しい。誰かにとっては簡単でも、誰かにとっては不便なものだからだ。今回の例で言えば、「atob/btoaを知っているブラウザのフロントエンドエンジニアにとっては簡単」だけど、知らないエンジニアにとっては一見わかりにくい Bad Parts 的なものであり、これ以上増えていくことは避けるべきではないかと思っている。

だからこそ、コアチームの中にも矛盾した思いがあり、「新しく実装したけどなるべく使わないでくれ」というメッセージを出している。

File system access API

Web を構成する要素として一番難しいブロックの一つにファイルの取り扱いがある。この APIファイルシステムにアクセスできる API をブラウザに実装しようというものだ。

wicg.github.io

Node.js はまだ検討中で、実装するようなフェーズに入っていない。とはいえ、アイデアとしては検討はしているようだ。

github.com

deno は検討中で、 draft PR は出されている。

github.com

github.com

この API はブラウザ間のコンセンサスがまだ取れていない。

mozilla.github.io

ブラウザ間のコンセンサスが取れてない状況で実装したとしても変わる可能性は大いに有り得るし、最終的に実装されなかった場合には誰も得しない API になってしまう。 まだマージするフェーズにどちらも入っていないものの、Web Standard API を採用するとしても、ブラウザのコンセンサスはさすがに取られたものにしてほしい。プラットフォーム側がいち早く Web API を実装しなくとも、コミュニティ側が「使いたい」という意見が出てから実装したとしても遅くないように思う。

そもそもファイルを取り扱うという一番サーバサイドでよくありそうな基本的な処理をクライアントとして使われるブラウザの API に任せるのは難しい気がしている。

fetch

Node.js では未だに議論を重ねている fetch のサポートだが、 deno には既に入っている。ただ fetch もよくよく仕様を読むと deno / Node.js には不要なものも多い。特に CORS 周りの同じドメイン以外にリクエストを送る時の仕様は Cross Origin という概念が存在しないサーバーサイドでは形だけ API として設定できるように使われていて、実行しても何も意味がなかったりする。つまり、 mode: "same-origin" など設定できるものの特に無意味でリクエストは送れてしまう。こういう形だけの API はどこまで正確に模倣するべきなのかは議論が分かれるところだと思う。逆に Cross Origin 相当の設計を今から deno / Node.js に取り入れるのも労力の割にリターンが見合わないし、どういうものになるのか想像がつかない。

Cache をどうやって取り扱うのかも fetch 内にオプションとして設定できる。ブラウザであればブラウザの cache storage を使う際のオプションとして使われるが、 サーバーサイドで fetch した時にはもちろん無視される。というより、サーバサイドで統一された cache storage なんてものはないし、あったとしても実装がメモリ内に保存するのか永続化するのか、するんだとしてどうやって expired データを取り扱うのかといった概念をセキュリティに配慮しながら実装するのも不毛な気がしている。

Cookie とかはさらに頭が痛い問題である。ブラウザで幅広く使われているが、サーバサイドに持ってくるべきかどうかに関しては今もってお互い議論中だ。

なので、 fetch はあくまでも表向きのよく使われそうな仕様だけ実装してあり、ブラウザとの 100% compatible なものを目指すことは deno にせよ Node.js にせよ考えていない。

とはいえ、それでも HTTP をリクエストするという API においては、ブラウザにせよ Node/deno にせよ必要な API であり、どちらも表向きでいいから同じ API が欲しいというのは理解できる。ただし表向き同じ API というのがどこまでを指していっているのかが、Node.js / deno コミュニティ内で深く考えきれていない気がする。単純に似たようなものであれば、 Next.js でも提供されているし、 unfetch などの 3rd party 製のものもある。 Node.js のコアチームは undici と呼ばれる HTTP クライアントを次の HTTP クライアントとして提供している。

※ ちなみにマニアックな話になるが、 deno の fetch は ALPN を使った HTTP/2, HTTP/1.1 のネゴシエーションをしてくれるが、上述した Node.js のライブラリはどれも ALPN でのネゴシエーションはしてくれない。

fetch の中身を見ずにただ「fetchという表向き同じAPI」を指して、 fetch をコアの中に入れようとするとその「表向き同じ実装をどこまで頑張るのか」のコンセンサスを取るのに難しいし、既存のエコシステムを壊さないように入れるのは非常に時間がかかる。

筆者は fetch が一番複雑な思いを抱いている。現状の Node.js の http クライアントはブラウザのクライアントとはノリが違いすぎるので、気軽に call できる新しいクライアントはほしい。一方でブラウザがブラウザのために作った API と同じ API が実装されることは表向き良いとは思うものの、実際使ってみたら無意味な設定や設定しているつもりでも動かない機能が多くなり、結果として Bad Parts になってしまわないかという懸念はある。特に fetch は前述の simple か easy かという議論で言うと、 easy 寄りの API として提案されているように思える。 URL を fetch 関数に渡せば Promise で response が返ってくるという仕様は非常にわかりやすいが、実際には fetch クライアントが中でやっている処理は非常に複雑になっている。ブラウザが実装するものとしてはセキュリティに配慮した形でこのような API になることも理解できる。一方で、サーバサイドで呼び出す時にこの API がマッチしているのかに関しては、まだそこまで検討が進んでいないと思う。

自分が isomorphic だとか universal だとか言ってきた頃より時代が進み、同様なものが実装されるようになってきた。これ自体は良い兆候であると思う。一方でどこまで行っても「表向き同じもの」であって、細部がどこまで表現されているかはドキュメントには書かれていないことが多い。どこまで実装されているのかはもう少しドキュメントに書かれてほしい。

その他

これ以外にも clipboard API とかを実装したり、 navigator にあるようなクライアントの情報が取れる API を実装したりしようとしている issue も見かけたが、サーバサイドで実行された時にセキュリティホールになりそうだと最初に思ってしまった。基本的にブラウザのセキュリティモデルとサーバサイドで動くことが基本のプラットフォームとは同じ感覚で考えすぎるのは良くないと思っている。 deno にはパーミッションで防げる仕組みがあるとはいえ、サーバサイド内で実行されて困るような API を実装するべきではないと思っている。

まとめ

これまでは同じ API が増えることが Web というエコシステムを後押しするように思えていた。一方で、なんでもやりすぎるのはどうなのかと一旦立ち止まって考えるようになってしまった。特に atob/btoa を実装した辺りが個人的に立ち止まって考える切っ掛けになった部分だ。 Web Platform Test のカバレッジが増えることが良いことのようにされ、 mdn 上にある星取表が Yes になることが良いことだと思われているが、一方で、本当になんでも入れるのが良いことなのかについては考えていくようにして、フィードバックしたい。

2020年振り返り

はじめに

yosuke-furukawa.hatenablog.com

今年もちゃんと書きました。

マネジメントとシニアソフトウェアエンジニア

二足のわらじで4年目になりましたね。去年も書いたんですが、メンバーが優秀であるがゆえに二足のわらじができていると思っていて、それを4年目も継続できました。新しく何名か入ったおかげで非常に強力なフロントエンド体制ができてるなと思っています。

adventar.org

recruit-tech.co.jp

recruit-tech.co.jp

recruit-tech.co.jp

上記のブログは技術ブログの方に書いてもらったやつですが、他にも CodeZine@IT 等に記事にしてもらっています。

codezine.jp

www.atmarkit.co.jp

関連会社の技術顧問

また去年から新しくリクルートの関連会社のニジボックスの技術顧問として活動していますが、そこでもエンジニアコミュニティを作って活性化させようとしています。

www.wantedly.com

www.wantedly.com

speakerdeck.com

speakerdeck.com

speakerdeck.com

頼りになるメンバーが自組織の成長だけではなく、関連会社全体にも広げることができたので、これをより成長させていかないとなと思っています。

社内イベント

R-ISUCONを開催、スピードハッカソンも同時期に開催しました。ギリギリ COVID 19 が本格化する前だったのですが、どちらもオンラインではなくやりました。開催できてよかったですが、これから COVID-19 が本格化した影響でイベント系は予定がすべて一旦延期になりました。なので、開催できたのは2つだけです。

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

社内ISUCONも開催3度目で、今回はWebメールがお題でした。これがきっかけ(?)で、本家の ISUCON でも運営になる機会をもらえました。ISUCONについては後述します。

recruit-tech.co.jp

スピードハッカソンも開催しました。

recruit-tech.co.jp

イベント

コロナの影響で早々に JSConf は開催を断念しました。

yosuke-furukawa.hatenablog.com

しかし、 ISUCON は運営側としてオンラインで開催できました。オンライン開催する話も増えてるので来年は JSConf.jp やりたいです。

ISUCON 10 の予選に運営として参加した。

ISUCON 10 の予選を作成しました。予選の問題はある程度コンパクトでありかつチャレンジングな課題を設定できたと思っています。非常に好評だったのでよかったのですが、運営がはじめてということもあり、色んな人に迷惑をかけてしまったな、という反省は反省でありました。

isucon.net

ただ参加して本当に良かったと思っています。 941 さんとも色々話した結果はこちらに掲載されています。

zine.qiita.com

登壇系

AMPFest 2020 に英語で登壇した。

www.youtube.com

AMPFest 2020 に英語で登壇する機会をもらえました。この話をきっかけに英会話を学ぶようになり、英語を再学習するきっかけになったのと、 AMPFest 2020 という大きな舞台でオンラインとは言え話ができて非常に良かったです。

Chrome Advisory Board のメンバーとして LT を実施

docs.google.com

こちらも英語で登壇しました。年に2回も英語で発表するきっかけをもらえてよかったです。

継続して来年もどこかでやりたいですね。2019年のふりかえりで書いたことがこんなに早く叶うとは思ってなかったのですが、きっかけになってめっちゃ良かったです。

DevSumi 2020

デブサミ2020でクッキーの話ししました。ブラウザの中ではプライバシーが非常にホットなトピックでしたね。

speakerdeck.com

FEStudy 2

パフォーマンスチューニングの話をしました。 Web Vitals 周りの話だけだともうたくさんされているので、それをキープするために考えることや対処することをまとめました。

speakerdeck.com

PWA Study

Next.js と AMP のはなししました。 Next.js はほんとに今年かなり大きなプラットフォームになりましたね。

speakerdeck.com

競技プログラミング

全体的にイベントがそこまで多くなかったので、それを逆手に取ってインプットに回ることができました。インプットを全力にやった結果を以下のブログにまとめました。競技プログラミングを割と本格的に入門できました。今年は JavaScript で解いてしまったのですが、来年は Rust とか新しい言語でも挑戦してみます。

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

Node.js

あまり大きな話題は少なかったのですが、いくつかアウトプットしました。

Node.js v14/v15 のまとめ

www.codegrid.net

www.codegrid.net

新機能系

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

英語

ほぼ毎日英会話50分間してます。ただあまり語彙が増えてない気がしているので、ちょっと他のチャレンジも考えてみます。

f:id:yosuke_furukawa:20201231214857p:plain

数学

毎日 Youtube の問題といてました。今でも解いてるので続けます。

もう少しやらなきゃなぁと思ってたけどできなかったこと

今回ベーシックな競技プログラミングや英語や数学やってたらできなかったですね。もう少し来年は上記のところもバランス良くできるように頑張っていきます。

まとめ

今年もマネジメントにプログラミングにイベント開催に登壇にと色々やれた1年でした。非常に充実していたのですが、反省もありました。また来年も自分の反省を乗り越えて色々できるように精進していきます。今年お世話になった皆様ありがとうございました。

Advent of Code 2020 完答した。

memo.sugyan.com

すぎゃーんの宣伝によって参加したけど、「楽しかった!が八割、辛かった!が二割」って感じでした。

Advent of Code とは

adventofcode.com

所謂アルゴリズム系の問題が25日間のアドベントカレンダー形式ででてくるので、ひたすら解く感じのものです。 ランキングとかを狙おうとしなければそこまで厳しいものではなく、既に解答した人が Reddit だったり youtube とかに解法を上げてたりするので、あまりにも厳しい時はそれを見て回答するのもありです。

最初のほうが簡単なので、『あれ?これなら普通に解けるんじゃね?』と思わせておいてからの Day 17 あたりから急に牙を剥いてくる感じがありましたね。。。

前半の問題

入力値を正規表現だったり、 parse したりして、 扱いやすい形に変換したら問題の半分は解けたような問題が多かったです。

Day12 の船を目的地まで操縦させるようなゲームとか。

Day 12 - Advent of Code 2020

Day11 の飛行機の空いてる座席を探させるやつとかも面白かったですね。

Day 11 - Advent of Code 2020

最初の関門は中国の剰余定理を使わないといけない Day13 でしたね。なんとか数学の知識が役に立ってよかったです。

Day 13 - Advent of Code 2020

後半の問題

前半に比べて後半は格段に難しくなりましたね。特に問題文を噛み砕いて理解するまでが時間かかりました。 難しかったのは Day17 でした。問題が読み解けなかったのと、何言ってんだろっていうのを理解しきれず、タイムアップでしたね。

Day 17 - Advent of Code 2020

答えを見てみるとなんのことはなく、最初から例題の読解よりも書かれている事をある程度愚直に実装したら解けそうでした。

Day20 の part2 も歯が立ちませんでしたね。これはジグソーパズルを解かなきゃいけないのですが、解き方はわかってもパズルのピースをぐるぐる回したり、デバッグしてたりしてたら時間が全く足らなくて、諦めました。

Day 20 - Advent of Code 2020

part1 がジグソーパズルそのものを解かなくてもよかったので安心してたら part2 で格段に難しくなったのがビックリしましたね。

個人的には Day19 の part1 が好きでしたね。正規表現に変換すればいいやと思いついた後は楽でした。part2は解法があってたのか自信がないですが、答えを導くことはできました。

Day 19 - Advent of Code 2020

まとめ

AoC は楽しいです。多分来年もやるだろうなーとは思います。次は Rust とかで挑戦してみようかなと。周りで流行ってるし、最近 すぎゃーんが作ってくれた slack workspace があるので、そこで聞きながらやってみようかなーと思います。

Node.js で最近変わりそうな Permission Policy について

さてさて、 25日目の Node.js アドベントカレンダーです。もう年の瀬ですね。振り返りシーズンなんで色々書きたかったんですが、ネタを見つけているうちにこの日になってしまいました。

Permission Policy とは

Node.js に新しく Permission を提供しようという試みです。元々 Node.js では同じプロセス内で動いてしまえば どんなモジュールであろうと同じ権限で色々できますね。外部ネットワークにアクセスしたり、ファイルを読み書きしたり。

プロセスに元から許可されている権限は全てできてしまいます。これが今まででは普通でしたが、今後はもしかしたら変わるかも?という話です。

権限に関して制限をかけて、拒否させることが可能です。

以下のような要領で拒絶させることができるようになります。

$ node --policy-deny=net

上のオプションでプロセス内のネットワークアクセスを拒否させることが可能です。lint や formatter のときはネットワークアクセスとか不要なのでこのようなオプションで動かすほうが望ましいかも知れませんね。

元々 policy が去年入ってた のですが、それをエンハンスするような形ですね。

ちなみにまだ議論真っ最中です。

github.com

Options

下記のオプションを設定することが可能です (現時点での Permission の実装では)。

  • fs ファイル読み書き
  • fs.in ファイル読み込み
  • fs.out ファイル書き込み
  • net ネットワークアクセス
  • net.in ネットワーク入力
  • net.out ネットワーク出力
  • process 子プロセス起動
  • signal プロセスシグナル送信(プロセスの強制終了などを許可するか否か)
  • env 環境変数読み込み
  • worker ワーカースレッド起動
  • wasi WASI の可否
  • timing 高精度タイマーを拒否(おそらく process.hrtime が使えなくなる、サイドチャネル攻撃対策)
  • addon ネイティブアドオンの実行拒否 (native addon から何でもやられてしまうのを防ぐ目的)

などなど、どう使うのか使途がよくわからないものもありますが、 上記の処理に制限をかけることが可能です。

この他にも、 Permission Policy の機能にはそもそも policy.json で細かく設定できるように予定されているものもあり、例えば port 番号で制限するとかネットワーク接続先で制限するとかの今後の展望は用意されています。 ブラウザの Content-Security-Policy のような allow/deny list で管理するとかも検討が進めばできるようになるかもしれません。

内部実装

ここは内部実装向けなので、興味のある方だけで良いです。

Node.js が v8 を起動する際に permission が設定されます。Permissionチェックは runInPrivilegedScope 関数が用意され、その関数の中で実行した場合のみ、権限チェックが行われます。その関数のコールバックの外側では権限チェックがされないので、内部の fs, net などのコアパッケージでは権限チェックをするところと権限チェックが行われない特権実行の両方が行われます。これは Node.js 内部関数で全てを制限されてしまうと何もできなくなるため、特権を持った関数実行と特権を持たないユーザーが設定した Permission とを区別するために行われています。

function foo(a, b, c) {
  return a + b + c;
}

const privilegedFoo = runInPrivilegedScope.bind(this, foo); // この中でのみ権限チェックされる

console.log(privilegedFoo(1, 2, 3));

// この外では権限チェックされない。

これって deno の permission と一緒?

そう思った方は deno を追いかけてる方ですね!

FAQ に書いてありますが、その回答としては Yes でもあり No でもあるとのことです。

元々の発想は deno から来ていますが、実装は物凄くシンプルに作られていて、 Node.js の internal API 部分で制御しています。 deno はセキュリティモデルからして node とは違うので同じものとは考えにくいです。 deno は特権が必要なときに (OS がカーネルに伺いを立てるかのごとく) 特権を要求するように設計されています。そのタイミングで制限をかけるようになっています。またデフォルトでは制限されていて、 opt-in で権限を付ける所が deno のがセキュリティとしては強いものであると言えるでしょう。

node はデフォルトでは全ての実行が許可されています。制限を後からかけるように設計されています。後から追加した機能なので、ある程度徐々に制限を厳しくできるように設計されたものと言えるでしょう。

package 毎の制御はできるのか?

これを思った方は deno を追いかけてる(以下略

今はできないと思います。 deno もできないのですが、本来はより粒度を細かい単位で実現できるできないの制御をしたいですよね。一律プロセス全体で ネットワーク接続できる、できない、ではなく、このパッケージではできるけど、このパッケージでは許可しない、といったような制御がしたいですよね。

deno も検討中ですが、まだできてないですね。

ただし、面白そうな議論は既に上がっています。

github.com

permission フィールドを package.json に追加させるようにして、その単位で制限をかけられないか?という npm の RFC ですね。 npm のスクリプト実行時に net へのアクセスが必要ならそのタイミングで --grant=net のような許可を要求できるようにしたい、というものです。

まだ全然議論が始まったばかりですが、このあたりは非常に注目していくほうが良いでしょう。

Deno 化する Node.js / Node.js 化する Deno

Node.js に ESM が入り、 top-level await などのモダンな機能が追加されていき、パーミッションの実行までできるようになってくると徐々に deno との差別化ポイントは薄くなっていきます。

Deno は Deno で Node.js との完璧な互換性を求めていないものの、 std:node のような標準モジュールを用意し、 compatible にできるところは合わせていく流れもあったりします。

github.com

結局エコシステムがどういう機能を求めるか次第で JavaScript の世界の API や環境は変わっていくので、どちらもそこまでの差がなくなっていくようにも思えました。

筆者はよく「これからは Deno を使ったほうが良いのでしょうか?」という旨の質問を受けることがあります。本質問に対しての筆者の意見を書いておきます。

Deno も Node.js もどちらも Web コミュニティの中にあるものであり、 Web コミュニティの進化に合わせて機能が作られていくという意味では外側の API 面ではあまり変わらないものになっていくと思われます。 Hello World サーバは Deno でも Node.js でもほとんど同様の書き方で提供されています。

Deno

import { serve } from "https://deno.land/std@0.82.0/http/server.ts";
const s = serve({ port: 8000 });
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

Node.js

import { createServer } from 'http';
import { on } from 'events';
const reqs = on(
  createServer().listen(3000), 
  'request'
);
for await (const [_, res] of reqs) {
  res.end('Hello World\n');
}

外側に大きな差異がないのであれば、非機能要件での進化、パフォーマンス、セキュリティ、運用継続性などの観点で選べばよいと思っています。そのうちどちらも変わらない書き方で提供されるようになれば移行もそこまで難しくはなくなると思います。

開発者のタイミングで Deno を使うか Node.js を使うかを選べば良いのではないかと思っています。

この半年やったこと、継続していること

syohex.hatenablog.com

studio3104.hatenablog.com

あまりにも同じことをしていたので「せっかくなので」と思って筆を執ることにする。

@syohex さん@studio3104 さん も僕もだいたい同年代の人たちが同年代の sugyan や色々な方の影響を受けて同じことをしているというのはシンパシーを感じますね。

僕は今の仕事はフロントエンドエンジニアであることが多いのですが、「知識に垣根は作らない」をモットーにしているので、色々半年間挑戦してみました。

Leetcode

この半年で545問解きました。

github.com

sugyan が leetcode に取り組んでいたのも見てたのですが、僕の場合は自分の会社の面接でコード面接をやることがあり、コード面接の時に自分が知らないような事を問題として出すのは恥ずかしいな、という思いから勉強し始めたら楽しくてハマってしまったという経緯です。(もちろん同世代の sugyan がやれていることにも憧れはありました)

あとコロナ禍だったりでリモートになり、仕事と家の間の行ったり来たりが減ったことで、割と融通がきくようになったのでここは一つと思って競技プログラミングの勉強がてら、習慣として leetcode を毎日実施しています。

f:id:yosuke_furukawa:20201217021152p:plain

https://leetcode.com/yosuke-furukawa/

毎日 Leetcode を書いているおかげか、Easy, Medium の問題は全てではないですがなんとか解けます。Hard もそこまで難しい問題ではないときがあるので解けるときは解いてます。

ちなみにずっと JavaScript で解いてるのですが、こういう競技プログラミングには全く向きませんね。 Java とかであれば LinkedList が使えたり、 Heap の実装が既にあったりするのですが、 JS の場合はそれを自作するところからだったりします。

標準ライブラリを仕様に入れるという構想があるようなので、コレクションクラスの充実を望みます。

書き始めてから半年しか経過してないのでなんとも言えませんが、レビュー等での指摘で多少賢いやり方を教えられることが増えたようにも思うのと、 OSS でも基礎的なアルゴリズムを使ったようなものは多少すばやく書けるようになった気がします。

後最近は Advent of Code も毎日出るんで解いてます。

f:id:yosuke_furukawa:20201219013703p:plain
Advent Of Code 2020

個人的に好きな leetcode の問題

LRU Cache ですね。

leetcode.com

一度ライブラリで自作した時に JavaScript で実装するとこんなに面倒なのか、、、と思ったのですが、 leetcode でも出てきて「おおっ」となりました。ちなみに OSS で自分の自作した LRU Cache を使っているのですが、一時的な API の Cache として保持する時に使っています。有用だし、高速化されるし、作ってみると面白いと思います。

他には数独 Solver も好きです。

leetcode.com

Backtrackで解ける問題昔は苦手だったのですが Leetcode を経て楽しく解けるようになりました。

英語

DMM 英会話くらいしかやってませんが、半年で 4900 分間レッスンを受けました。

f:id:yosuke_furukawa:20201219011736p:plain

こっちは物理的にも時間的にも制約があるのと相性の良い先生とのレッスンを取ろうとすると相手の都合もあったりで、毎日コンスタントにやるのは難しいのですが、大体週5-6日は英語で50分間はしゃべっています。

これもコロナ禍の空き時間を使った形ですね。英語を再勉強するというきっかけになりました。あまり発音も良くないので、発音を矯正しようと思って本で勉強したりしています。

www.amazon.co.jp

あと今年は海外での登壇予定があったので、そのためにも気合を入れてやってました。

youtu.be

これまでもそれなりに話せたといえば話せたのですが、発音を気をつけてやってみたら agektmr さんからも褒められて嬉しかったです。

後最近は Youtuber の Daijiro さんの動画を笑いながら見てます。

www.youtube.com

数学

あまりちゃんとやれてないですが、数学も解いてます。 Youtube で数学の問題を必ず一日一問解説してる 鈴木貫太郎さんの動画があるので、それを見てます。

www.youtube.com

大学のノートみたいなのを買って朝の30分だけ解いたりしてます。朝起きた時に問題を見て、解いてみて、あってるか動画で見て、動画に自分がどうやって解答したかを書いてます。完全に知識が鈍ってるのでエレガントな方法をスッと思いつくのは苦手ですが。

数学ができると leetcode の問題でも数学の知識を問う問題があって解けるようになったりと色々付加的な効果がありました。

鈴木貫太郎先生が教えてくれる整数問題はパズルのようで楽しいです。ただやってると高校の時にもっと勉強しておけばよかったな、、、と思うことが多いですね。

競技プログラミングや英語と違い、数学は特に明確な目的があって始めたわけじゃなく、趣味みたいな感じです。

これから

こういう活動を会社の仕事や家の事があるので柔軟な時間にちょこちょことやってます。多分これからも同じことを続けていきたいと思いつつ、ある程度切りの良いところを見つけたら別な挑戦もしてみるかもしれません。新しいプログラミング言語を身につけるとか低レイヤをやってみるとか、ずっと漠然とした憧れがありながらもできていないので、コロナ禍のまだリモートワークを中心とした生活の中で会社の仕事とのバランスを取りながらやってみようかと思ってます。

yuroyoro.hatenablog.com

(ちなみに、この記事すごくいいですよね。やってみたくなります。)

assert.CallTracker と must-call

この記事はリクルートエンジニアアドベントカレンダーの2日目の記事です。過ぎてるかもしれませんが、メンバーから脅迫されて書いてます。

f:id:yosuke_furukawa:20201204210950p:plain

assert.CallTracker

Node.js で experimental な API として assert.CallTracker がv14 で追加されました。 この機能は呼び出された回数を検証するという機能を持った新しい assert 関数です。

コールバック関数をテストする際に使える便利関数です。機能としては地味ですが、使いこなせると便利なので紹介します。

const assert = require('assert');

const tracker = new assert.CallTracker();

function func() {}

// 一度だけ呼ばれることを期待
const callsfunc = tracker.calls(func, 1);

// ここで呼び出す。
callsfunc();

// tracker.verify を呼ぶことで何度呼ばれたかを検証する。
// これはプログラムが終わるときに1度だけ呼ばれてることを検証する。
process.on('exit', () => {
  tracker.verify();
});

これだけ見ていると何が便利かわからないかも知れませんが、テストを書いた時にコード内の不具合で assert が呼ばれずに終わってしまう状況を防ぐことができます。

const assert = require('assert');
const tracker = new assert.CallTracker();

const http = require('http');

const server = http.createServer();
// 一回だけリクエストが来ることを期待
server.on("request", tracker.calls((req, res) => {
  assert.strictEqual("/", req.url);
  res.end("Hello World!");
}, 1));

server.listen(3000);

process.on("SIGINT", () => {
  // もしも一度もリクエストが来なかったらここで検証失敗が出る
  tracker.verify();
  process.exit();
});

とまぁ、ここまでが CodeGrid で書いた内容です。

www.codegrid.net

ここから蛇足と言うか多少細かすぎて伝わらない話をしようかと。

mustCall

この機能もともとどこから来たかと言うと、 Node.js のコアのテストでよく書かれてた common.mustCall っていう test utility 関数から来ています。

https://github.com/nodejs/node/tree/master/test/common#mustcallfn-exact

これを一般向けに公開したのがこの機能です。 Node.js の中では Event を使ったテストが多く、『イベントが呼ばれなかった時に検証が行われずそのままパスしてしまう』というのを防ぐモチベーションで作られていました。

今では実は jest とかでもやっちゃうときはやっちゃいますよね。

余談ですが、この機能はめっちゃ便利だなーと思っていたので昔 must-call っていうライブラリとして公開していました。

www.npmjs.com

f:id:yosuke_furukawa:20201204215906p:plain

今となっては assert.CallTracker で置き換えるか、 npm モジュールにしなくても数行で書けると思うので、役目を終えたライブラリになりました。

他にも test common な utility にある便利な関数はあると思うので、意外とこの辺りは公開すると便利かもしれませんね。

https://github.com/nodejs/node/tree/master/test/common

急遽書いたので内容薄いですが、もう少し濃いやつはまた次回。