一年の振り返り的サムシング
なんとなく振り返りたくなったので振り返ってみる……
COVID-19 と在宅勤務みたいな話はもう前提だと思うので省く。
ゲーム
Baba Is You
switch で配信されて 2 月ぐらいまでやってた:
面白かったな〜。会社の Slack にも Baba の絵文字が追加されたので多用していました。
スーパーマリオ 3Dコレクション
スーパーマリオ64やっぱおもしれ〜というのは安定としてマリオサンシャイン未プレイだったんですがこっちもめちゃおもしれ〜となりました。モンテ族の人々が基本的に相手を信用しない話しぶりをするのでなんや不必要に感じ悪いなあと思ったけどそれ以外はゲームの難易度が高くて全く飽きずに楽しめました。シャインは 117 枚集めたんですが青コイン探しだけが残った状態になってしまっているので、攻略情報を見て単に回収していくのはそもそもつまらないし、しかしノーヒントでしらみつぶしにやっていく作業もつらそうだしな〜、とりあえずもう良いか……となり一旦やめにしました。
プログラミング言語と型
社では諸々あるんですが基本的には RoR の面倒を見るお仕事をやっていました。
………
やっていたんですが、しかし何年も前からわかっていたこととして僕は非常に注意力散漫でうっかりミスが多い人間なのでコンパイラの支援無しにコードを書くという行為がかなり辛い。
スコープに存在しない変数名とか存在しないメソッド名とかを間違って書いてしまっていたらコンパイル時に静的にエラーになってほしい……という、生命の素朴な欲求が満たされない環境にずっと晒されて、…とも言いたくはあるがしかしこれはただ僕の脳がこの言語に向いてないだけだとは思います。
「Ruby はクソ、Rails は死んだ」と口で言うのは簡単なのですが、一般論として、ある対象の問題を指摘する際はその問題を独立して抽出してから批判を加えなければ筋の通った論として成立しません。とりあえず Ruby や Rails に対する感情的な部分は無視するとして、Rails による苦しみは Ruby に型が付けば解決するのか?マヌケな NoMethodError
が実行時(!)に発生するのは真に「動的型付け」のせいであると言えるのか?静的型付けは何が本質なのか?型システムによって何が静的に保証され何が保証できないのか……要するに少なくともまず型システムの知見を持っていなければ「動的型付け」言語に対してまっとうな発言ができないわけです。
(念のため書いておくと、仮に僕が型システムや型理論を完全に理解していたとしても「Ruby はクソ、Rails は死んだ」という発言を陽にすることはありません……Ruby や Rails が好きな人は世界中にいるし、わざわざ水を差したり喧嘩を売ったりする必要なんてどこにもありません。それに社内にも、同じチーム内にも Ruby や Rails が好きな人がたくさんいます(というかチーム内では僕以外全員好きだと思う)。暴言を吐くことで一緒に働く仲間とのチームビルディングを台無しにする、あるいは生産性を阻害する……明確にマイナスの影響しかもたらさない行為を良く思う人はいないでしょう。
たとえば Rust という良い側面をいくつも兼ね備えた言語がありますが、Rust を称賛するときに「Ruby には◯◯が無くてダメなんですが〜」みたいな枕詞を添える必要も別に無いわけです。これは単に品性の問題な気がしますが……それに自分が知らないだけで実は Ruby でもできることかもしれないし。いずれにせよそれは発言者がいわゆる「ダメな反対派」になってしまうだけで、称賛している対象のものの評価を逆に下げてしまうことになります。……なんか自明なことばかり無駄につらつら書き続けているだけな気持ちになってきたのでもうやめます。)
とにかく型の勉強をしたことがなかったので勉強したいな〜となり、「プログラミング言語の基礎概念」という教科書を読みました:
from:pione30 プログラミング言語の基礎概念 - Twitter Search
導出木が横に広がるので久々にキャンパスノートとペンを使って問題を解くという営みをやって楽しかったです。
その後 TaPL を購入して第Ⅰ部まで読んだところで気が抜けて積んでしまってる。またぼちぼち続きを読もうかな。
それで趣味ではサーバサイドを Rust (warp + diesel) で書くなどをして呼吸をしつつ、仕事ではここ 1 ヶ月くらいは関わっている Web サービスのフロントエンドに React (+ TypeScript) を導入して、とある画面を React で書き換えることを(メイン業務の合間を縫って)やっていました。
これは個人的な秘めたる思いとして Web 開発の領域でどうにか型をゲットしたい、となると TypeScript がとりあえずお手軽でコンセンサスも得やすい、かつ TypeScript と相性が良く実績もあるライブラリというと React だろうという気持ちで虎視眈々と機会を伺っていたところ、諸々があってチーム内に「宣言的 UI 最高!宣言的にもっと UI を書きたい!」と思ってくれる方が急に増えたので、独り善がりでなくやっていける環境が発生した感じです。
TypeScript の型システムは健全性に関して gotcha がたくさんあるという話もよく聞くしそういう記事もよく見るので今後の僕の精神状況がどう変化するかはまだなんとも言えませんが、現時点ではやっと辛うじて息継ぎができるようになったというのが率直な気持ちです。なのでしばらくやっていくつもりです。
正直それまで「現時点でも問題なく動いていて変更もあまり入らない JS のコードをあえて書き換えることに意味はあるのか」と問われたときに明確に回答できない……と自問不自答(?)して意気消沈することが何度かあったところを勢いで書き換えたのですが、予想しなかったその後の副次的な効果として「React ならアドバイスできるよ」と言ってくれる社内の先輩が数人どこからともなく現れて Slack のチャンネルやリポジトリにコメントを残していってくれるようになり、こんなところで言うのもアレですが感謝しかないッスね…… というか現在進行系で人気のある技術についてはそのぶん知見を持った人の数が相対的に多くて、最新の知見がインターネット上に多く存在したり、関連ライブラリが良くメンテナンスされていたりして、するとプロダクトに付加価値をつける新たなアイデアを実装するのも楽になるわけだなあと今さらながら気づきました。コミュニティに人が多いということはすなわちパワーだということを感じる……
来年以降は静的に型が付くとか型が推論されるような環境は前提にして、より発展的な型の話題の知見をためていきたいと思っています。
ピアノ
夏頃に電子ピアノとブルグミュラー 25の練習曲を買った。全然練習できていない。身体がもう一個欲しいですね。
webpack 5 Node.js polyfill
webpack 4 以前では Node.js core module の polyfill が webpack 自体に同梱されていて、bundle したい module が 1 つでも core module の機能を使っていれば自動的にその polyfill が追加されていました。 しかし webpack 5 からはこの自動 polyfill が廃止され、真に必要な場合にのみユーザが手動で polyfill を追加するというようになりました 1 。
具体例を挙げると、react-markdown は unified 2 を通じて vfile というパッケージに依存しており 3、vfile は path
module に依存している 4 ので、webpack 5 では何も polyfill の設定をしないと react-markdown を利用したコードのビルドに失敗します。
小さな例で実験してみましょう。
webpack v4 の場合
package.json
{ "scripts": { "build": "webpack --mode production" }, "devDependencies": { "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "ts-loader": "^8.0.11", "typescript": "^4.1.2", "webpack": "^4.44.2", "webpack-cli": "^4.2.0" }, "dependencies": { "react": "^17.0.1", "react-dom": "^17.0.1", "react-markdown": "^5.0.3" } }
webpack.config.js
const path = require("path"); module.exports = { entry: path.resolve(__dirname, "src/index.tsx"), output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), }, module: { rules: [ { test: /\.tsx?$/, use: { loader: "ts-loader", } }, ], }, resolve: { extensions: [".ts", ".tsx", ".js"], }, }
tsconfig.json
{ "compilerOptions": { "target": "es2020", "module": "esnext", "moduleResolution": "node", "jsx": "react-jsx", "strict": true, "esModuleInterop": true } }
src/index.tsx
import ReactDOM from "react-dom"; import { Markdown } from "./components/Markdown"; ReactDOM.render(<Markdown></Markdown>, document.getElementById("root"));
import ReactMarkdown from "react-markdown"; export const Markdown = () => { const body = ` ## Hello, Markdown! Hello! react-markdown is [here](https://github.com/remarkjs/react-markdown). `; return ( <> <ReactMarkdown children={body}></ReactMarkdown> </> ); };
dist/index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <div id="root"></div> <script src="bundle.js"></script> </body> </html>
この状態で npm run build
を実行するとビルドは正常に完了し、 dist/index.html をブラウザで開けば想像通りの画面が表示されます
(ちなみにこの状態で bundle.js のサイズは 210 KiB でした)。
webpack v5 の場合
しかし上記の状態で webpack を最新バージョン(執筆時点では v5.9.0)に上げると:
- "webpack": "^4.44.2", + "webpack": "^5.9.0",
npm run build
は path
を解決できずに失敗します:
ERROR in ./node_modules/vfile/core.js 3:11-26 Module not found: Error: Can't resolve 'path' in '/<path-to-the-current-directory>/node_modules/vfile' BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default. This is no longer the case. Verify if you need this module and configure a polyfill for it. If you want to include a polyfill, you need to: - add a fallback 'resolve.fallback: { "path": require.resolve("path-browserify") }' - install 'path-browserify' If you don't want to include a polyfill, you can use an empty module like this: resolve.fallback: { "path": false } @ ./node_modules/vfile/index.js 4:12-32 @ ./node_modules/unified/index.js 8:12-28 @ ./node_modules/react-markdown/lib/react-markdown.js 5:14-32 @ ./src/components/Markdown.tsx 2:0-43 9:45-58 @ ./src/index.tsx 3:0-49 4:21-29
そこで、エラーメッセージに従って path-browserify をインストールし resolve.fallback
の設定 5 を行います:
package.json
"devDependencies": {
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
+ "path-browserify": "^1.0.1",
"ts-loader": "^8.0.11",
"typescript": "^4.1.2",
"webpack": "^5.9.0",
webpack.config.js
resolve: { extensions: [".ts", ".tsx", ".js"], + fallback: { + "path": require.resolve("path-browserify"), + } }, }
すると一応 npm run build
は成功するようになります。
が、 dist/index.html をブラウザで開くと画面には何も表示されず、コンソールに ReferenceError: process is not defined
というエラーが表示されるでしょう。これは実は vfile が process
という名のグローバルオブジェクトに依存しているためです 6 。
Node.js では process
はグローバルな文脈に存在します 7 が、ブラウザ環境では process
の shim は手で行わなければなりません。そういった特定のグローバル変数の shim を行うために imports-loader という loader が存在します:
こいつの仕事は主に test
でマッチしたファイルの先頭に何らかのモジュールを import することです。
そこで
% npm i -D imports-loader process
の後 imports-loader の設定 8 を行い
webpack.config.js
loader: "ts-loader", } }, + { + test: /node_modules\/vfile\/core\.js/, + use: { + loader: "imports-loader", + options: { + type: "commonjs", + imports: ["single process/browser process"], + }, + }, + }, ], },
改めて npm run build
を実行すれば、晴れて react-markdown が使えるようになります。
ちなみにこの状態で bundle.js のサイズは 208 KiB でした。きめ細かく polyfill を設定できるようになったことで若干バンドルサイズも小さくなり、嬉しいですね。
-
https://webpack.js.org/blog/2020-10-10-webpack-5-release/#automatic-nodejs-polyfills-removed↩
-
https://github.com/remarkjs/react-markdown/blob/5.0.0/package.json#L47↩
-
https://github.com/unifiedjs/unified/blob/9.0.0/package.json#L51↩
-
https://github.com/webpack/webpack.js.org/blob/dd7ed89d8b31bdb342fee26173d0b3c81266a292/src/content/configuration/resolve.md#resolvefallback↩
-
https://nodejs.org/docs/latest-v14.x/api/process.html#process_process↩
-
https://github.com/vfile/vfile/issues/38#issuecomment-683198538↩
warp filter 設定
warp
warp は reqwest や hyper の作者である Sean McArthur 氏が hyper の上で作っている Rust 製の Web フレームワーク: github.com
執筆時点での最新バージョンは 0.2.5: docs.rs
どのエンドポイントにどんな種類のリクエストが来たらどんな処理をするかを記述するための Filter と呼ばれる概念があり、これを使ってサーバ側の処理を書いていくのが warp の特徴。 README 1 に乗っている例はこんな感じ:
[dependencies] tokio = { version = "0.2", features = ["full"] } warp = "0.2"
use warp::Filter; #[tokio::main] async fn main() { // GET /hello/warp => 200 OK with body "Hello, warp!" let hello = warp::path!("hello" / String) .map(|name| format!("Hello, {}!", name)); warp::serve(hello) .run(([127, 0, 0, 1], 3030)) .await; }
組み立てた filter を warp::serve
に渡して走らせる。
Filter
Built-in Filter の一覧がここに載っている:
眺めてみるだけでもなんとなく使い方が予想できるのではないか。
path
起点となるのは path
filter かもしれない:
その名の通り path を定義する。
ドキュメント 2 にあるコード例は:
use warp::Filter; // GET /hi let hi = warp::path("hi").map(|| { "Hello, World!" });
param
path からパラメータを抜き出したいときは param
を使う:
warp::filters::path::param - Rust
こんな感じ:
use warp::Filter; // GET /sum/:u32/:u32 let sum = warp::path("sum") .and(warp::path::param::<u32>()) .and(warp::path::param::<u32>()) .map(|a, b| format!("{} + {} = {}", a, b, a + b));
path や param を定義するのに便利な path!
マクロもある:
ドキュメント 3 にあるコード例:
use warp::Filter; // GET /hello/from/warp let hello_from_warp = warp::path!("hello" / "from" / "warp").map(|| { "Hello from warp!" }); // GET /sum/:u32/:u32 let sum = warp::path!("sum" / u32 / u32).map(|a, b| { format!("{} + {} = {}", a, b, a + b) });
header
header
filter もよく使いそう:
例えばこんな感じ:
use warp::Filter; let authorization_token = warp::header::<String>("authorization").map(|autorization: String| { autorization .trim() .strip_prefix("Bearer ") .unwrap_or("") .to_string() });
CORS
場合によっては CORS のことも考えないといけない。
warp には cors
filter もある:
warp::cors
を呼ぶと warp::filters::cors::Builder が返るので適宜設定していく。
allow_headers
, allow_methods
, allow_origin
らへんを主に使うと思う。こんな感じに:
use warp::Filter; let allowed_origin = env::var("ALLOWED_ORIGIN").expect("ALLOWED_ORIGIN must be set"); let cors = warp::cors() .allow_origin(allowed_origin.as_str()) .allow_headers(vec!["authorization"]) .allow_methods(vec!["GET", "POST", "PUT", "DELETE"]);
warp の cors
のテストコード も参考になると思う。
ちなみに cors
filter を設定しておくとプリフライトリクエストの処理は warp 側でよしなにやってくれるので、アプリケーションコードには OPTIONS
リクエストの処理は書かずに単純に目的のエンドポイントの処理だけ書けば良い。
Filter の合成
filter たちを合成してルーティングを構築したり値を抜き出して加工したりするためのメソッドが Filter
trait には定義されている:
and, or
一番使うのは and
と or
だと思う。
前述までの例にも出てきていたが、例えばこんな感じでルーティングを定義したりできる:
use warp::Filter; let api = warp::path("api"); let v1 = warp::path("v1"); let users = warp::path("users").map(|| { let data = vec!["Alice", "Bob", "Carol"]; warp::reply::json(&data) }); let posts = warp::path("posts").map(|| { let data = vec!["Hello", "Nice", "Uh-oh"]; warp::reply::json(&data) }); let routes = api.and(v1.and(users.or(posts)));
map, and_then
filter たちが抜き出したり作ったりした値を加工して返す。
ドキュメント 4 にあるコード例:
use warp::Filter; // Map `/:id` warp::path::param().map(|id: u64| { format!("Hello #{}", id) });
and_then
に渡すクロージャは TryFuture
を返し、 Err
内部の値は Rejection
型である必要があるとドキュメントに書かれていて、実際ドキュメント 5 のコード例にも
use warp::Filter; // Validate after `/:id` warp::path::param().and_then(|id: u64| async move { if id != 0 { Ok(format!("Hello #{}", id)) } else { Err(warp::reject::not_found()) } });
と書かれている。
勘違いしやすいが、4xx/5xx エラーであったとしても必ずしも Rejection とするべきとは限らない。 warp の filter システムにおける Rejection というのは「この filter では前提条件を満たせず処理できなかったけど、他の filter なら正常に処理できるかもしれないのでそっちに期待します」という意思の表明であって、マッチしたフィルター内で 4xx/5xx エラーが発生したとしてそれを最終結果としたい(他の filter に移りたくない)のであれば、 Err(Rejection) よりむしろステータスコードとともに Ok(Reply) を返すべきである。
Sean McArthur 氏のコメント や examples/todos も参考になるかもしれない。
boxed
filter の数が増えてくると、それらを関心別に関数に分割したくなると思う。
しかし単純にやろうとすると、こういうダルい型を書かなくてはならない:
use warp::Filter; fn routes() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { let api = warp::path("api"); let v1 = warp::path("v1"); api.and(v1.and(users().or(posts()))) } fn users() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { warp::path("users").map(|| { let data = vec!["Alice", "Bob", "Carol"]; warp::reply::json(&data) }) } fn posts() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { warp::path("posts").map(|| { let data = vec!["Hello", "Nice", "Uh-oh"]; warp::reply::json(&data) }) }
こういう処理をより楽に書くために boxed
というメソッドが用意されている。 boxed
は BoxedFilter
という型を返し、こんな風に書ける:
use warp::{Filter, filters::BoxedFilter, Reply}; fn routes() -> BoxedFilter<(impl Reply,)> { let api = warp::path("api"); let v1 = warp::path("v1"); api.and(v1.and(users().or(posts()))).boxed() } fn users() -> BoxedFilter<(impl Reply,)> { warp::path("users") .map(|| { let data = vec!["Alice", "Bob", "Carol"]; warp::reply::json(&data) }) .boxed() } fn posts() -> BoxedFilter<(impl Reply,)> { warp::path("posts") .map(|| { let data = vec!["Hello", "Nice", "Uh-oh"]; warp::reply::json(&data) }) .boxed() }
いかがでしたか
他にもいろいろありますがとりあえずこの辺で。