Buri Memo:

アイデアや気づきとかが雑に書き殴られる

ブラウザで Tensorflow.js を動かすと GPU メモリが増加し続けてクラッシュした

Tensorflor.js をブラウザで動かし、モデルをループで推論(predict)させていると GPU のメモリがジワジワと増え続け最終的にクラッシュしてしまうという現象があった。結論としては tf.Tensor オブジェクトは状況によっては(WebGL バックエンドが自動的に選択された場合) GC されない仕様になっており、リソースの明示的な開放が必要だった。

記事末リンクの、メモリ管理で説明されている tf.tidy を嚙ませてあげることでメモリリークが解消した。

ちなみに、WebGL バックエンドはブラウザでの実行時に選択されるらしい。現在使われているバックエンドはこのメソッドで取得できる。

console.log(tf.getBackend());

また Node.js や cpu バックエンドでは GC されるが tf.tidy で開放してもデメリットはなく、むしろパフォーマンス的にメリットがあるようなのでとりあえず明示的に開放してあげる方が安全な気がする。

www.tensorflow.org

CSRF攻撃の具体例と、SameSite(Cookie) での対策

Web アプリケーションの脆弱性、特に有名なものに3つあるが CSRF だけはピンと来なかった。色々調べて理解できたので具体的な例でまとめてみる。

  • XSS
  • SQL Injection
  • CSRF

CSRF の脆弱性

CSRF (Cross-Site Request Forgeries)脆弱性があると、利用しているサイトのユーザーに攻撃者が望む操作をさせることができる。

例えば、Aさんはあるショッピングサイトを利用しているとする。Aさんがパスワードを変更する場合はパスワード変更画面にアクセスしてそこで新しいパスワードを入力し変更ボタンを押す。

実際に行われることを細かく書くと以下のようになる。

  1. Aさんのブラウザでパスワード変更画面を開く(すでにログイン済み)
  2. 新しいパスワードを入力し、[変更する] を押す
  3. ショッピングサイトのサーバーに 新しいパスワード とAさんのリクエストであることを認証するため session token (cookie)が送られる
  4. session token が正しいことを確認した上で、DB に保存されているパスワードを cA3tB9FJSdZm に更新する 1

パスワード変更画面

攻撃例

CSRF攻撃の例

このパスワード変更画面に CSRF の脆弱性がある場合のことを考えてみる。
以下のような手順が成功すると、攻撃者がパスワードを任意の文字列に変更することができてしまう。

  1. 攻撃者は Aさんに攻撃用ページへのリンクと、クリックさせるような文章のメールを送る。
  2. 騙された Aさんがページを開く
  3. ページに設置されたスクリプトによって、ショッピングサイトへ「パスワードを "abc123" に変更する」リクエストが送信される。この時 Aさんのブラウザに保存された shop.jp の cookie が送信される。
  4. ショッピングサイトは session token を見て Aさんからの正規のリクエストだと判断しパスワード変更処理を行う

ここで [3] のスクリプトは以下のようなものである。ポイントはこのフォームが送信されるときに cookie (Aさんの持つ session token が含まれる)が勝手に送信されてしまうことだ。

ちなみに、Ajax(XMLHttpRequest)リクエストは行っていないので 同一オリジンポリシーによる制限は受けない

<form name="form1" action="https://shop.jp/me/password/change/" method="POST">
<input type="hidden" name="password" value="abc123">
</form>
<script>document.form1.submit()</script>

これにより、攻撃者は Aさんになりすましてリクエストを偽造することができる。攻撃者のサイト経由でショッピングサイトへのリクエストを偽造することから、Cross-Site Request Forgeries と呼ばれている。

対策

IPA では以下のような対策が紹介されている

  • リクエストは POST で行う(後述: SameSite=Lax にする場合は必須)
  • csrf トークンをフォームに含めて、サーバーでチェックする
  • リファラーが期待するドメインか(前述の例では shop.jp )をチェックする

安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ) | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構

サーバー側で行う対策だけでなく、補助的に cookie を利用したクライアントサイドの対策もある。(新しいフォームの実装時に CSRF 対策を忘れてしまった場合にリスクを減らせる)

cookie には SameSite という属性がある。Strict に設定すると cookie を発行したサイトからのリクエストでのみ送信される(それ以外では送信されない)。strict では Google の検索結果からアクセスする場合や、お気に入りからアクセスした最初のリクエスト時に cookie が送られないためログインされずユーザー的に不便になる。Lax では GET リクエストのみ送信するのでバランスがいい。

ちなみに、モダンなブラウザでは Lax がデフォルトで設定される。ただし、SSO 時などの認証フロー上で cookie 送信が必要になるケースがあるようで Chrome では cookie 発効後 2分間は設定されない。( Google Developers Japan: 2020 年 2 月の SameSite Cookie の変更: 知っておくべきこと

ただし、すべてのブラウザでデフォルトが Lax に設定されているわけではないので、明示的に設定するのが安全である。(MDN 情報では Firefox, Safari はデフォルトで設定しないらしい)

繰り返しになるがブラウザに依存した対策であり、クライアントの環境によっては使えないことがあり得る。さらに、session token を必要としない処理に関しては対策とならない(例えば匿名掲示板に爆破予告を書き込ませる)。そのため補助的に使うのが良さそう。

https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie/SameSitedeveloper.mozilla.org



  1. 実際にはセキュリティ的な観点でパスワードをハッシュ化したものを保存する

RxjsのSubjectを活用したNode.jsプログラムの依存関係制御

Node.js で依存関係をうまく制御するために、Pub/Sub パターンを使いたかった。調べて見つけた Rxjs の Subject を利用することで上手く実装できたと思う。Pub/Sub を使うと何が嬉しいのかと、Subject を使った実装をまとめてみる。

例には以下の FizzBuzz クラスを使う。300ms ごとに 1~15までカウントアップして、Fizz Buzz が表示されるだけ。

そして、このクラスに対して Fizz が表示される回数を数える ことを考えてみる。

class FizzBuzz {
    static async run(): Promise<void>{
        const sleep = (ms: number) => new Promise((r) => setTimeout(() => r(null), ms));
        for(let i=1; i<=15; i++){
            await sleep(300);
            if(i % 15 === 0){
                console.log("FizzBuzz");
                continue;
            }
            if(i % 3 === 0){
                console.log("Fizz");
                continue;
            }
            if(i % 5 === 0){
                console.log("Buzz");
                continue;
            }
            console.log(i);
        }
    }
}

FizzBuzz.run();

/*
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
*/

素直に実装してみる

FizzCounter を追加して、"Fizz" を出力するタイミングでインクリメントするという感じにしてみた。問題なく動いてそうですね。

const fizzCounter = new class FizzCounter {
    private _count = 0;

    get count(): number {
        return this._count
    }

    increaseCount(): void {
        this._count++;
    }
}();

class FizzBuzz {
    static async run(): Promise<void>{
        const sleep = (ms: number) => new Promise((r) => setTimeout(() => r(null), ms));
        for(let i=1; i<=15; i++){
            await sleep(300);
            if(i % 15 === 0){
                console.log("FizzBuzz");
                continue;
            }
            if(i % 3 === 0){
                console.log("Fizz");
                fizzCounter.increaseCount();  // 👈 追加
                continue;
            }
            if(i % 5 === 0){
                console.log("Buzz");
                continue;
            }
            console.log(i);
        }
    }
}

(async () => {
    await FizzBuzz.run();
    console.log(`> Fizz count is ${fizzCounter.count}`);
})();

/*
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
> Fizz count is 4
*/

正直、シンプルなプログラムであれば問題にはならないと思うが、以下のような点が気になる。

  • fizzCounter.increaseCount() を公開しているため他の箇所からでもアクセスでき count の不整合になる可能性ある。
  • FizzBuzz クラスが自分の処理には関係ない fizzCounter.increaseCount() に依存してしまっている(無駄な結合)。
  • fizzCounter.increaseCount() のインターフェースが変わったりすると一番重要な FizzBuzz クラスが壊れる可能性がある。

これを、以下のように直したいなと思った。

  • 依存方向を FizzCounter --> FizzBuzz の方向に逆転させたい(安定方向への依存)。
  • fizzCounter が変化したとしても、FizzBuzz クラスに影響を与えないようにしたい(疎結合)。

Subject を使って書いてみる

Subject を使うことで Pub/Sub 的な形が作れる。Observable と似ているが、subscribe のタイミングや同時に publish できる数の制限はなくて融通が利く感じがする。

rxjs.dev

以下の様にすることで、依存方向が fizzCounter --> FizzBuzz になり、結合も疎になった(MQ を使った Pub/Sub には及ばないが、前のコードよりは疎結合だと思う)。

さらに、fizzCounter.increaseCount() は公開する必要がなくなったため非公開にすることができるようになった。やったー。
(テストのために公開したいということはあるかもしれない)

import { Subject } from "rxjs"

class FizzBuzz {
    static readonly fizzed = new Subject<void>();
    static async run(): Promise<void>{
        const sleep = (ms: number) => new Promise((r) => setTimeout(() => r(null), ms));
        for(let i=1; i<=15; i++){
            await sleep(300);
            if(i % 15 === 0){
                console.log("FizzBuzz");
                continue;
            }
            if(i % 3 === 0){
                console.log("Fizz");
                this.fizzed.next();  // 👈 Subject の next を呼び出す(Publish)
                continue;
            }
            if(i % 5 === 0){
                console.log("Buzz");
                continue;
            }
            console.log(i);
        }
    }
}

const fizzCounter = new class FizzCounter {
    private _count = 0;
    constructor() {
        // 👇 FizzBuzz.fizzed に Subscribe する。
        FizzBuzz.fizzed.subscribe(() => this.increaseCount());
    }

    get count(): number {
        return this._count
    }

    // 👇 private に変更した
    private increaseCount(): void {
        this._count++;
    }
}();

(async () => {
    await FizzBuzz.run();
    console.log(`> Fizz count is ${fizzCounter.count}`);
})();


/*
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
> Fizz count is 4
*/

@golevelup/nestjs-rabbitmq で実装した consumer が queue を消化できなくなった

このライブラリで作成した consumer が queue を消化できなくなり、メモリ使用率がじわじわと上昇し(300MB/8h 位の速度) OOM で停止してしまった。
RabbitMQ の GUI やサーバーのメトリクスを見ると以下のような状況になっていた。

  • 起動しているすべての consumer が Unacked メッセージを持った状態で止まっている
  • Redelivered が 1,000~2,000/s になっている
  • RabbitMQ ・ consumer 間のネットワーク通信量が大きく上がった

github.com

原因

  1. JSON として不正なメッセージを consumer が受け取る
  2. ライブラリ側のデシリアライズが失敗した際、nack されメッセージが queue の先頭に戻される
  3. requeue されたメッセージを再び consumer が受け取る
  4. [1] へ戻る、を永遠に繰り返す

このようなループを繰り返したことで queue の消化ができなくなってしまっていた。
メモリが上昇した理由は分からない。ループによるリソースの消費が速すぎて、ガベージコレクションが間に合わなかったとかかもしれない。

対策

No way to handle failed serialization (non-json) · Issue #137 · golevelup/nestjs · GitHub で説明されていた。

@golevelup/nestjs-rabbitmq@1.15.0 以上で追加された allowNonJsonMessages を true に設定することで JSON として不正なメッセージをハンドラが受け取れるようになる。それを basic.ack で消化してしまったり、basic.reject で削除したり、デッドレターキューに移動することで requeue ループを防ぐ。

@RabbitSubscribe({
    exchange,
    routingKey: [nonJsonRoutingKey],
    queue: 'subscribeQueue',
    allowNonJsonMessages: true,
})

docker container のとめかた

このような Dockerfile を $docker build . -t webapp:latest でビルドした後、$ docker run webapp:latest でコンテナを立ち上げることができる。

# Dockerfile
FROM python

WORKDIR /app

COPY . /app
RUN pip install flask

ENTRYPOINT ["bash", "entorypoint.sh"]
# entorypoint.sh
echo "Starting server..."
python3 webapp.py
# webapp.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

app.run(host="0.0.0.0")

そして、その後停止するためには、$ docker stop をすれば良い。

$ docker stop b42e16eddd7d

kill-docker-container_webapp-exec_1 exited with code 137

... はずだが、すぐ終了せず、10秒経ってから強制終了(exited 137)された。

docker stop とは?

公式ドキュメントによると、以下のように説明されている。

1つまたは複数の実行中コンテナを 停止stop します。
コンテナ内のメイン・プロセスが SIGTERM を受信し、一定期間の経過後、 SIGKILL を送信します。.... (docker stop — Docker-docs-ja 24.0 ドキュメント

SIGTERM, SIGKILL は Unix / Linux でプロセスに対して送られる signal 。

SIGTERM を受け取ったプロセスは、ハンドラがあれば正常終了のための処理を行って停止という動きをできる。
プロセスに signal を送るには、kill コマンドを利用できる。

$ kill -15 { PID (Process ID) }

SIGKILL を受け取ったプロセスは、強制的に終了させられる。ちなみに、ゾンビプロセスは既に終了していることになっているため効かないらしい。もちろん、これも kill コマンドで送信できる。

$ kill -9 { PID (Process ID) }

つまり、exited 137 となったということは、SIGTERM では止まらず 10s 後、SIGKILL によって強制的に終了させられたことを表している。

コンテナ内のメイン・プロセスが SIGTERM を受信し...

docker stop コマンドの説明で引っ掛かるのはここ。docker stop コマンドを実行すると、コンテナの「メイン・プロセス」に対して SIGTERM が送られるらしい。

コンテナのメインプロセスとは何だろう?

コンテナのメインプロセス

コンテナでメインとして実行するプロセスは、 Dockerfile の最後に書かれている ENTRYPOINT か CMD か、あるいは両方によって指定します。 (コンテナ内で複数のサービスを実行 — Docker-docs-ja 19.03 ドキュメント

なるほど、 Dockerfile の一番最後のコマンドがメインプロセスとして扱われるらしい。今回作成した dockerfile では bash entorypoint.sh がメインプロセスとなるはず。

docker container 内でプロセスを見てみると、bash entorypoint.sh は PID 1 を割り当てられていた。

root@5c63c147bb2e:/app# ps 1
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:00 bash entorypoint.sh
root@5c63c147bb2e:/app#

--init プラグを使うと、コンテナ内で PID 1 として使われるべき init プロセスを指定できます。(Docker run リファレンス — Docker-docs-ja 24.0 ドキュメント

リファレンスを漁ってみると、このような説明があった。

メインプロセスは、init プロセスで、PID 1 が割り当てられているということだと思う。

init プロセスは UNIX / Linux のシステムで、ブートローダーによって起動したカーネルが起動するプログラムで、init が全てのプロセスを起動する。そして PID 1 が付与される。

init - Wikipedia

手元にある ubuntu で PID 1 を見ると、確かに init だった。そして /sbin/init は systemd へのシンボリックリンクだった。

$ ps 1
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:00 /sbin/init splash
$ ll /sbin/init 
lrwxrwxrwx 1 root root 20  110  2022 /sbin/init -> /lib/systemd/systemd*

ちなみに、このプロセスを殺そうとしても無駄だ(PID 1 は特別に保護されているらしい)

$  kill -9 1
bash: kill: (1) - Operation not permitted

$ sudo kill -9 1
(何も起きない)

container を止める

最初の docker container を止めるためには、python3 webapp.py のコマンドをメインプロセスにする必要がある。(bash が実行したコマンドは子プロセスになる)

そのためには、bash から別のコマンドを実行する際に exec を付けて実行すれば OK。

# entorypoint.sh
echo "Starting server..."
exec python3 webapp.py

確認してみると、ちゃんと PID 1 が python のコマンドになっている。

root@382c9f96490b:/app# ps 1
    PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:00 python3 webapp.py

しかし、まだちゃんと停止してくれない。
これは、前述の PID が保護されていることによるものだろう。

$ docker stop cea116b31db2

kill-docker-container_webapp-exec_1 exited with code 137

SIGTERM で終了させるためには、--init で init プロセス(メインプロセス)を PID 1 以外にしたり、SIGTERM 用のハンドラを用意したりする。

# webapp.py
from flask import Flask
import signal

def handler(signum, frame):
    print(f"signal {signum} received!")
    exit(0)
    
signal.signal(signal.SIGTERM, handler)

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

app.run(host="0.0.0.0")

このように SIGTERM のハンドラを追加して、docker stop を実行してみるとすぐに終了した。

やったね。

$ docker stop 78a6563659b5
webapp-exec_1  | signal 15 received!
kill-docker-container_webapp-exec_1 exited with code 0

疑問

無事 docker container を止めることができた。しかし、何故メインプロセスが停止した時にコンテナが止まるのか(逆にコンテナを止めるためにメインプロセスを停止させている?メインプロセス自体がコンテナの実態?多分どちらも違う)。

逆にメインプロセスがメモリ不足で終了した際に、コンテナが止まるのはどういう仕組みなのか....

そもそもコンテナを止めるとはどういうことなのか?1
メインプロセスとコンテナの関係とは?

その謎を解明するため、我々調査隊はアマゾンの奥地へと向かった――


  1. もしかすると、私は本当の意味でコンテナを止められていないのかもしれません。

Jest でテスト網羅率(カバレッジ)を計測したい

Jest は Meta(元 Facebook) が保守している、JavaScript のテスティングフレームワーク、らしい。Jest を使うと、コード網羅率(カバレッジ)を簡単に計測できる。

カバレッジを計測することで、テストが不足しているコードが検知できるようになって嬉しくなる。

jestjs.io

コードは長いので隠しとく

テスト対象

// add.ts
export function add(a: number, b: number): number {
    const res = a + b;
    return res;
}
// isEven.ts
export function isEven(num: number): boolean {
    let result = false;
    if (num % 2 === 0) {
        result = true;
    }
    return result;
}

export function isEven2(num: number): boolean {
    if (num % 2 === 0) {
        return true;
    }else {
        return false
    }
}

export function isEven3(num: number): boolean {
    return num % 2 === 0 ? true : false;
}
// toCSV.ts
export function toCSV(recodes: { name: string; age: number }[]): string {
    let header = "name"; header += ", age";
    const csv = recodes.map((row) => `${row.name}, ${row.age}\n`);
    return `${header}\n${csv}`;
}
// dog.ts
class Dog {
    constructor(public name: string){};

    eatBone(foods: string[]): string[] {
        return foods.filter(f => f !== "bone");
    }
}

テストコード

import { describe, it, expect } from "@jest/globals";
import { add } from "./add";
import { isEven, isEven2, isEven3 } from "./isEven";

describe("add()", () => {
    it("3 + 2 = 5", () => {
        expect(add(3, 2)).toEqual(5);
    });
});

describe("isEven", () => {
    it("8 is even", () => {
        expect(isEven(8)).toBeTruthy();
    })
})

describe("isEven2", () => {
    it("8 is even", () => {
        expect(isEven2(8)).toBeTruthy();
    })
})

describe("isEven3", () => {
    it("8 is even", () => {
        expect(isEven3(8)).toBeTruthy();
    })
})

カバレッジを計測する

--coverage オプションをつけて jest を実行すると、このような表示と Istanbul がキレイに成形した html を作ってくれる。

ファイルごとに、Stmts, Branch, Funcs, Lines メトリクスがいい感じに見れる。また、html を開くとテストコードが網羅できてない部分をマークで教えてくれる。すごく良い。

メトリクスについて

ステートメント網羅(Stmts)

ファイルに含まれる命令がどれだけテストで実行されたか。add.ts では 3つ命令があって、それが全て網羅できてる。

一番基本的なメトリクスになると思う。こちらは必ず見たい。

分岐網羅(Branch)

ファイルに含まれている分岐がどれだけテストで実行されたか。isEven.ts では分岐ルートが5つあって、3つだけ網羅できている。

ステートメント網羅だけではパスを網羅できていることまではわからないので、こちらも見ておくとベターか。ただし、分岐が一か所もないコードでは 100% になるので Branch 単体では使えない。

ちなみに、本来の分岐網羅的に isEven() には A, B のパスがある。テストでは「A」しか実行していないが、(num % 2 === 0) が false になるパターンの「B」を通っていないことを指摘されなかった。Jest では B のパスがテストできていないことをチェックできないのか??(明示的に else を書いた isEven2() ではちゃんと指摘してくれている)

関数網羅(Funcs)

ファイルに含まれている、関数やクラスのコンストラクタ、メソッド、やコールバック関数の内どれだけテストで実行されたか。一つも実行されていない(テストが無いので)

ステートメント網羅や条件網羅では、関数が網羅できていることまではわからないので、Stmts, Branch だけでは微妙そうだった場合追加で見ていいかもしれない。

行網羅(Lines)

ファイルに含まれている、命令がある行数がどれだけテストで実行されたか。ほとんどはステートメント網羅と同じ感じになるが、1行に複数の命令があると差ができる。こちらも1行も実行されていない(テストが無いので)

ステートメント網羅の方がより細かく見れるので、行網羅は気にしなくていいかも。

CI パイプラインに組み込む

coverageThreshold を設定することで、カバレッジが閾値を満たしていない場合、jest コマンドの exit code が 0 以外になるので、パイプラインに組み込んで自動でチェックするようにできる。

Jestの設定 · Jest

ただ、以下をどのように設定するべきかがちょっと難しく、コードによって調整する必要がある。

  1. どのメトリクスを使うか
  2. OK とする閾値はいくつにするか

どう設定するのが良さそうか?

軽く調べただけなので、理解がずれてるかもしれない。

1 に関して、簡単に設定するのであれば Stmts だけを見る。+で Branch を見るとより良いと思う。Funcs は何個以上網羅できてなければ失敗(閾値を負の数にすればできる)するようにすれば「一つのデカい関数はテストできてるが、ほかの小さな関数は全くテストされてない」みたいな状況でもコケさせることができる。Lines は Stmts より荒い指標なので見る必要はないと思う。

2 に関しては、下限は 60% くらい。基本的には 70~90% 位が良いみたいな意見をよく見かけた。
ミッションクリティカルなコードは 100% 近くにしてもいいかもしれないが、カバレッジ 100% というのは負担とテストの品質の効率があんまりよくない(大変な割にあまり質は上がらない)とされているっぽいので全ファイルの要求カバレッジを厳しく設定するのは辛そうだ。

追記: 実際に試行錯誤してみたところ、ちゃんとテストを書いていれば Stmts 70%~85% くらいが丁度いい閾値に感じた。75% 位であれば意識してテストコードを書けば余裕でクリアできる。

Branch を Stmts と同じレベルにすると論理的には絶対通らない(可能性は0ではないが、わざわざテストするかここ....?というレベルの if )が、TypeScript の型的にチェックを入れておく必要があるみたいな場合で引っ掛かりやすかった。なので気持ち抑えめにしておくか、Ignoring code for coverage のようなアノテーションをつけて Branch 計測しないようにするみたいなことが必要になるだろう。

Funcs は網羅できてない時点でテストケースが不足していると言えるため、より厳しく設定しても良さそうだった。厳しくしたときの副次的な効果として、一切使われていない関数を教えてくれるというメリットもあり。

感想

最近リグレッションテストが必要な場面が多くなり、ユニットテストが十分あると安心できるなーということに納得できるようになってきました。テストを書き忘れてないか(十分テストできているかまでは保証できない)を自動でチェックするために、カバレッジを CI に組み込むのはかなり有用そうだなーって思いました。

UUID v4 はなぜ暗号的に安全な乱数が使われているんだろう?

UUID は分散システムで一意な Identifier として使えること(= 衝突しない前提で使える)を目的にした ID で、その version 4 はランダムに作るやつ。

この前ふと、uuid v4 をパスワードのような使い方をしていいのか?が気になって調べてみた。

Secure - Cryptographically-strong random values

Node.js を良く触るが uuid モジュールでは、暗号学的に安全なランダムを使ってるよう。

ちなみに、version 7未満では Math.random() が使われることがあり、暗号的に安全じゃないかもしれないらしい(Math.random() はここで少し触れてる 疑似乱数は本当に予測できるのか試したい - Memo:

www.npmjs.com

uuid モジュールは内部的には、crypto.randomUUID() を呼んでいる。crypto は標準的な暗号機能のための API で、node.js の実装は、OpenSSL のラッパーみたいなものらしい。

Crypto | Node.js v21.1.0 Documentation

const crypto = require("crypto");
console.log(crypto.randomUUID());

randomFill() のここで乱数を作ってそう。

  const job = new RandomBytesJob(
    kCryptoJobAsync,
    buf,
    offset,
    size);

node/lib/internal/crypto/random.js at v16.x · nodejs/node · GitHub

更にたどっていくと...

多分ここっぽいです。OpenSSL の RAND_bytes() を呼んでますね。

bool RandomBytesTraits::DeriveBits(
    Environment* env,
    const RandomBytesConfig& params,
    ByteSource* unused) {
  CheckEntropy();  // Ensure that OpenSSL's PRNG is properly seeded.
  return RAND_bytes(params.buffer, params.size) != 0;
}

node/src/crypto/crypto_random.cc at v16.x · nodejs/node · GitHub

OpenSSL の wiki を眺めると RAND_bytes には software 的な乱数と、hardware 的に作られる物があるみたい。
どっちを使っているのかは分からないが、エントロピーが溜まるまで待ってるということは、hardware 的な乱数を使ってるような気はする(自信ない)。

Random Numbers - OpenSSLWiki

どこから呼んでるのかは全く分からなかったが、 SetEngine から engine を設定することで RAND_bytes がハードウェアを使ってくれるようになるみたいなので、恐らく使えるときはハードウェアを使おうとするとかなんじゃないかと想像してる。 C++ 追うの辛すぎひ。

node/src/crypto/crypto_util.cc at v16.x · nodejs/node · GitHub

とりあえず、node.js の uuid モジュールの v4 はパスワード的な使い方をしても問題なさそう。(※ すべての実装に暗号的強度があるとは言えないので、uuid の実装は必ず調べる必要がある)

なぜ UUID v4 の生成に暗号的乱数を使ってるのか?身長・本名や年齢・誕生日、出身地や大学、年収は?

UUID は用途上、暗号に使うわけではなく分散システムでの ID が目的なので、衝突しにくさがあれば別にセキュアである必要はない気がするけど、どうしてわざわざ暗号的に強い乱数を使って作ってるのだろう....。

調べた結果........

わかりませんでした!!

予想

「暗号論的擬似乱数生成器 の wikipedia 」の要求仕様の項目では、2つの仕様が満たす必要があるとあった。 1を満たすということは、単純に良質な乱数であるともいえる。2を満たすということは乱数に周期性や規則性が無いといってもいいと思う。

  1. 統計的無作為試験に合格するほど、無作為な乱数になってる
  2. 初期状態や途中の状態が攻撃者に明らかになっても破られないこと

この2つを満たした乱数は、「予測」し辛いだけではなく「衝突」もしにくいからという気がしてる。また、乱数に周期性があると2つの生成器の周期が重なってしまった場合、ほぼ100% 衝突するようになってしまう。2を満たしていれば周期性が無いはずなのでそのようなことも起きない。

ということは、衝突耐性を持たせるために暗号論的擬似乱数生成器を選んでいる....

ってことじゃないかと半ば無理やり納得しておわります。

暗号論的擬似乱数生成器 - Wikipedia



ところで、UUID といえば何故か奇妙なハイフンで区切られているが、なぜそんな形式になったんだろう。それが気になって仕方なかったので以下で考えてみた。よかったら見てね。

burion.net

JavaScriptのProxyを使って、サードパーティーのクラスを書き換えずにログを出力したい

以下のようなサードパーティのクラスがあって、メソッドに渡された引数や結果をログに残すためにインターセプトしたい。

自分らが管理しているクラスなら、デコレーターを作るのがいいと思うが、サードパーティーのクラスを書き換えるのはちょっと嫌だ(保守性の低下や動作が壊れるリスク的な観点で)。

またクラス構造がネスト・継承されている場合、デコレーターが複雑になり保守もしにくくなりそうだなーと思っていた。

// thirdPartyClass.ts
interface Data {
    [Key: string]: number;
}

export class ThirdPartyClass {
    private data: Data = {};
    getData(key: string): number {
        return this.data[key];
    }

    setData(key: string, value: number): void {
        this.data[key] = value;
    }

    reset(): void {
        this.data = {};
    }
}

Proxy を使う

JavaScript には Proxy という、オブジェクトのプロキシを作成するコンストラクタが標準である。関数呼び出しであれば、 apply ハンドラーを使うことでインターセプトできる。

Proxy - JavaScript | MDN

import { ThirdPartyClass } from "./thirdPartyClass";

function wrap<M extends string, I extends Record<M, Function>>(instance: I, methods: M[]) {
    const result = {} as Pick<I, M>;
    for (const key of methods) {
        result[key] = new Proxy<I[M]>(instance[key], {
            apply: (target: I[M], _: any, argArray: any[]) => {
                const res = target.apply(instance, argArray);
                console.log(`args: ${argArray}, res: ${res}`);
                return res;
            },
        });
    }
    return result;
}

const obj = new ThirdPartyClass();
const wrappedObj = wrap(obj, ["getData", "setData"]);

wrappedObj.setData("my-key", 19); // args: my-key,19, res: undefined
wrappedObj.getData("my-key"); // args: my-key, res: 19

Generics について

また、Generics が少し複雑なのでコメントを入れてみる。
この generics によって、定義されていないメソッド名を指定したり、関数以外のプロパティが指定されると型エラーになる。

こんな複雑な generics も書けるのか~と驚いた。

function wrap<
    M extends string, // 制約: M は string
    I extends Record<M, Function>, // 制約: I[M] は Function
>(
    instance: I,
    methods: M[], // method=["add", "print"] の時 type M = "add" | "print"
) {
    /* 
        Pick では渡された I の0個以上のプロパティ M の型を作る

        # 例
        type I = {
            add: (a: number, b: number) => number;
            sub: (a: number, b: number) => number;
            print: (a: string) => string;
        };
        type M = "add" | "print";

        Pick<I, M> の型は以下になる
        {
            add: (a: number, b: number) => number;
            print: (a: string) => string;
        };
    */
    const result = {} as Pick<I, M>;
}

参考

今回のような複雑な generics がいくつか説明されていました。

www.digitalocean.com

JavaScript でクラスのメソッド名を取得する方法 - プロトタイプチェーンを利用して列挙不可能プロパティを取得する

TypeScript でクラスをインスタンス化して、そのメソッド名をループで取得したかったが簡単にできなくてハマったので残しておく。

class MyClass {
    property = 24;
    method(a: number, b: number): number {
        return a + b;
    }
}

const obj = new MyClass();
console.log(Object.keys(obj));

for (const key in obj) {
    console.log(key);
}
/*
'property'  // method がない!
*/

JavaScript ではプロトタイプ継承という仕組みがある。ザックリいうと、MyClass のインスタンス objmethod は持っていないのだが、obj.method とアクセスしたときにチェーンされている prototype をさかのぼり実体の MyClass.prototype.method が検索されアクセスできるという感じの機能。

継承とプロトタイプチェーン - JavaScript | MDN

for...in 文は、キーが文字列であるオブジェクトの列挙可能プロパティすべてに対して、継承された列挙可能プロパティも含めて反復処理を行います (Symbol がキーになったものは無視します)。 for...in - JavaScript | MDN

プロトタイプチェーンをふと思い出したので、これが原因か...?と思ったが key..in 文はプロトタイプチェーンをたどってくれるらしい。

「列挙可能プロパティ」というのが気になる Object.getOwnPropertyDescriptor を使えば確認できるようなので見てみる。

console.log(Object.getOwnPropertyDescriptor(obj, "property"));
// { value: 24, writable: true, enumerable: true, configurable: true }

console.log(Object.getOwnPropertyDescriptor(obj, "method")); // --> undefined
console.log(Object.getOwnPropertyDescriptor(MyClass.prototype, "method")); // 実体を見る必要がある
/*
{
  value: [Function: method],
  writable: true,
  enumerable: false,  // 列挙不可能プロパティに設定されていた!
  configurable: true
}
*/

obj.property は列挙可能プロパティだが、obj.method は列挙不可能なので、出てこなかったみたいだ。

結論

Object.getPrototypeOf() でプロトタイプにアクセスし、そこから Object.getOwnPropertyNames() で得られる。 Object.getOwnPropertyNames()は列挙不可能プロパティも取得できるという特性を持っているので取得が可能になる。

class MyClass {
    property = 24;
    method(a: number, b: number): number {
        return a + b;
    }
}

const obj = new MyClass();

console.log(Object.getOwnPropertyNames(obj)); // [ 'property' ]
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(obj))); // [ 'constructor', 'method' ]

継承している場合

また、何重にも継承をしていると、プロトタイプチェーンが長くなるので再帰的にアクセスする必要があったりする。これで親の親クラスにあるメソッド名 'methodOfSuperParent' を得られた。色々関係ないメソッド名まで出てきてしまうので、もう少し工夫は必要そうではあるけれど。

class SuperParentClass {
    methodOfSuperParent(a: string, b: string): string {
        return `${a} * ${b}`;
    }
}

class ParentClass extends SuperParentClass {
    methodOfParent(a: string, b: string): string {
        return `${a} + ${b}`;
    }
}

class MyClass extends ParentClass {
    property = 24;
    method(a: number, b: number): number {
        return a + b;
    }
}

const obj = new MyClass();

let keys = Object.getOwnPropertyNames(obj);
let prototype = Object.getPrototypeOf(obj);
while (true) {
    keys = keys.concat(Object.getOwnPropertyNames(prototype));
    prototype = Object.getPrototypeOf(prototype);
    if (!prototype) {
        break;
    }
}

console.log(keys);
/*
[
  'property',             'constructor',
  'method',               'constructor',
  'methodOfParent',       'constructor',
  'methodOfSuperParent',  'constructor',
  '__defineGetter__',     '__defineSetter__',
  'hasOwnProperty',       '__lookupGetter__',
  '__lookupSetter__',     'isPrototypeOf',
  'propertyIsEnumerable', 'toString',
  'valueOf',              '__proto__',
  'toLocaleString'
]
*/

DRY原則と直交性、仕様変更時の影響を考えてみる

最近、達人プログラマー第二版を読んで、特に第二章の前半で話題になっている「DRY原則」と「直交性」が印象的であったので、考えてみる。

良い設計とは?

良い設計は悪い設計よりも変更しやすい

第2章の初めの Tips である。確かにその通りだとおもう。
良い設計とは?と聞かれるとパッと答えるのは難しいが「悪い設計とは?」と聞かれたときには以下のようなものが思い浮かぶ。

  • 文字通り暗号みたい(code)で読むのが難しい
  • 同じ処理があちこちに書かれている
  • 機能を追加・変更したときに、一見関係ない部分が壊れる

他にも例は挙げられると思うが、概ね同意してもらえるはず。
一番上は可読性的な意味で例えばリーダブルコードのような本があったりする。2番目は「DRY原則」が、3番目が「直交性」に対応していると思う。

DRY 原則

DRY 原則とは Don't Repeat Yourself の略で、「繰り返しを避けろ」という原則。

これに違反している例で真っ先に思いつくのは、以下のように全く同じコードをコピペしまくった以下のようなコードだと思う。

※他にも 変数の知識の重複による不整合を防ぎたい(DRY原則とオブジェクト指向) - Memo: で書いた知識の二重化があり変数間の不整合の原因になる

unitPrice * quantity の計算や、商品ごとの金額の表示部分は繰り返されてしまっている。こういったコードを見ると、関数化・メソッド化をしたり、クラス作成をしたくなる。抽象的なコードを作成しそれを使いまわすことで繰り返しを排除できる。

class Item {
    constructor(public name: string, public unitPrice: number, public quantity: number) {}
}

const apple = new Item("Apple", 150, 1); // リンゴを1個
const orange = new Item("Orange", 90, 2); // みかんを2個
const pork = new Item("Pork", 2, 200); // 豚肉200g

// それぞれの金額と合計金額を表示する
console.log(`${apple.name} x ${apple.quantity} = ${apple.unitPrice * apple.quantity}円`);
console.log(`${orange.name} x ${orange.quantity} = ${orange.unitPrice * orange.quantity}円`);
console.log(`${pork.name} x ${pork.quantity} = ${pork.unitPrice * pork.quantity}円`);

const totalPrice =
    apple.unitPrice * apple.quantity +
    orange.unitPrice * orange.quantity +
    pork.unitPrice * pork.quantity;

console.log(`合計 ${totalPrice}円`);

/*
  Apple x 1 = 150円
  Orange x 2 = 180円
  Pork x 200 = 400円
  合計 730円
*/

例えば、以下のようにリファクタリングできる。unitPrice * quantity の計算を calcPrice() メソッドで行うようにし、商品ごとの金額表示を printeLine() メソッドで行うようにした。これで各処理は1か所だけになった。ついでにメソッドの呼び出しを1か所にするために items という配列で持つようにした。

これで二重化を排除することができた。しかし、これは本当にいいコードだろうか...?

class Item {
    constructor(public name: string, public unitPrice: number, public quantity: number) {}

    calcPrice(): number {
        return this.unitPrice * this.quantity;
    }

    printLine(): void {
        console.log(`${this.name} x ${this.quantity} = ${this.calcPrice()}円`);
    }
}

const items = [
    new Item("Apple", 150, 1), // リンゴを1個
    new Item("Orange", 90, 2), // みかんを2個
    new Item("Pork", 2, 200), // 豚肉200g
];

// それぞれの金額と合計金額を表示する
for (const item of items) {
    item.printLine();
}

const totalPrice = items.reduce(
    (totalPrice: number, item: Item) => totalPrice + item.calcPrice(),
    0,
);

console.log(`合計 ${totalPrice}円`);

仕様に変更があるとどうなるか?

ソフトウェアにおいて仕様が変わることはよくあることだ。もし、さっきの二重化を排除したコードで以下のような仕様にしてほしいといわれたとする。

  • 商品と合計金額の後ろに (税込み)と追加してほしい

これは二重化をを排除しているので簡単だ。 Item の printLine と、一番最後の合計金額を表示している部分の変更だけで済む。

では、次のような仕様変更があった場合どうすればいいだろうか?

  • 肉の商品は表示には「g」単位を入れてほしい

これは少し難しいと思う。メソッドを共通化したため calcPrice の中で「果物」なのか「肉」なのか判別が必要になってしまう。 とりあえず ItemTypes を追加して、メソッドの中で分岐を作成することにした...。

enum ItemTypes {
    Meat = "Meat",
    Fruit = "Fruit",
}

class Item {
    constructor(
        public name: string,
        public type: ItemTypes,
        public unitPrice: number,
        public quantity: number,
    ) {}

    calcPrice(): number {
        return this.unitPrice * this.quantity;
    }

    printLine(): void {
        let unit;
        if (this.type === ItemTypes.Meat) {  // 👈 肉専用の分岐を追加した
            unit = "g";
        }
        console.log(`${this.name} x ${this.quantity}${unit} = ${this.calcPrice()}円`);
    }
}

const items = [
    new Item("Apple", ItemTypes.Fruit, 150, 1), // リンゴを1個
    new Item("Orange", ItemTypes.Fruit, 90, 2), // みかんを2個
    new Item("Pork", ItemTypes.Meat, 2, 200), // 豚肉200g
];

// それぞれの金額と合計金額を表示する
for (const item of items) {
    item.printLine();
}

const totalPrice = items.reduce(
    (totalPrice: number, item: Item) => totalPrice + item.calcPrice(),
    0,
);

console.log(`合計 ${totalPrice}円`);

今はまだマシかもしれないが、このような変更が繰り返されていくと if の大量発生によりクラスが肥大化することは想像できる。2か月後以下のように変更して欲しいと言われるかもしれない。

  • 肉の単価は 100g あたりにしてほしい
  • 肉を 1kg 以上買ったときに、10% 値引きしてほしい
  • 果物は最低3つ以上でないと購入できないようにしてほしい

更に、この変更で良くないのは今回関係ない「リンゴ」と「オレンジ」も変更する必要があり、壊れてしまう可能性があることだ。

そして実は先ほどのコードでは、「リンゴ」と「オレンジ」の表示が壊れてしまっている!(printLine で unit = "" と初期化する必要があった)。

Apple x 1undefined = 150円
Orange x 2undefined = 180円
Pork x 200g = 400円
合計 730円

記事の冒頭で「良い設計は悪い設計よりも変更しやすい」と引用した。
「肉」についての仕様を変更しただけで、その変更とは関係ない「果物」が壊れてしまうというのは変更しにくい、悪い設計といえると思う。

この例のようにコードの二重化を排除したとき、逆に変更に弱くなってしまうことがある。これは二重化をなくしたときの副作用として依存関係が生まれるからだと思う。「肉」の処理を変更するために Item.printLine() を修正したが。以下のような依存関係があることで Item.printLine() に依存していた「果物」まで影響を受けてしまったことが原因である。

肉も果実も同じメソッドに強く依存している

直交性

ある変更をしたときに 関係ない部分に影響を及ぼしてしまう可能性がある 状態を「直交していない」状態であると達人プログラマーでは述べられている。逆に、関係ない部分のそれぞれが独立していたり、依存が少なかったり、疎結合になっている状態が直交している良い状態である。

個人的に、二重化を排除することよりも直交させることが難しいと思う。依存関係というのは目に見えにくいこと。そして二重化を排除すると必ず依存ができるが、どのように依存させるかの判断がむずかしいからだ。

前の例では、商品といっても「肉」と「果物」で扱いが異なりそうであることが分かった。この二つを別のクラスとして分離してみる。

abstract class Item {
    abstract name: string;
    abstract unitPrice: number;
    abstract quantity: number;

    calcPrice(): number {
        return this.unitPrice * this.quantity;
    }

    printLine(): void {
        console.log(`${this.name} x ${this.quantity} = ${this.calcPrice()}円`);
    }
}

// 👇 一つだった Item クラスを Fruit  と Meat に分離した。
class Fruit extends Item {
    constructor(public name: string, public unitPrice: number, public quantity: number) {
        super();
    }
}

class Meat extends Item {
    constructor(public name: string, public unitPrice: number, public quantity: number) {
        super();
    }
}

const items: Item[] = [
    new Fruit("Apple", 150, 1), // リンゴを1個
    new Fruit("Orange", 90, 2), // みかんを2個
    new Meat("Pork", 2, 200), // 豚肉200g
];

// それぞれの金額と合計金額を表示する
for (const item of items) {
    item.printLine();
}

const totalPrice = items.reduce(
    (totalPrice: number, item: Item) => totalPrice + item.calcPrice(),
    0,
);

console.log(`合計 ${totalPrice}円`);

上のコードは「DRY 原則」の一番最後にあるものと動きは同じだが、‘Item‘ は抽象クラスとして定義して、それを Meat と Fruit が実装している。

さて、このコードに同じ仕様変更の依頼が来たとする。

  • 肉の商品は表示には「g」単位を入れてほしい

今回は Meat クラスを変更するだけでよい。かなりシンプルだし、Fruit や Item は 100% 壊れないと自信を持てる。

class Meat extends Item {
    constructor(public name: string, public unitPrice: number, public quantity: number) {
        super();
    }

    printLine(): void {
        // 👇 ここに追加するだけ
        console.log(`${this.name} x ${this.quantity}g = ${this.calcPrice()}円`);
    }
}

今回の依存関係は以下のようになっている。Meat に依存しているのは Pork だけなので他に影響を与えず変更することができた。 このように依存関係を制御することで、コードの二重化を防ぎつつ変更しやすさを損なわないようにできる。

依存関係を整理し肉と果実を直交するようにした

更に、この3つの仕様変更を行う時どうなるかを試してみる。

  • 肉の単価は 100g あたりにしてほしい
  • 肉を 1kg 以上買ったときに、10% 値引きしてほしい
  • 果物は最低3つ以上でないと購入できないようにしてほしい

以下のような変更だけで済んだ。比較的にシンプルに追加できたと思う。「DRY 原則」の最後のコードではこれほどシンプルに追加するのは難しいはずだ。

もし「XX は △△ じゃないと購入できない」や「○○ を □□ 以上購入で XX % 値引き」というパターンがよく出てくるようであれば、それを購入ポリシーや割引ポリシーとして Fruit や Meat クラスから分離すると良さそう(ストラテジーパターンというデザインパターン)。

更に表示用の printLine も ItemPrinter クラスとして分離しておいた方がいいかもしれない。商品の表示の変更は料金計算などと比べると、変更頻度が高いことが予想できる。実際の表示先は紙のレシートになるかもしれないが、そのときに他を壊してしまうためだ。

abstract class Item {
    abstract name: string;
    abstract unitPrice: number;
    abstract quantity: number;

    calcPrice(): number {
        return this.unitPrice * this.quantity;
    }

    printLine(): void {
        console.log(`${this.name} x ${this.quantity} = ${this.calcPrice()}円`);
    }
}

class Fruit extends Item {
    constructor(public name: string, public unitPrice: number, public quantity: number) {
        super();

        // 👇 果物は最低3つ以上でないと購入できない
        if (!this.isBuyable()) {
            throw new Error("購入できません!");
        }
    }

    private isBuyable(): boolean {
        return this.quantity >= 3;
    }
}

class Meat extends Item {
    constructor(public name: string, public unitPrice: number, public quantity: number) {
        super();
    }

    calcPrice(): number {
        const pricePer100g = this.unitPrice / 100; // 👈 肉の単価は 100g あたり
        const grams = this.quantity;

        const price = pricePer100g * grams;
        return this.discount(price);
    }

    private discount(price: number): number {
        // 👇 肉を 1kg 以上買ったときに、10% 値引き
        const discountRate = 0.1;
        const minDiscountableGrams = 1000;
        if (this.quantity >= minDiscountableGrams) {
            return price * (1 - discountRate);
        }
        return price;
    }

    printLine(): void {
        console.log(`${this.name} x ${this.quantity}g = ${this.calcPrice()}円`);
    }
}

const items: Item[] = [
    new Fruit("Apple", 150, 4), // リンゴを4個
    new Fruit("Orange", 90, 6), // みかんを6個
    new Meat("Pork", 200, 1200), // 200円/100g の豚肉を1.2kg
];

// それぞれの金額と合計金額を表示する
for (const item of items) {
    item.printLine();
}

const totalPrice = items.reduce(
    (totalPrice: number, item: Item) => totalPrice + item.calcPrice(),
    0,
);

console.log(`合計 ${totalPrice}円`);

感想

このように二重化を防ぎつつ依存関係を制御することで変更しやすいコードにできる。これは SOLID 原則やデザインパターン、クリーンアーキテクチャ(特に安定依存の原則)の考え方が参考にできそうだなと思った。また、そもそもどのようなドメインをコードとして記述しているのかが分からないと、どこをどのようにするべきかの判断が難しかったりするので、やっぱりドメイン知識も重要になってきそう。

他にもこの辺の本は結構勉強になった。オブジェクト指向プログラミングの全体像的な部分では『Clean Architecture』が、具体的な活用方法については『 Java言語で学ぶデザインパターン入門』が良かった。

TypeScript で stripe-node API のレスポンスから requestId や statusCode を取得したい

TypeScript + stripe-node で API の requestId や statusCode 等を取得したい時、API の返り値の lastResponse から得られる。しかし、 Property 'lastResponse' does not exist on type 'Session'. と TypeScript に怒られることがある。

var customer = await stripe.customers.create();
console.log(customer.lastResponse.requestId);  // Type Error !

結論

実は lastResponse は存在するのだが型定義が間に合っていなかっただけ。
stripe-node を 最新にアップグレードすれば動く。

理由があって上げにくい場合は、any にキャストしてしまって、無理やり lastResponse 呼び出してしまえば一応は使える。

github.com

疑似乱数(xorshift)の内部状態と生成される数値の関係について考えてみる

※細かく調べたり検証を行ったわけではないので、話半分に見てください

以下の記事で疑似乱数(xorshift)について考えていた時にある疑問が浮かんだので、それについて書いてみる。

buri83.hateblo.jp

実験のしやすさのため、周期が 216 - 1 の xorshift を準備する。このアルゴリズムで乱数を生成するときは内部状態 xs にゴニョゴニョと操作をしたものを返す。そしてその出力はそのまま次の内部状態になる。

当然だが xs が同じ場合、生成される乱数は同じになる。

unsigned short xs = 17;
unsigned short xor16(){
    xs ^= xs << 7;
    xs ^= xs >> 9;
    xs ^= xs << 8;
    return xs;
}

つまり、例えば 1 --> 4 --> 2 と一度出てきた疑似乱数アルゴリズムでは、1 --> 34 --> 5 の並びは存在しない 。1 の次は必ず 4 だし、その次は絶対 2 が出てくる。何度乱数を作っても 1 の次に 3 が出ることはない。雑に言うと順番がゴチャゴチャなカウンタみたいだ...。さらに、これは seed (内部状態の初期値)がいくら変わったところで生成される値の順番は変わらないということも言えそう。

このようなプログラムで検証もしてみた。周期(同じ値が出るまでの生成回数)は 65,535 で「0」は一度も現れなかった。0 は何度シフトしても 0 なので当然だろう。なので、このアルゴリズムの周期は 216 - 1 ということになる。(疑似乱数の周期でよく 2n -1 になるのは、出ない値が存在するからなのかな?)

#include <stdio.h>

unsigned short xs = 17;
unsigned short xor16(){
    xs ^= xs << 7;
    xs ^= xs >> 9;
    xs ^= xs << 8;
    return xs;
}

int main(void){
    unsigned short res[65536];
    for(int i=0; i<65536; i++) res[i] = 0;
    
    int r = 0;
    for(int i=0; i<65536; i++){
        r = xor16();
        if (res[r] != 0){
            printf("Duplicate !! r=%d res[r]=%d i=%d\n", r, res[r], i);
        }
        res[r]++;
    }

    printf("res[0]=%d\n", res[0]);
}

/*
# 実行結果
Duplicate !! r=40341 res[r]=1 i=65535
res[0]=0
*/

出てくる値が無秩序だったとしても、「生成した約6.5万個の乱数の中に重複した値は存在しない」というのは乱数としては違和感があるなぁ。

内部状態と出力を異なる bit 数にしてみる

先程の xor16() は内部状態が 16bit で、生成する値も 16bit だった。これでは1周期中に同じ値はでなさそう。

内部状態は 16bit のまま、生成する値が 8bit (内部状態よりも小さい)になると、1周期中に 28 (216 / 28 で) くらいは同じ値が登場するんじゃないか?という気がするので試してみる。

#include <stdio.h>

unsigned short xs = 17;
unsigned char xor8(){
    xs ^= xs << 7;
    xs ^= xs >> 9;
    xs ^= xs << 8;
    return xs & 0x00ff;  // 下位 8bit を取り出す
}


int main(void){
    unsigned int res[256];
    for(int i=0; i<256; i++) res[i] = 0;
    
    unsigned char r = 0;
    for(int i=0; i<65535; i++){
        r = xor8();
        res[r]++;
    }

    for(int i=0; i<256; i++){
        printf("res[%d]=%d\n", i, res[i]);
    }
}

/*
# 実行結果
res[0]=255
res[1]=256
res[2]=256
res[3]=256
...
res[253]=256
res[254]=256
res[255]=256
*/

予想通り、1周期中に 256回( 28 回)同じ値が出るようになった。res[0] が少ないのは「0」が出ないからだろうだが、もともとは 0 は一度も出なかったことを考えると偏りは改善されているとも言えそう。

また、最初の xor16() では 216 回生成した時に重複することはなかったが、xor8() では部分的に偏りができた。以下は 256 回生成した結果だが、「144」は 5 回もでているし、逆に「3」は 1 回も現れていない。

149, 62, 69, 205, 16, 65, 224, 201, 54, 95, 142, 251, 13, 190, 189, 15, 185, 125, 216, 231, 18, 42, 13, 175, 49, 251, 114, 30, 122, 125, 155, 22, 85, 233, 27, 162, 226, 171, 93, 210, 192, 130, 161, 122, 2, 59, 209, 39, 203, 112, 47, 236, 162, 213, 61, 196, 238, 136, 200, 158, 224, 214, 190, 230, 244, 225, 60, 112, 24, 122, 59, 233, 117, 142, 209, 18, 28, 27, 215, 5, 148, 170, 171, 21, 228, 158, 204, 203, 91, 176, 175, 12, 42, 19, 167, 105, 137, 79, 189, 62, 45, 163, 20, 44, 9, 234, 134, 136, 160, 176, 148, 159, 124, 126, 28, 119, 250, 180, 205, 97, 165, 26, 46, 70, 28, 15, 152, 165, 99, 204, 182, 186, 133, 25, 143, 61, 222, 165, 101, 142, 129, 110, 45, 243, 40, 109, 197, 62, 85, 129, 117, 166, 143, 66, 126, 98, 23, 172, 234, 227, 35, 250, 160, 130, 193, 82, 124, 51, 233, 125, 136, 219, 83, 230, 217, 124, 56, 46, 100, 5, 167, 127, 135, 29, 142, 249, 76, 76, 121, 174, 197, 61, 212, 226, 157, 11, 218, 208, 198, 194, 151, 44, 74, 91, 177, 111, 157, 6, 31, 142, 187, 61, 234, 178, 159, 90, 100, 113, 128, 209, 92, 104, 114, 77, 199, 95, 191, 111, 211, 114, 118, 20, 121, 246, 255, 118, 89, 204, 140, 169, 50, 124, 19, 177, 39, 171, 88, 81, 228, 154, 143, 126, 111, 146, 130, 179,

当然、216 - 1 という周期があるので周期性がなくなったわけではないが、1通りの乱数列から256通りの乱数列になり、より乱数っぽさは増した気がする。

このような使い方が最適なのかは良くわからないが、「内部状態」と「出力」の bit 数を変えていることはよく見かける。JavaScript (V8)の Math.random() は以下のようになっているらしい(There’s Math.random(), and then there’s Math.random() · V8)。

uint64_t state0 = 1;
uint64_t state1 = 2;
uint64_t xorshift128plus() {
  uint64_t s1 = state0;
  uint64_t s0 = state1;
  state0 = s0;
  s1 ^= s1 << 23;
  s1 ^= s1 >> 17;
  s1 ^= s0;
  s1 ^= s0 >> 26;
  state1 = s1;
  return state0 + state1;
}

こちらは 128bit(64bit * 2)の内部状態をもっており、関数の出力は 64bit になっている。これにより、この2つの問題をマシにしているかもしれない。

  • 0 が出ない問題
  • ある値の次に出てくる乱数が固定になってしまう問題

また、周期が長いといえばメルセンヌ・ツイスタがあるが、ある程度の bit 数を出力したい & ランダムのバリエーションを増やしたいというニーズがあって作られたのかなと思った。しらんけど。(他にもメリットは沢山あるはず)

いろいろ考えてみたが正直な所、こういう乱数とかの細かい問題について、Amazon で本を調べても出てこなかったり、ググり方が分からないので実際どうなのかは分からないまま...

おわり。

疑似乱数は本当に予測できるのか試したい

世の中には2種類の人間がいる... という文句、妙に説得力がありそうで、この後主語もスケールも大きい名言が続きそうなやつ。
人間だけではなく疑似乱数についても、2種類に分けられるらしい。

「世の中には2種類の疑似乱数がある、次の出力を予測できるか出来ないかのどちらかだ」 1

あるサイトに登録したときメールで、https://example.com/register/?token=blquu5jrdrmd のようなユーザー認証用のリンクが送られてくることがある。通常、token にはその人以外が知りえない(総当りしようとすると数年〜数千年位じゃ終わらない)ようなランダムな文字が渡されている。なのでこのリンクをクリックして開いた人は、そのメールアドレスの所持者であるということを証明できる。もし、この token が簡単に予測できるとしたら、この「ユーザー認証」自体が成り立たなくなる。2

この token はおそらく自動で生成しているが、これはコンピューターが計算で求めた「疑似乱数」を元にしている。その疑似乱数には「暗号論的擬似乱数生成器」と呼ばれる、出力される乱数の予測が困難なものと、そうでないものがある。

例えば Python の random モジュールは予測可能な乱数であるので、もしこれを使って例の token を作ってしまうと認証が安全なものとは言えなくなってしまう。

メルセンヌツイスタは完全に決定論的であるため、全ての目的に合致しているわけではなく、暗号化の目的には全く向いていません。
random --- 擬似乱数を生成する — Python 3.12.0 ドキュメント

import random
r = random.random()

また、JavaScript では Math.random を利用できる。これはランタイムによって実装が異なるとは思うが、Node.js や Chrome で採用されている V8 では、xorshift128+ が使われているよう。Firefox や Safari も同じアルゴリズムになっているっぽい。 そして、こちらもメルセンヌ・ツイスタ同様に、予測可能(暗号学的に安全ではない)乱数なので予想できてしまうとまずい用途には利用しちゃいけない。

we decided to reimplement Math.random based on an algorithm called xorshift128+
(中略)..even though xorshift128+ is a huge improvement over MWC1616, it still is not cryptographically secure.
There’s Math.random(), and then there’s Math.random() · V8

const r = Math.random()

Python では secrets というモジュールを利用することで、暗号学的に安全な乱数が作成できるので、こちらを使う必要がある。言語によって色々な「暗号学的に安全な乱数」を生成できるモジュールが準備されているはずなので、用途に応じて使い分ける。3

secrets モジュールを使って、パスワードやアカウント認証、セキュリティトークンなどの機密を扱うのに適した、暗号学的に強い乱数を生成することができます。
secrets --- 機密を扱うために安全な乱数を生成する — Python 3.12.0 ドキュメント

import secrets
r = secrets.token_hex(8)

python の secrets モジュールの実装を追ってみると、os.urandom() を元に乱数を作成していた。Linux 環境では getrandom() というシステムコールが呼び出されるそう。
getrandom() は /dev/random という疑似デバイスから乱数を取得する。これはデバイスドライバー等の入出力から得られた情報を環境ノイズとして集めて、「真の乱数」として使えるようにすることを目的にしているものらしい。

/dev/random - Wikipedia

環境ノイズを元に乱数を生成するものの他に、暗号学的に安全な乱数の生成の方法として暗号学的ハッシュ関数に通すやりかたもあるらしい。

暗号学的に安全ではない乱数は、本当に予測できるのか?

メルセンヌ・ツイスタや xorshift は予想できて、環境ノイズから作られた乱数は予想できないというのは本当なのか?
暗号学的に弱い乱数が予測できると言われているのは、出力には周期性があるためで何度か乱数を出力しているといずれ、昔出てきた乱数のパターンが現れるからであるそう。

例えば、先程見た乱数生成アルゴリズムの周期は以下のようになっている。

  • メルセンヌ・ツイスタ --> 219937 - 1
  • xorshift128+ --> 2128 - 1

xorshift128+ では例えば最初に「192 → 41 → 12」の順で出力された後、2128 - 1 回後にはまた「192 → 41 → 12」が出てくるという意味。

ちょっと128bit だと時間的に大変そうなので、32bit バージョンの xorshift で試してみた。
周期は 232 - 1なので、関数の返り値が 232 - 1 回後も全く同じになるのかを見てみる。

※ 実装は Xorshift - Wikipedia からほぼコピペ

#include <stdio.h>

unsigned int xor32(void) {
  static unsigned int y = 2463534242;
  y = y ^ (y << 13);
  y = y ^ (y >> 17);
  y = y ^ (y << 5);
  return y;
}

int main(void){
    unsigned int r1 = xor32();
    printf("r1 is %u\n", r1);  // 723471715

    for(long i=0; i<4294967295 - 1; i++){
        xor32();
    }

    unsigned int r2 = xor32();
    printf("r2 is %u\n", r2);  // 723471715
}

232 - 1 回後に全く同じ数字が出てきた!当然なのだろうけどおもしろい!!

つまり、もし最初の例の token を生成するのに使用した乱数に周期性がある場合、何度か token を作らせてみてその並び順から現在の乱数の状態を予測できる。その状態と利用しているアルゴリズムがわかれば、そこから次に出力される乱数がバレてしまう。

以降は2種類の疑似乱数生成器の出力を眺めてみる。

暗号論的擬似乱数生成器

先ほど紹介した、Python の secret モジュールを使い、0~23 の乱数を100個生成してみた。
数列を眺めても良くわからないのでグラフで見てみる。これだけで完全にランダムだと言い切るのは無理はあるけれど、なんとなく出力に偏りはなさそうに見える。(ランダム性の証明ってできるのか?)

import secrets
from matplotlib import pyplot as plt

fig = plt.figure()

r = []
for i in range(100):
    r.append(secrets.randbelow(24))
    print(r[-1], end=", ")
    
plt.yticks([0, 8, 16, 23])
plt.plot(r, "o-")
plt.show()
8, 20, 1, 20, 2, 11, 13, 4, 21, 18, 2, 5, 5, 14, 13, 4, 5, 11, 9, 6, 7, 14, 5, 22, 4, 2, 3, 22, 20, 3, 18, 17, 23, 14, 8, 12, 9, 15, 12, 16, 22, 17, 9, 11, 10, 0, 9, 5, 3, 15, 12, 19, 6, 16, 6, 11, 13, 16, 10, 13, 23, 0, 8, 18, 3, 17, 4, 3, 15, 7, 11, 0, 15, 11, 9, 7, 16, 18, 13, 9, 15, 16, 8, 15, 20, 18, 20, 11, 3, 0, 15, 10, 12, 5, 0, 12, 18, 15, 14, 10,

暗号論的擬似乱数生成器で作成した乱数列

周期が極端に短い疑似乱数生成器

ついでに、線形合同法という疑似乱数生成アルゴリズムを使い周期がかなり短い乱数を作ってみて、どのように出力が出てくるのかを眺めてみる。線形合同法というアルゴリズムを使い、周期が24というかなり短い周期の疑似乱数生成器を用意する(線形合同法 - Wikipedia から写しただけ)。

#include <stdio.h>

unsigned int lcg(){
    static unsigned int x = 8;
    unsigned int a = 13;
    unsigned int b = 5;
    unsigned int m = 24;
    x = (a * x + b) % m;
    return x;
}

int main(void){
    for(int i=0; i<100; i++){
        printf("%u, ", lcg());
    }
}
13, 6, 11, 4, 9, 2, 7, 0, 5, 22, 3, 20, 1, 18, 23, 16, 21, 14, 19, 12, 17, 10, 15, 8, 13, 6, 11, 4, 9, 2, 7, 0, 5, 22, 3, 20, 1, 18, 23, 16, 21, 14, 19, 12, 17, 10, 15, 8, 13, 6, 11, 4, 9, 2, 7, 0, 5, 22, 3, 20, 1, 18, 23, 16, 21, 14, 19, 12, 17, 10, 15, 8, 13, 6, 11, 4, 9, 2, 7, 0, 5, 22, 3, 20, 1, 18, 23, 16, 21, 14, 19, 12, 17, 10, 15, 8, 13, 6, 11, 4,

周期が極端に短い疑似乱数生成器による乱数列

こんなの乱数っていえるかよ!(疑似乱数なので当然だけど)

暗号学的な強度が求められているときはもちろん、十分無作為である必要があるときに自作の乱数を使ってしまうと大変なことになりそう。

予測してないじゃん!と思ったあなたへ

実際に予測する記事書きました

burion.net


  1. さっき牛丼を食べながら思いつきました
  2. 自動生成のイメージしやすい例として URL に付与される token を上げた。しかし、実際はこのケースの総当たり(オンライン攻撃)は通信が必要になるため試行速度はレイテンシで制限されてしまう。それは本質的ではなく、ここで考えたい「予測の難しさ」は ZIP ファイルのパスワードの総当たりの様に(オフライン攻撃)最先端の非常に強力なコンピューターを使って高速に試行したとしても成立するレベルの難しさを意図している。
  3. 逆に暗号的な強度は不要で乱数生成速度を重視したい場合は「暗号的に安全な乱数」は向かない。アルゴリズムによると思うが getrandom() では乱数生成に必要なエントロピーが溜まるまでは処理をブロックするため遅くなるし、暗号学的ハッシュ関数を通す場合も必要な処理が増えるためパフォーマンスは出しにくい思う。

変数の知識の重複による不整合を防ぎたい(DRY原則とオブジェクト指向)

オブジェクト内で同じ意味を持つ変数が複数存在する場合(つまり、DRY な状態になっていない)、不整合が発生する可能性がある。

例えば、注文を表す Order と、商品を表す Item を考えてみる。Order は複数の Item オブジェクトをまとめる役割である。Item にはそれぞれ商品の名前と価格を持っている。更に、Order クラスには合計金額を持たせたい。この場合、以下のように書けると思う。(アクセス修飾子は適当気味)

また、sum(), printOrder() は本質的にはどうでもいい関数なので、これ以降は省略する。

// 合計を計算する(以後省略)
function sum(numbers: number[]) {
    return numbers.reduce((prev, curr) => prev + curr, 0);
}

// order をいい感じに表示する(以後省略)
function printOrder(order: Order) {
    console.log(`ORDER ID: ${order.id}`);
    console.log(`Items:`);
    for (const i of order.items) {
        console.log(`${i.name} ${i.price}円`);
    }
    console.log("");
    console.log(`Total: ${order.totalPrice}円`);
}

class Item {
    constructor(readonly name: string, private _price: number) {}

    get price(): number {
        return this._price;
    }

    setPrice(price: number): void {
        this._price = price;
    }
}

class Order {
    constructor(
        readonly id: string,
        readonly items: Item[],
        private _totalPrice: number,
    ) {}

    get totalPrice(): number {
        return this._totalPrice;
    }

    setTotalPrice(totalPrice: number): void {
        this._totalPrice = totalPrice;
    }
}

const items = [
    new Item("Apple", 120),
    new Item("Beef", 300),
    new Item("Milk", 200),
];

const totalPrice = sum(items.map((i) => i.price));
const order = new Order("ORDER_01", items, totalPrice);

printOrder(order);

/*
[LOG]: "ORDER ID: ORDER_01" 
[LOG]: "Items:" 
[LOG]: "Apple 120円" 
[LOG]: "Beef 300円" 
[LOG]: "Milk 200円" 
[LOG]: "" 
[LOG]: "Total: 620円" 
*/

実行してみると、ちゃんと動いてるように見える。

ただ、この書き方はあまり良くない。
以下のコードのように、商品 "Beef" の価格を 500 円に変更したとする。万が一 order.totalPrice の更新を忘れてしまうと、誤った情報を表示することになってしまう。

これは、商品の「金額」という情報が order.totalPriceitem.price 両方が持っているためで、いわゆる知識が重複している状態(DRY 違反)だといえる。

// Beef の価格を変更する
order.items[1].setPrice(500);

// order.totalPrice の更新を忘れて表示してしまう
printOrder(order);

/*
[LOG]: "Total: 620円" 
[LOG]: "ORDER ID: ORDER_01" 
[LOG]: "Items:" 
[LOG]: "Apple 120円" 
[LOG]: "Beef 500円" 
[LOG]: "Milk 200円" 
[LOG]: "" 
[LOG]: "Total: 620円"  【間違い!!!!】
*/

一度計算した値を保持するのではなく、以下のコードのように order.totalPrice が必要になる度計算してあげるように変更すると、そのような問題は防げる。
再計算によるパフォーマンスの悪化よりも、保守性や不整合が起きた時のリスクを考えると DRY な状態にした方が合理的な場合が多いのでできればこうしたい。

class Item {
    constructor(readonly name: string, private _price: number) {}

    get price(): number {
        return this._price;
    }

    setPrice(price: number): void {
        this._price = price;
    }
}

class Order {
    constructor(readonly id: string, readonly items: Item[]) {}

    totalPrice(): number {
        return sum(this.items.map((i) => i.price));  // 都度計算する
    }
}

const items = [
    new Item("Apple", 120),
    new Item("Beef", 300),
    new Item("Milk", 200),
];

const order = new Order("ORDER_01", items);

printOrder(order);

// Beef の価格を変更する
order.items[1].setPrice(500);

// order.totalPrice の更新は必要ない
printOrder(order);

/*
[LOG]: "ORDER ID: ORDER_01" 
[LOG]: "Items:" 
[LOG]: "Apple 120円" 
[LOG]: "Beef 500円" 
[LOG]: "Milk 200円" 
[LOG]: "" 
[LOG]: "Total: 820円"  【合ってる!】
*/

...まぁできるだけそうしたいんだけど、DRY に違反にせざるを得ない状況というのがあった。

整合性が崩れたデータを読めないようにする

以下の Rectangle のように DRY でなくても、1クラスに完全に隠蔽することができれば、そのクラスを慎重に作ることで外からどう扱われようと不整合が起きない状態にできる。

class Rectangle {
    private height: number = 0;
    private width: number = 0;
    private area: number = 0;

    setSize(height: number, width: number): void {
        this.height = height;
        this.width = width;
        this.area = this._calculateArea()
    }

    getArea(): number {
        return this.area;
    }

    private _calculateArea(): number {
        return this.height * this.width;
    }
}

ただ、前の例のように Order, Item と 2 つ以上のクラスが関係していると、完全にカプセル化することが難しく不整合が起きる可能性が高まってしまう。 そこで、Order.totalPrice を読もうとしたときに、不整合が起きている可能性があればエラーを出してしまうのはどうか?と考えてみた。おかしなデータのまま処理を進めるよりは止めた方がいい。

アイデアとしては以下の図のように依存関係を持った Dirty flag でオブジェクトをラップする。値が変更されたときには、そのオブジェクトに依存しているオブジェクトを dirty にしてあげることで、不整合な値を読んでしまうことを防ぐという感じ。

dirty-flagを使った不整合対策

【2022-04-25 追記】Observerパターンだと教えていただきました

このようなアイデアは Observer パターンとしてあるもの。 Observer パターン を用いて totalPriceItem に subscribe し、Item が変更されたときには totalPrice 側に通知をして isDirty = true にする(もしくは最新の状態に更新してしまう)。
これで以下と全く同じ動きになる。もし似たようなことをしたければ使いたい言語の Observer パターンの実装があるか探してみるのが良さそう。

ja.wikipedia.org

以降は消すのももったいないので残しておくが、非推奨。

【非推奨】 自作の Dirtiable クラス

class Dirtiable<T> {
    private _data: T;
    private _isDirty: boolean = false;
    private _dependencies: Set<Dirtiable<any>> = new Set();
    private onDirtyRead = (): void => {
        throw new Error("Dirty read occurred!!");
    };

    constructor(data: T, onDirtyRead?: () => void) {
        if (onDirtyRead) {
            this.onDirtyRead = onDirtyRead;
        }
        this._data = data;
    }

    get(): T {
        if (this._isDirty) {
            this.onDirtyRead();
        }
        return this._data;
    }

    set(data: T): void {
        this._data = data;
        this.clean();
        this._dirtyDependencies(new Set<Dirtiable<any>>());
    }

    dependsOn(dependenciesTo: Dirtiable<any>[]): void {
        for (const d of dependenciesTo) {
            d._addDependency(this);
        }
    }

    private _addDependency(dependency: Dirtiable<any>): void {
        this._dependencies.add(dependency);
    }

    isDirty(): boolean {
        return this._isDirty;
    }

    clean(): void {
        this._isDirty = false;
    }

    // 自身と、自身に依存している Dirtible を dirty にする。
    // 依存関係にループがあったとしても、無限に再起呼び出しされないようにするため複雑になっている
    dirty(): void {
        this._dirtyAll(new Set<Dirtiable<any>>());
    }

    private _dirtyAll(
        alreadyDirtied: Set<Dirtiable<any>>,
    ): void {
        this._isDirty = true;
        alreadyDirtied.add(this);
        this._dirtyDependencies(alreadyDirtied);
    }

    private _dirtyDependencies(alreadyDirtied: Set<Dirtiable<any>>): void {
        for (const d of this._dependencies) {
            if (alreadyDirtied.has(d)) {
                continue;
            }
            d._dirtyAll(alreadyDirtied);
        }
    }
}

class Item {
    private _price: Dirtiable<number>;

    constructor(readonly name: string, price: number) {
        this._price = new Dirtiable<number>(price);
    }

    get price(): Dirtiable<number> {
        return this._price;
    }

    setPrice(price: number): void {
        this.price.set(price);
    }
}

class Order {
    private _totalPrice: Dirtiable<number>;

    constructor(
        readonly id: string,
        readonly items: Item[],
        totalPrice: number,
    ) {
        this._totalPrice = new Dirtiable<number>(totalPrice);

        // items.price に依存させる
        this.totalPrice.dependsOn(items.map((item) => item.price));
    }

    get totalPrice(): Dirtiable<number> {
        return this._totalPrice;
    }
}

const items = [
    new Item("Apple", 120),
    new Item("Beef", 300),
    new Item("Milk", 200),
];

const totalPrice = sum(items.map((i) => i.price.get()));

const order = new Order("ORDER_01", items, totalPrice);

printOrder(order);

// Beef の価格を変更する
order.items[1].setPrice(500);

// order.totalPrice の更新を忘れて表示してしまう
printOrder(order);

/*
[ERR]: Dirty read occurred!! 
*/

上のように、DirtiableItem.price, Order.totalPrice をラップする。Order.totalPrice --> Item.priceという依存関係があるので Order のコンストラクタで設定しておく。

この状態で不整合を起こして表示しようとしてみる。すると、Dirty read occurred!! というエラーがでて失敗する。いい感じ。

これをちゃんと表示できるようにするためには、"Beef" の価格を変更した後に、order.totalPrice を更新してあげる必要がある。
このように、更新漏れは例外の発生で気づけるようになる。

// Beef の価格を変更する
order.items[1].setPrice(500);

// 再設定してあげる
const newTotalPrice = sum(items.map((i) => i.price.get()));
order.totalPrice.set(newTotalPrice);

printOrder(order);

/*
[LOG]: "Total: 620円" 
[LOG]: "ORDER ID: ORDER_01" 
[LOG]: "Items:" 
[LOG]: "Apple 120円" 
[LOG]: "Beef 500円" 
[LOG]: "Milk 200円" 
[LOG]: "" 
[LOG]: "Total: 820円" 
*/

個人的には面白いアイデアなのかなーと思った。このような仕組みやライブラリって実際使われていたりするのかが気になる。

きっと誰かがライブラリとして実装していたり、もっと素晴らしい方法が無いかと思い調べてみたが、探し方が悪いのか全然見つからなかった。どう調べれば見つかるんだろうか...。

参考