30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

『白と黒のとびら オートマトンと形式言語をめぐる冒険』を読んだ

オートマトンの世界に触れることが出来る技術書、ではなくて、ファンタジー小説。
今年はいろいろとよい本を読めたが、これも非常によかった。
本当によく出来ていて、物語と学習が高度に融合している。
以下の出版社のサイトで、プロローグと1章が無料で公開されている。

www.utp.or.jp

本書を知ったキッカケは、『基礎からわかるTCP/IP』のときと同じで、鹿野さんのツイート。

twitter.com

技術書を選ぶときは、鹿野さんvoluntas さんの意見を参考にしている。こういう方々の意見を参考にすれば、まあ間違いがない。もちろん人によって合う合わないはあるにせよ。

今もそうだが「コンピュータサイエンスの基礎を学びたい」と漠然と思っているのだが、どうしたらいいのか分からない。
いきなり重厚な本を読むと間違いなく挫折するので、とにかく軽くて、それでいてちゃんとした内容の本を読みたい。
本書はその条件を満たしているように感じた。結果として、大正解だった。

妖精や魔法が出てくるファンタジー小説を読みながら、オートマトン理論や形式言語理論の世界に触れることが出来る。
そういう「設定」だけを借りてきたのではなく、本当に、ファンタジー小説として成り立っている。

妖精たちの「言語」と、妖精たちが作った「遺跡」が、物語の中心にある。
魔術師に弟子入りした主人公は、「遺跡」にまつわる問題を解決しながら、「言語」に対する理解を深めていく。
それがそのまま、オートマトン理論や形式言語理論の初歩の内容を表現している。

各章の構成はほとんど同じで、序盤で示される知識や情報を使って、その章の内容の問題を解決していくことになる。
そのため、主人公だけでなく読者も、どうすれば解決できるのか考えながら読み進めることができ、自然と物語に入り込める。

後半になるにつれ問題が複雑になっていくのだが、「主人公が自分の考えを整理するために図を描く」という形で随時図解されるので、それを見て考えながら読み進めることが出来る。

注意点として、本書は入門書ではない。だから、オートマトンや形式言語に関する、何か具体的な知識を得られるわけではない。
入門書よりももっと前の、手引きや序論のようなもの。巻末に解説が載っているが、それも含めて、雰囲気を知り、興味を喚起するものに過ぎない。
実際に私は本書を読んで、こういった分野に対して面白そうだと思えたし、「オートマトンや言語理論の入門書を読んでみようか」「こういう理論や考え方を身に付けてみたい」という気持ちになった。
だが、本書自体から理論を学べるわけではない。

それと、ロジカルに、あるいはパズル的に物事を考えるのが好きな人じゃないと、楽しめないかもしれない。
プログラミングの知識は一切不要だが、プログラミングのように、「与えられた法則や制約のなかで解決策を考えること」を楽しめないと、厳しいと思う。
逆にそういうことが好きな人なら、かなり楽しめると思う。
既に書いたようにプロローグと第一章を無料公開してくれているので、これを試し読みして面白いと思えれば、買って損はないはず。

forwardRef と useImperativeHandle

コンポーネント内の DOM 要素にRefオブジェクトを渡すための機能であるforwardRefと、コンポーネントにメソッドを生やす Hooks であるuseImperativeHandleについて、説明する。
どちらもRefオブジェクトやref属性を使った機能なので、それらを理解していることが前提になる。
理解が不十分な場合はまず Ref の基本を学ぶことをおすすめする。

numb86-tech.hatenablog.com

この記事のコードは React のv16.10.2で動作確認している。

forwardRef

forwardRefを学ぶための題材として、テキストボックスへのフォーカスを扱う。

以下のコードでは、focusボタンを押すとテキストボックスにフォーカスする。

import React, {useRef} from 'react';

const App = () => {
  const ref = useRef();

  const onClick = () => {
    ref.current.focus();
  };

  return (
    <div>
      <input ref={ref} type="text" />
      <br />
      <button type="button" onClick={onClick}>
        focus
      </button>
    </div>
  );
};

export default App;

Refオブジェクトをinput要素のref属性に渡すことで、input要素の操作(この例ではフォーカス)が可能になる。

では以下のように、input要素がApp直下の要素ではなく、子コンポーネントの要素だった場合は、どうすればいいのか。

import React from 'react';

const MyInput = () => {
  return <input type="text" />;
};

const App = () => {
  return (
    <div>
      <MyInput />
    </div>
  );
};

export default App;

<MyInput ref={ref} />のようにMyInputpropsとしてrefを受け取り、さらにそれをinputref属性に渡す、ということは出来ない。
refは特殊な属性で、propsには含まれないからだ。そもそも、関数コンポーネントにrefを渡すことは出来ない。

そこで、forwardRefを使う。これを使うことで、関数コンポーネントにrefを渡すことができ、さらにそのなかで自由にrefを使うことが出来る。

forwardRefは関数を返す関数で、引数として関数コンポーネントを渡すと、そのコンポーネントに機能を追加した新しいコンポーネントを返す。
その追加機能によって、関数コンポーネントにref属性を設定できるようになり、設定されたrefは関数コンポーネントの第二引数として受け取る。

以下の例では、forwardRefMyInputを渡して、MyInputをベースとしたWrappedMyInputという新しい関数コンポーネントを作成している。
そして<WrappedMyInput ref={ref} />とすることで、そのコンポーネントは第二引数としてrefを受け取るようになる。

import React, {useRef} from 'react';

const MyInput = (props, ref) => {
  console.log(ref); // {current: undefined}
  return <input type="text" />;
};

const WrappedMyInput = React.forwardRef(MyInput);

const App = () => {
  const ref = useRef();

  return (
    <div>
      <WrappedMyInput ref={ref} />
    </div>
  );
};

export default App;

この機能を利用することで、App側から子コンポーネント内の DOM 要素を操作することが出来る。

import React, {useRef} from 'react';

const MyInput = (props, ref) => {
  return <input ref={ref} type="text" />;
};

const WrappedMyInput = React.forwardRef(MyInput);

const App = () => {
  const ref = useRef();

  const onClick = () => {
    ref.current.focus();
  };

  return (
    <div>
      <WrappedMyInput ref={ref} />
      <br />
      <button type="button" onClick={onClick}>
        focus
      </button>
    </div>
  );
};

export default App;

さて、ここまで書いてきたが、実はforwardRefを使わなくても、同等のことは出来る。
refは特殊な属性だと書いたが、裏を返せば、refとは異なる別の名前でRefオブジェクトを渡してしまえば、それで済む。
それがこのサンプル。

import React, {useRef} from 'react';

const MyInput = props => {
  return <input ref={props.customRef} type="text" />;
};

const App = () => {
  const ref = useRef();

  const onClick = () => {
    ref.current.focus();
  };

  return (
    <div>
      <MyInput customRef={ref} />
      <br />
      <button type="button" onClick={onClick}>
        focus
      </button>
    </div>
  );
};

export default App;

customRefという属性を用意し、それを通してpropsのバケツリレーを行うことで、問題なくinput要素にRefオブジェクトを渡せている。

そのため、forwardRefの必要性が、私にはよく分からない。
各々が独自の属性を用意するのではなく、Refオブジェクトを渡す属性はrefに固定してしまう、ということが可能になるのが、利点なのかもしれない。

useImperativeHandle

useImperativeHandleは Hooks のひとつで、関数コンポーネントにメソッドを追加し、それを親コンポーネントから使えるようにするための機能。
言葉で説明するとイメージしづらいが、サンプルを見れば、大して難しい話でもないということが分かると思う。

説明に入る前に断っておくと、他のRefに関する機能と同様、useImperativeHandleも基本的には使用を避けるべき。
公式ドキュメントにも、以下のように書かれている。

いつもの話ですが、ref を使った手続き的なコードはほとんどの場合に避けるべきです

あくまでもこのような機能もある、という話であり、実際に使うケースはほとんどない。

useImperativeHandleを使うにはまず、Refオブジェクトを関数コンポーネントに渡す必要があるので、forwardRefを使うことにする。
そして関数コンポーネントのなかで、以下のようにuseImperativeHandleを使う。使う場所は、他の Hooks と同様に、関数コンポーネントのトップレベル。

  useImperativeHandle(親コンポーネントから受け取った`Ref`オブジェクト, () => ({
    // ここに、追加したいメソッドを書いていく
  }));

具体例として、MyInputlogというメソッドを持たせた。

import React, {useRef, useImperativeHandle} from 'react';

const MyInput = (props, ref) => {
  useImperativeHandle(ref, () => ({
    log: () => {
      console.log('called log');
    },
  }));

  return <input type="text" />;
};

const WrappedMyInput = React.forwardRef(MyInput);

const App = () => {
  const ref = useRef();

  const onClick = () => {
    ref.current.log(); // called log
  };

  return (
    <div>
      <WrappedMyInput ref={ref} />
      <br />
      <button type="button" onClick={onClick}>
        click
      </button>
    </div>
  );
};

export default App;

こうするとApp側でref.current.logを実行できるようになる。

これも、forwardRefを使わずに書くことが可能。

const MyInput = props => {
  useImperativeHandle(props.customRef, () => ({
    log: () => {
      console.log('called log');
    },
  }));

  return <input type="text" />;
};

useImperativeHandle の依存配列

useImperativeHandleは第三引数に依存配列を渡すことが出来る。
依存配列については以下の記事に詳しく書いたが、これを正しく使うことで、同じ内容のメソッドを何度も作り直してしまうことを防げる。

numb86-tech.hatenablog.com

まずは依存配列を使わないパターン。
ボタンを押す度にstateが更新されるため再レンダーが発生し、useEffectが実行される。
その度にMyInputも再レンダーされるため、都度useImperativeHandleが実行される。
そうするとsomeMethodも新しく作り直されるため、Xは常にfalseとなる。

import React, {useRef, useImperativeHandle, useEffect, useState} from 'react';

const MyInput = (props, ref) => {
  useImperativeHandle(ref, () => ({
    someMethod: () => {},
  }));

  return <input type="text" />;
};

const WrappedMyInput = React.forwardRef(MyInput);

const list = [];

const App = () => {
  const ref = useRef();

  const [state, setState] = useState(0);

  useEffect(() => {
    list.push(ref.current.someMethod);

    if (list.length >= 2) {
      console.log(list[list.length - 2] === list[list.length - 1]); // X
    }
  });

  const onClick = () => {
    setState(s => s + 1);
  };

  return (
    <div>
      <WrappedMyInput ref={ref} attr={state} />
      <br />
      <button type="button" onClick={onClick}>
        click
      </button>
    </div>
  );
};

export default App;

MyInputを以下のように書き換えると、useImperativeHandleは初回レンダー時にしか実行されないため、someMethodは同じ参照を指し続ける。
そのため、Xは常にtrueになる。

const MyInput = (props, ref) => {
  useImperativeHandle(
    ref,
    () => ({
      someMethod: () => {},
    }),
    []
  );

  return <input type="text" />;
};

参考資料