1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ReactのuseEffectを悪用してBrainf*ckを実装する

Posted at

TL; DR

useEffect を意図的に無限ループさせてbrainf*ckインタープリターを走らせた

const [brain, setBrain] = useState(
    Brain.create("", new TextEncoder().encode(""))
);

useEffect(() => {
  setBrain((brain) => brain.next());
}, [brain]);

はじめに

React初心者なので、よくうっかり useEffect で自身が参照しているstateを更新してしまい、無限ループを起こしてしまいます。ハングするUI、空費されるメモリ...

// レンダリング→useEffectでstate更新(=component更新)→レンダリング→useEffectで...
const [num, setNum] = useState(0);
useEffect(() => {
    setNum(num => num+1);
}, [num]);

そこで今回は、悩みの種の無限ループを逆手にとって useEffect でbrainf*ckインタープリターを走らせてみました

作ったもの

sceenshot.png

source にソースコードを入れると実行結果が result に出力されます。見た目は普通のUIインタープリターと変わりありません。

動作原理

仕組みは冒頭の無限ループのコードと同じです。インタープリターの状態をオブジェクト化し、useEffectの中で「現在のトークンを評価した直後の状態」をセットします。

useEffect(() => {
  setBrain((brain) => brain.next());
}, [brain]);

無限ループによって、結果的にソースコードのトークンを先頭から末尾まで評価した後の状態が得られます1

useEffectの動作の仕組みについては、以下の記事が非常に参考になりました。

インタープリターの状態は、以下のオブジェクトで管理しています。入出力、メモリ、ソースコードとそれぞれのどの場所を今読んでいるかを保持しています。

export class Brain {
  private constructor(
    public readonly input: Uint8Array,
    public readonly inputCursor: number,
    public readonly output: Uint8Array,
    public readonly memory: Uint8Array,
    public readonly memoryCursor: number,
    public readonly source: string,
    public readonly sourceCursor: number
  ) {}

  public next(): Brain { // ソースコード上の次のトークンを評価
    if (this.sourceCursor >= this.source.length) {
      return this;
    }

    switch (this.source[this.sourceCursor]) {
      case "+":
        return this.add();
      case "-":
        return this.sub();
      case ">":
        return this.inc();
      case "<":
        return this.dec();
      case ",":
        return this.read();
      case ".":
        return this.write();
      case "[":
        return this.loop();
      case "]":
        return this.break();
    }

    return this.comment();
  }

  private add(): Brain { // +:今参照しているポインタの値をインクリメント
    return new Brain(
      this.input,
      this.inputCursor,
      this.output,
      updateUint8Array(this.memory, this.memoryCursor, (e) => e + 1),
      this.memoryCursor,
      this.source,
      this.sourceCursor + 1
    );
  }
}
// その他のメソッド...

特徴としては、内部の状態を変化させる代わりに 「評価後の状態」を新たに生成 して返しています。状態変化を useEffect に任せられるためこのような実装になりましたが、結果的にループも破壊的変更も無いメソッドになりました。Reactは、純粋関数型Brainf*ck処理系を実装するのにうってつけです

いろいろなソースコードを試してみる

せっかく作ったので色々なbrainf*ckソースコードを実行してみます。
こちらの記事でいろいろなサンプルコードが紹介されていたので一通り試してみました。

想定通りの結果が得られたので、インタープリターに目立ったバグは無さそうです。

square.png

とはいえ、レンダリングをステップ数分実行しているので、実行にはかなり時間がかかりました
(上の例だと15万回ステップ、表示が終わるまで3分程度かかった)
1ステップずつ実行されるので、1文字ずつ出力される様子が目視できます。気長に待ちましょう。

おわりに

しょうもない記事に最後までお付き合いいただきありがとうございました。

今回の方法は、仕組み上言語処理系以外でもループが必要なアルゴリズムならなんでも使えます、ソートなりニュートン法なりお好きなものを useEffect で計算しましょう。 実行速度とチームからの微妙な視線については責任を負いかねますのでご了承ください。

(おまけ:過去の文法悪用シリーズ)

  1. ソースコードを読み終わると brain の状態が変わらなくなるため、無限ループは停止します。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?