一年の振り返り的サムシング

なんとなく振り返りたくなったので振り返ってみる……

COVID-19 と在宅勤務みたいな話はもう前提だと思うので省く。

ゲーム

Baba Is You

switch で配信されて 2 月ぐらいまでやってた:

store-jp.nintendo.com

面白かったな〜。会社の Slack にも Baba の絵文字が追加されたので多用していました。

スーパーマリオ 3Dコレクション

www.nintendo.co.jp

スーパーマリオ64やっぱおもしれ〜というのは安定としてマリオサンシャイン未プレイだったんですがこっちもめちゃおもしれ〜となりました。モンテ族の人々が基本的に相手を信用しない話しぶりをするのでなんや不必要に感じ悪いなあと思ったけどそれ以外はゲームの難易度が高くて全く飽きずに楽しめました。シャインは 117 枚集めたんですが青コイン探しだけが残った状態になってしまっているので、攻略情報を見て単に回収していくのはそもそもつまらないし、しかしノーヒントでしらみつぶしにやっていく作業もつらそうだしな〜、とりあえずもう良いか……となり一旦やめにしました。

プログラミング言語と型

社では諸々あるんですが基本的には RoR の面倒を見るお仕事をやっていました。

………

やっていたんですが、しかし何年も前からわかっていたこととして僕は非常に注意力散漫でうっかりミスが多い人間なのでコンパイラの支援無しにコードを書くという行為がかなり辛い。

スコープに存在しない変数名とか存在しないメソッド名とかを間違って書いてしまっていたらコンパイル時に静的にエラーになってほしい……という、生命の素朴な欲求が満たされない環境にずっと晒されて、…とも言いたくはあるがしかしこれはただ僕の脳がこの言語に向いてないだけだとは思います。

Ruby はクソ、Rails は死んだ」と口で言うのは簡単なのですが、一般論として、ある対象の問題を指摘する際はその問題を独立して抽出してから批判を加えなければ筋の通った論として成立しません。とりあえず RubyRails に対する感情的な部分は無視するとして、Rails による苦しみは Ruby に型が付けば解決するのか?マヌケな NoMethodError が実行時(!)に発生するのは真に「動的型付け」のせいであると言えるのか?静的型付けは何が本質なのか?型システムによって何が静的に保証され何が保証できないのか……要するに少なくともまず型システムの知見を持っていなければ「動的型付け」言語に対してまっとうな発言ができないわけです。

(念のため書いておくと、仮に僕が型システムや型理論を完全に理解していたとしても「Ruby はクソ、Rails は死んだ」という発言を陽にすることはありません……RubyRails が好きな人は世界中にいるし、わざわざ水を差したり喧嘩を売ったりする必要なんてどこにもありません。それに社内にも、同じチーム内にも RubyRails が好きな人がたくさんいます(というかチーム内では僕以外全員好きだと思う)。暴言を吐くことで一緒に働く仲間とのチームビルディングを台無しにする、あるいは生産性を阻害する……明確にマイナスの影響しかもたらさない行為を良く思う人はいないでしょう。

たとえば 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"));

src/components/Markdown.tsx

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 buildpath を解決できずに失敗します:

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 が存在します:

github.com

こいつの仕事は主に 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 を設定できるようになったことで若干バンドルサイズも小さくなり、嬉しいですね。

warp filter 設定

warp

warpreqwesthyper の作者である 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 の一覧がここに載っている:

warp::filters - Rust

眺めてみるだけでもなんとなく使い方が予想できるのではないか。

path

起点となるのは path filter かもしれない:

warp::filters::path - Rust

その名の通り 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! マクロもある:

warp::path - Rust

ドキュメント 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 もよく使いそう:

warp::filters::header - Rust

例えばこんな感じ:

use warp::Filter;

let authorization_token =
    warp::header::<String>("authorization").map(|autorization: String| {
        autorization
            .trim()
            .strip_prefix("Bearer ")
            .unwrap_or("")
            .to_string()
    });

CORS

場合によっては CORS のことも考えないといけない。

developer.mozilla.org

warp には cors filter もある:

warp::filters::cors - Rust

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"]);

warpcors のテストコード も参考になると思う。

ちなみに cors filter を設定しておくとプリフライトリクエストの処理は warp 側でよしなにやってくれるので、アプリケーションコードには OPTIONS リクエストの処理は書かずに単純に目的のエンドポイントの処理だけ書けば良い。

Filter の合成

filter たちを合成してルーティングを構築したり値を抜き出して加工したりするためのメソッドが Filter trait には定義されている:

warp::Filter - Rust

and, or

一番使うのは andor だと思う。

前述までの例にも出てきていたが、例えばこんな感じでルーティングを定義したりできる:

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 というメソッドが用意されている。 boxedBoxedFilter という型を返し、こんな風に書ける:

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()
}

いかがでしたか

他にもいろいろありますがとりあえずこの辺で。

warp は独自の概念を理解するのにちょっと時間がかかったけど慣れると面白い Web フレームワークだと思う。