😨

Reactは関数型プログラミングを必ず学習しなければいけないという誤解

2023/04/29に公開6

はじめに

私はよく趣味で関数型プログラミングの話題を日々ウォッチしているのですが、以下のようなニュアンスのことを度々目にします。

  • Reactらしく書けているかわからないため、ベースとなっている関数型プログラミングを習得しなければ上手く書くことができない
  • Reactは関数型プログラミングがベースになっているので自分には苦手だ
  • Reactで関数型プログラミングの理解が深まった
    • そのため、初学者は関数型プログラミングを学んだ方が良い

これらの理解は1から100まで誤解ではないのですが、大部分で不必要な誤解のため、Reactを必要以上に難しく感じてしまったり、関数型プログラミング(言語)に嫌悪感を抱いてしまうのは、あらゆる方面でアンハッピーなのではないか?と思ってこの記事を書きました。

この記事のゴールとして、Reactのアプリケーションの書き方と関数型プログラミングスタイルの共通する部分と共通しない部分を簡単に抑えることで、互いの概念が足枷になって学習が止まらなくなる。変な嫌悪感を抱かなくなり、正しい学習の道筋が確認出来ればなと思います。

Reactの関数型プログラミングを採用している部分

まず初めにReactが関数型プログラミングのエッセンスを採用し、従来の開発体験から、より良くしている部分を確認していきましょう。Reactに限らず近年の言語・フレームワークは、関数型プログラミングである・でないのような0 or 1ではなく、取り入れれば良くなるものは取り入れますし、良くない部分は排除されて最適化されていきます。そのため、極端な結論に行き着く前に、ここの部分はエッセンスを取り入れているのだ。と理解できるようになるのが、誤解を解くための第一歩と考えています。

ReactはWebアプリケーションを構築するためのフレームワークです。そのため、DOMを構築していくことは必須の工程になります。今回関数型プログラミングスタイルを取り入れているDOMの構築処理部分を手続き型と対比させる形で見ていきましょう。例として、以下のようなDOMを構築する手順を考えてみましょう。

<div>Hi John</div>

手続き型スタイルのUI構築

こちらが、DOM APIを利用した手続き的にDOMを生成する方法です。
DOMの状態を想像しながら、1文1文命令を呼び出し、手続き的に処理を書いて完成を目指すのが、この手法の特徴になります。

function hello(name) {
  let name = document.createTextNode(`Hi ${name}`); // Hi John の部分もテキストノード
  let divElem = doc.createElement("div"); // <div></div> を作成
  divElem.appendChild(name); // Hi Johnを <div></div> に挿入
  document.body.appendChild(divElem); // body直下にdivを挿入
}

関数型プログラミングスタイルのUIの構築

Reactの関数型プログラミングスタイルの手法では、基本は静的HTMLのように書き、動的に変化したい部分を引数(props)で受け取り値を埋めることでDOMを作るのが特徴です。完成されたDOMとコードのギャップが少ないことが旨味になります。これらの情報から、実際にはDOM APIを叩き、Bodyに挿入などの操作が必要ですが、そこはReactが隠蔽しており、開発者にはその裏方を意識させないようにして開発体験を向上させています。これは、宣言的UIなんて呼ばれることが多いです。

export default function Hello(props) {
  return <div>Hi { props.name }</div>;
}

// <Hello name="John" />

もう1段階だけ突っ込んでみましょう。Reactのドキュメントから拝借した例になります。文字列の配列を使って、複数のリストアイテムを持つリストを構築したいとします。先ほどの例の通り、Reactではrender関数で手続き的な手法を使わずにUIを構築する必要があります。そのため、forループを利用してしまうと、DOM APIでDOMを作らざるを得なくなってしまいます。これを回避するために使うのが、mapfilterメソッドなどの関数を引数として受け取る関数の利用(高階関数と呼ばれたりします)です。Reactチュートリアルも丁寧で豊富なため、ここでは、改めて高階関数とは何か? map, filterとは何かの説明は省略させていただきます。

const people = [
  'Creola Katherine Johnson: mathematician',
  'Mario José Molina-Pasquel Henríquez: chemist',
  'Mohammad Abdus Salam: physicist',
  'Percy Lavon Julian: chemist',
  'Subrahmanyan Chandrasekhar: astrophysicist'
];

export default function List() {
  const listItems = people.map(person =>
    <li>{person}</li>
  );
  return <ul>{listItems}</ul>;
}

以上が、Reactで関数型プログラミングのエッセンスを採用している部分になります。端的にまとめますと、Reactのrender関数では以下のことが禁じられています。

  • グローバル変数(windowオブジェクト配下の値)や関数外の値の参照
  • DOM APIやwindow関数など関数の中の値以外の状態を書き換えることの禁止
  • 最終的に<div>のようなタグをreturnする

これらがどっぷり関数型プログラミングですか?と言われていたら、本当にエッセンスを借りている程度の影響ですし、多くの方はReactとはこう書くのか、こういうルールなのか 程度で乗り越えている内容ではないでしょうか? まずこの理解が、関数型プログラミングを必ず深く理解しなければならない。という足枷を外す第一歩となります。

Reactの関数型プログラミングを採用していない部分

先ほどのセクションでは、Reactが関数型プログラミングのエッセンスを採用して色の濃い部分を説明しました。ここからは逆に関数型プログラミングをしていない部分の説明をしていきましょう。

先ほどのセクションのまとめで、関数外の値を参照したり、書き換えたりすることが禁止されているという話をしました。それらの処理のことをプログラミング言語の用語として、副作用と呼んでいます。副作用の重要なもう一つのルールとして、関数は同じ引数が渡される限り、同じ値が評価(返却)されるという特徴も持っています。これは、Reactのrender関数では保証されていない部分です。

関数型プログラミングでは、言語のシステム(ランタイム)に副作用の実行を肩代わりさせるための手法が言語によって様々、埋め込まれています。ポイントとしては、開発者に副作用を起こすコードをなるべく書かせないこととなっております。

Reactでは、useEffectという、ここで副作用を起こしても良いよ! というhooksが標準搭載されています。これは良いか悪いかというのは別として、関数型プログラミングスタイルとは明確に違う考え方であると、強く意識する必要があります。

例えば、レンダリング時に1-100の乱数を生成し、それをdivに表示する例を見てみましょう。これは、ブラウザがレンダリングされるたびに異なる値が表示される可能性があります。

function getRandomInt(max) {
  return Math.floor(Math.random() * max);
}

function RandomNumber() {
  const [num, setNum] = useState(0);

  useEffect(() => {
    setNum(getRandomInt(100));
  }, []);
  
  return <div>{ num }</div>; // <div>1-100の数値</div>
}

Reactでは副作用を起こしていけないではなく、むしろ上手く活用することでアプリケーションを構築していくことが求められます。役に立つであろうと、良かれと思って関数型プログラミングに傾倒してしまうと、むしろReactの学習においては足枷となり学習のスピードは落ちてしまうことでしょう。(私がそうでした)

なぜReactが関数型プログラミングの学習が必須であると誤解されてしまったか

ここまで、Reactが関数型プログラミングのエッセンスを持っている箇所・持たない箇所について説明をしてきました。それではなぜ、Reactを学習する上で関数型プログラミングの学習が必須であると誤解されてしまったかについて簡単に触れて終わろうと思います。

オブジェクト指向ベースから関数ベースのスタイルへの移行

Reactの旧公式ドキュメントでは、classで書かれたオブジェクト指向のスタイルでプログラムの例が多く書かれていました。それが、関数とhooksを用いた手法が推奨されシフトしていきました。本来、オブジェクト指向と関数型プログラミングスタイルは相反する概念ではないのですが、このシフトにより対立構造のように見えてしまったのだと思います。もし、関数を使うスタイルが関数型プログラミングスタイルと呼ぶのであれば、C言語も関数型プログラミング言語として呼ばれているはずです。実際に見ていくとJavaScriptの関数の中で、オブジェクト記法は利用され、メソッド呼び出しなども使われているはずです。

宣言的UIへのシフト

これは、DOM APIによるUI構築の手法の対比で取り上げました。この手法に関しては、紛れもなく関数型プログラミングのエッセンスを取り入れているので仕方ないとは思いますが、これらが流行り出した時代として、手続きが主体であるプログラミング手法が主だったためカルチャーショックが大きかったため、関数型を深く理解しなくては、という声が強く印象に残ってしまったのではないかと思います。Reactと直接関係のないRxJSなどのライブラリの普及なども混ざりより印象的だったのではないでしょうか。

しかし、現代では配列に対してのmap, filterなどは当然のように使われ市民権を得ています。プログラミングの学習は反復のため、なども使っているうちに自然に使える程度の道具として定着してきたのではないかと思います。そのため、これは関数型だ!と身構える必要もないのではないでしょうか。

まとめ

Reactの関数型プログラミングのエッセンスを感じられる部分、大きく反している部分について軽く触れてきました。一つの教訓としては、似た響きの用語であればそれをすぐ鵜呑みにして誤認しないことではないかということです。今回では、関数を使っていたら関数型プログラミングであるという言説の鵜呑みに当たります。

結局のところReactを上手く使うために近道や別解はなく、結局のところReactのアプリケーションをたくさん作り、Reactの公式ページなどから哲学を読み取る以外に上達の方法はないのではないかという結論に至りました。

ただ、これは学習の方法を否定するものではなく、個人のやり方として関数型プログラミングを深く学んで、しっかり違いを認識した上でReactを学びたい。関数型の知識をフルに利用したReactアプリケーションを設計・実装したいなどの道も全然ありなのではないかと思います。ただ、大きく誤解した上で学習の妨げになったり、挫折する人をひとりでも減らしたいと思い、このような記事を書かせていただきました。みんなで楽しく学んでいきましょう!

Discussion

Seiji OSeiji O

なかなか今日日深かったです。
関数型について、私もPowerAppsのキャンバスアプリのちょっとだけかじった時に最初
モヤモヤしていたのですが、この辺は作っていると直ぐに慣れてくるんじゃないかと思います。
まずは触ってみて、後からFunctional Reactive Programmingについて知識を得るなどすると
理解が早いのかもしれませんね。

六兵衛六兵衛

forループを利用してしまうと、DOM APIでDOMを作らざるを得なくなってしまいます。

すみません。これは本当ですか?

ABAB↑↓BAABAB↑↓BA

少し説明不足だった気がしますね。申し訳ありません。

<ul>
  arr.map(e => <li>{ e }</li>)
</ul>

こちらは、arr.map の関数自体がli(React.Component型)のリストを返すのに対して

forループは、for自体が値を返すものではなく文のため、DOM APIのように何かの値を変更することでしかDOMを構築できないアプローチになります。

ということで説明になっているでしょうか?

六兵衛六兵衛

ありがとうございます。理解しました。
ただ公式的には、関数内部でのローカル変数の変更は問題ないそうです。

ABAB↑↓BAABAB↑↓BA

はい。特に関数内でforを使ってはいけません。という話ではなく、render関数によるコンポーネントの値はreturnしなくてはならない。ということですね。

うらがみうらがみ

趣旨から外れますが、RandomNumberコンポーネントが表示するのは1-100の数値ではなく0-99の数値だと思います。