「関数型プログラミングの基礎」モナドを作るを理解する
と同じ本の、p.268~p.274あたりでつまずいたので、メモを残しておく。
コードはここにおいてある。
akimichi.github.io
モナドの機能は、「値にコンテキストを付加すること」、「コンテキストを付加したまま処理を合成すること」である。
unit
関数は、モナドのインスタンスを生成するための関数で、unit:: T => M[T]
という型情報を持つ。
T
はモナドに渡される値の型であり、M
はモナドの型である。
flatMap
関数は、処理を合成するための関数で、flatMap:: M[T] => FUN[T => M[S]] => M[S]
という型情報を持つ。
M[T]
というモナドのインスタンスを受け取ると、そのモナドから値を取り出してT => M[S]
という関数を適用し、その結果としてM[S]
を返す。
今回メモするモナドは、恒等モナドである。
恒等モナドは、値にコンテキストを付加することなく、そのまま値として扱う。
恒等モナドの関数を、以下のようにIDという名前空間に定義する。
const ID = { unit: (value) => { return value; }, flatMap: (instanceM) => { return (transform) => { return transform(instanceM); }; }, };
また、次のような関数を定義する。
const succ = (n) => { return n + 1; }; const double = (m) => { return m * 2; };
unit
関数は、単に値を返しているだけである。
ID.unit(1) == 1;
flatMap
関数を介して1
にsucc
関数を適用する処理は、succ(1)
と同じ結果になる。
ID.flatMap(ID.unit(1))((one) => { return ID.unit(succ(one)); }) == succ(1)
これを順を追ってみていく。
まず、ID.flatMap
は、引数ID.unit(1) = 1
なので、以下を返す。
(transform) => { return transform(1); };
そして、(one) => { return ID.unit(succ(one)); }
が適用される。
(one) => { return ID.unit(succ(one)); }(1)
よって元のモナドは、succ(1)
と同じ結果になる。
次に、flatMap
関数を入れ子にしたものをみていく。
これは、次の関数を合成する関数をおなじ働きをする。
const compose = (f, g) => { return (arg) => { return f(g(arg)); }; };
つまり、以下の等式が成り立つ。
ID.flatMap(ID.unit(1))((one) => { return ID.flatMap(ID.unit(succ(one)))((two) => { return ID.unit(double(two)) }) }) == compose(double, succ)(1)
これを順を追ってみていく。
まず、以下の関数に着目する。
ID.flatMap(ID.unit(succ(one)))((two) => { return ID.unit(double(two)) })
ID.flatMap
は以下を返す。
(transform) => { return transform(ID.unit(succ(one))); };
そして、(two) => { return ID.unit(double(two)) }
が適用される。
((two) => { return ID.unit(double(two)) })(ID.unit(succ(one)))
よって、元の以下の関数は、
ID.flatMap(ID.unit(1))((one) => { return ID.flatMap(ID.unit(succ(one)))((two) => { return ID.unit(double(two)) }) })
以下のようになる。
ID.flatMap(ID.unit(1))((one) => { return ((two) => { return ID.unit(double(two)) })(ID.unit(succ(one))) })
ID.flatMap
は、引数ID.unit(1) = 1
なので、以下を返す。
(transform) => { return transform(1); };
よって、以下の関数が適用されると、
(one) => { return ((two) => { return ID.unit(double(two)) })(ID.unit(succ(one))) }
最終的には以下のようになる。
(one) => { return ((two) => { return ID.unit(double(two)) })(ID.unit(succ(one))) }(1)
よって元のモナドは、関数を合成する関数と同じ働きをする。
const compose = (f, g) => { return (arg) => { return f(g(arg)) } } ID.flatMap(ID.unit(1))((one) => { return ID.flatMap(ID.unit(succ(one)))((two) => { return ID.unit(double(two)) }) }) == compose(double, succ)(1)
GoでPythonのrandom.shuffle()を実装し、カイ二乗検定を用いたユニットテストをする
背景
Goでコントリビュートできそうなレポジトリを探していると、gosh
というレポジトリがあった。
これは、JavaScriptやPythonでの構文をGoで書く、というものだった。
面白そうだったので、Pythonのshuffleを実装し、PRを出そうと思った。
Pythonのshuffleというのは、配列を受け取り、その配列の要素の順番をランダムに変換して返す、というのだった。
commitしようとしていたコードは、以下だった。
func Shuffle(s []interface{}) []interface{} { rand.Seed(time.Now().UnixNano()) rand.Shuffle(len(s), func(i, j int) { s[i], s[j] = s[j], s[i] }) return s }
しかし、commitしようとした時にはもう実装されていたのでissueを閉じた。
github.com
commitしようとした時のテストコードは、
- shuffleする前とした後で配列の長さが等しい
- shuffleする前とした後で配列の要素が等しい
ことを確認するものだった。
しかしよく考えると、上のコードでは破壊的な操作はしておらず、上記の2項目は自明であった。
肝心なのは、
- shuffleによって、要素の順番が満遍なく変換されている
ことを確認することだった。
今回のような、ランダム性をテストする時にはどうしたらよいかを調べてみると、以下のスライドがあった。
speakerdeck.com
このスライドによると、カイ二乗検定
によってランダム性をテストできる、ということだった。
カイ二乗検定とは
帰無仮説が正しければ検定統計量が漸近的にカイ二乗分布に従うような統計的検定法の総称である。
カイ二乗検定 - Wikipedia
ざっくりとした感じで例を出す。
サイコロを振って、目が均等に出ているかどうかを検定したいとする。
実際に振ってみて出た目の回数を観測度数、目が均等に出ている場合の回数を期待度数という。
それぞれの回数が以下の場合、
サイコロの目 | 1 | 2 | 3 | 4 | 5 | 6 | 合計 |
---|---|---|---|---|---|---|---|
観測度数 | 17 | 22 | 21 | 14 | 22 | 24 | 120 |
期待度数 | 20 | 20 | 20 | 20 | 20 | 20 | 120 |
という計算でを求める。
このが、棄却域より小さければ、目が均等に出ていることが保証される。
棄却域は有意水準と自由度で決まる。
有意水準は、たいてい5%か1%で、自由度は、(要素の数 - 1)で、今回だったら、(目の数6 - 1 )= 5となる。
棄却域の分布表は以下から取得できる。
http://www3.u-toyama.ac.jp/kkarato/2016/statistics/handout/chisqdist.pdf
コードの概要
今回作ったリポジトリが以下である。
github.com
shuffle.go
には、先述したshuffleのコードを書いている。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/shuffle.go
package chi_square import ( "math/rand" "time" ) func Shuffle(s []interface{}) []interface{} { rand.Seed(time.Now().UnixNano()) rand.Shuffle(len(s), func(i, j int) { s[i], s[j] = s[j], s[i] }) return s }
テストは、[]interface{}{"a", "b", "c", "d", "e"}
というスライスをShuffle
に渡したとき、うまくシャッフルできているかを確認する。
カイ二乗検定を行うChiSquare
は、第1引数に有意水準、第2引数に自由度、第3引数に観測度数、第4引数に期待度数をとり、が棄却域より小さい場合にtrue
、大きい場合にfalse
を返している。
Shuffle
を100回実行したとき、スライスの先頭、つまり"a"
の位置が、0,1,2,3,4番目にくる回数がそれぞれ等しかったら、うまくシャッフルできているといえる。
よって、期待度数は、[]float64{20, 20, 20, 20, 20}
となる。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/shuffle_test.go#L28
観測度数は、実際にShuffle
を100回実行し、"a"
のインデックスを累計して求めている。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/shuffle_test.go#L27
有意水準は5%に設定し、自由度は(配列の長さ-1)で求められている。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/shuffle_test.go#L29
if !ChiSquare(5, len(expected_frequency)-1, observation_frequency, expected_frequency) { t.Error("not shuffled") }
の計算および、棄却域との比較は、chi_square.go
でやっている。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/chi_square.go
... x += (math.Pow(observation_frequency[i] - expected_frequency[i], 2)) / expected_frequency[i] ... if x > rejection_area { return false } else { return true } ...
テスト実行と結果
実際にテストを実行してみると、以下のような出力を得る。
https://github.com/wafuwafu13/go-chi-square-test/blob/master/chi_square.go#L14
$ go test x: 1.0999999999999999, rejection_area: 9.49 PASS ok go-chi-square-test 0.474s
の値、つまり出力x
の値は実行するたびに異なる。
このことからもシャッフルがうまく行われていることがわかる。
< go test x: 4.1, rejection_area: 9.49 PASS ok go-chi-square-test 0.148s
また、shuffle.go
のrand.Seed(time.Now().UnixNano())
の処理を消すと、出力x
の値は実行するたびに同じになり、(テストの結果をログに残しておけば?)シャッフルの処理がうまく行われていないこともわかる。
ちなみに、テスト対象のスライスを[]interface{}{"a", "a", "a", "a", "a"}
にすると、毎回"a"
が0番目にあると判定されてしまうので、テストは落ちる。(そもそも同じ要素だけのスライスをshuffleする動機が謎なので落ちて正解という捉え方もできる)
https://github.com/wafuwafu13/go-chi-square-test/blob/master/shuffle_test.go#L9
$ go test x: 400, rejection_area: 9.49 --- FAIL: TestShuffle (0.00s) shuffle_test.go:30: not shuffled FAIL exit status 1 FAIL go-chi-square-test 0.414s
テスト対象のスライスによってexpected_frequency
の値を変えてTableDrivenTestsすればよいと思うけど、もう満足したので終わる。
数学3の教科書をSymPyで解く
自然言語処理に興味を持っている。
wafuwafu13.hatenadiary.com
が、本を読んでいても数式が結構出てきてきついところがあった。
数1と数2はかろうじて履修していたが、数3からは全くわからず、∞とかがでてくると拒否反応を示すレベルだった。
だからまず数学に入門することにした。
この本でSympy
を知り、せっかくだから積読していた数3の教科書を解いてみることにした。
Sympy
は、あくまで理解の補助として簡単な問題で使い、他の問題は途中式まで理解することを意識した。
とりあえず微分が理解できればよさそうだったので、3章の関数と極限と、4章の微分法とその応用までやった。
この資料もまとまっていてよかった
解いたのがこちら。
giste5ab71750d08bb4af1ad01eb5dd3200a
とりあえず数式に拒否反応はなくなったのでよかった。
「関数型プログラミングの基礎」代数的データ構造とパターンマッチを理解する
この本で関数型プログラミングの基礎を押さえようとしたけれど、p140の「代数的データ構造とパターンマッチ」あたりから理解に苦しんだのでメモをとりながらじっくり進めることにした。
全てのコードはここに置いてある。
akimichi.github.io
JavaScriptのswitch文では、配列のように可変なデータの場合にはアドレス値で比較されるので、マッチングは失敗する。
以下が良い例である。
よって、複数の選択肢からいずれか1つの型だけを選ぶデータ構造である代数的データ構造を使ってマッチングを行う。
代数的データ構造は、以下のように具体的なデータは作らず、(pattern) => return pattern.XXX
という関数を返す。
const empty = () => { return (pattern) => { return pattern.empty(); }; }; const cons = (value, list) => { return (pattern) => { return pattern.cons(value, list); }; };
パターンマッチを実行するmatch
関数の引数のdata
には代数的データ構造、pattern
にはオブジェクト型のインスタンスが入る。
このインスタンスによって条件分岐を実行する。
const match = (data, pattern) => { return data(pattern); };
代数的データ構造のリストに対するisEmpty
をmatch
関数を利用して実装する。
isEmpty
は、引数に渡されたリストが空のリストかどうかを判定する。
const isEmpty = (alist) => { return match(alist, { empty: () => { return true; }, cons: (head, tail) => { return false; }, }); };
実際に使ってみると以下のようになる。
isEmpty(empty()); // true isEmpty(cons(1, empty())) // false
1つ目のisEmpty(empty())
から詳しく読む。
isEmpty
の引数およびmatch
の第一引数であるdata
となるempty()
は以下の関数を返す。
(pattern) => { return pattern.empty(); };
また、pattern
は以下のオブジェクトである。
{ empty: () => { return true; }, cons: (head, tail) => { return false; }, }
よって、match
ではpattern
のempty
が実行され、true
が返る。
同じようにisEmpty(cons(1, empty()))
を読んでいく。
cons
の定義をもう一度みてみると、以下である。
const cons = (value, list) => { return (pattern) => { return pattern.cons(value, list); }; };
よって、isEmpty
の引数およびmatch
の第一引数であるdata
となるcons(1, empty())
は以下の関数を返す。
(pattern) => { return pattern.cons(1, (pattern) => { return pattern.empty(); } ); };
よって、pattern
のcons
が実行され、false
が返る。
次に、リストの先頭要素を調べるhead
の定義を読む。
const head = (alist) => { return match(alist, { empty: () => { return null; }, cons: (head, tail) => { return head; }, }); };
実際に使ってみると以下のようになる。
head(cons(1, empty())) // 1
これは、引数は同じだが、pattern
のcons
は以下のように第一引数のhead
を返す。
{ empty: () => { return null; }, cons: (head, tail) => { return head; } }
よって、1
が返される。
これで代数的データ構造とパターンマッチは理解できた。
せっかくなのでTypeScriptを使用して型をつけていく。
まずは、完成した型の全体像が以下である。
type IsEmptyPattern = { empty: () => boolean; cons: (head: number, tail: EmptyReturn | ConsReturn) => boolean; }; type HeadPattern = { empty: () => null; cons: (head: number, tail: EmptyReturn | ConsReturn) => number; }; type isEmptyReturn = ReturnType<typeof isEmpty>; type headReturn = ReturnType<typeof head>; type EmptyReturn = ReturnType<typeof empty>; type ConsReturn = ReturnType<typeof cons>; const empty = (): (( pattern: IsEmptyPattern | HeadPattern ) => boolean | null) => { return (pattern: IsEmptyPattern | HeadPattern) => { return pattern.empty(); }; }; const cons = ( value: number, list: | EmptyReturn | ((pattern: IsEmptyPattern | HeadPattern) => boolean | number) ): ((pattern: IsEmptyPattern | HeadPattern) => boolean | number) => { return (pattern: IsEmptyPattern | HeadPattern) => { return pattern.cons(value, list); }; }; const match = ( data: EmptyReturn | ConsReturn, pattern: IsEmptyPattern | HeadPattern ): boolean | null | number => { return data(pattern); }; const isEmpty = (alist: EmptyReturn | ConsReturn): boolean => { return <boolean>match(alist, { empty: () => { return true; }, cons: (head: number, tail: EmptyReturn | ConsReturn) => { return false; }, }); }; const head = (alist: EmptyReturn | ConsReturn): null | number => { return <null | number>match(alist, { empty: () => { return null; }, cons: (head, tail) => { return head; }, }); }; isEmpty(empty()); // ture isEmpty(cons(1, empty())); // false head(cons(1, empty())); // 1
isEmpty
、head
関数で使われているpattern
の型を定義する。
また、empty
、cons
、isEmpty
、head
関数の戻り値をReturnTypeでとっている。
isEmpty
、head
関数の戻り値にType assertionsをつけているのは、以下のようなエラーが出たので誤魔化した。
Type 'number | boolean | null' is not assignable to type 'boolean'.
Type 'null' is not assignable to type 'boolean'.
cons
の第二引数list
の型は、EmptyReturn | ConsReturn
にしたらType alias 'ConsReturn' circularly references itself.
のエラーが出たので自己参照しないようにした。
Type alias circularly references itself · Issue #14174 · microsoft/TypeScript · GitHub
この辺りで議論されているけど力尽きたのでとりあえずこのままにしておく。
この先もサンクを無限を表現するとか難しそうな項目があるのでじっくり進めていきたい。
Released npm package 「react-p5-components」
Hi :)
I released react-p5-components
.
npm
www.npmjs.com
Library features is that you can use p5.js in react without writing p5's code.
It means this library generate p5.js Canvas depending on your settings.
There are few types of components now, but I plan to increase them.
Add new components · Issue #3 · wafuwafu13/react-p5-components · GitHub
And there are many other issues, so I'm waiting for your contribution!
Issues · wafuwafu13/react-p5-components · GitHub
Reactのコードを読む(3)
前回はhello world
が描写されるところまでをみたので、今回はuseStateをみていこうと思う。
まずは、useState
がどこで定義されているのかを確認する。
<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react@17/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> </head> <body> </div> <script> console.log(React); </script> </body> </html>
この出力は、以下のようになる。
Children: {map: ƒ, forEach: ƒ, count: ƒ, toArray: ƒ, only: ƒ} Component: ƒ Component(props, context, updater) Fragment: Symbol(react.fragment) ... useEffect: ƒ useEffect(create, deps) useImperativeHandle: ƒ useImperativeHandle(ref, create, deps) useLayoutEffect: ƒ useLayoutEffect(create, deps) useMemo: ƒ useMemo(create, deps) useReducer: ƒ useReducer(reducer, initialArg, init) useRef: ƒ useRef(initialValue) useState: ƒ useState(initialState) ...
つまり、useState
はReact
のプロパティとして定義されていて、コードでは3552行目でエクスポートされている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react.development.js#L3552
exports.useState = useState;
次は、console.log(React.useState);
によってuseState
を出力してみる。
ƒ useState(initialState) { var dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }
結果をみると、useState
は関数だとわかる。
実際にコードを見にいくと、1531行目に定義されている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react.development.js#L1531
function useState(initialState) { var dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }
では、実際にconsole.log(React.useState());
で呼び出してみる。
すると、以下のエラーがでる。
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
このエラーは、1532行目で呼び出されていたresolveDispatcher
関数の内部の1501行目で起こっている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react.development.js#L1501
function resolveDispatcher() { var dispatcher = ReactCurrentDispatcher.current; if (!(dispatcher !== null)) { { throw Error( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem." ); } } return dispatcher; }
このエラーは、if (!(dispatcher !== null))
がtrue
のとき、つまりdispatcher
がnull
のとき生じる。
そのdispatcher
に代入されているReactCurrentDispatcher
は、115行目で定義されており、確かにReactCurrentDispatcher.current
はnull
である。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react.development.js#L115
var ReactCurrentDispatcher = { /** * @internal * @type {ReactComponent} */ current: null };
エラー内容に、Hooks can only be called inside of the body of a function component.
とあったので、function component
を定義し、その内部でuseState
を呼び出してみる。
そのためにはまず、コンポーネントを作成しないといけない。
コンポーネントの作成には、createElement()を用いる。
公式のReact 要素を作成するに、「JSX のそれぞれの要素は React.createElement() を呼ぶための単なる糖衣構文です。」という説明がなされており、今回はuseState
をみるのが目的なので深追いはしない。
では、以下のようにhello
コンポーネントを作成し、画面にhello world
を描写する。
そのhello
コンポーネント内でuseState
を呼び出して出力してみる。
<body> <div id="root"></div> <script> const hello = () => { console.log(React.useState()); return React.createElement("div", null, "hello world"); }; ReactDOM.render( React.createElement(hello), document.getElementById("root") ); </script> </body>
出力は以下のように、1つ目の要素がundefined
、2つ目の要素が関数の配列が返ってくる。
(2) [undefined, ƒ] 0: undefined 1: ƒ () arguments: (...) caller: (...) length: 1 name: "bound dispatchAction" __proto__: ƒ () [[TargetFunction]]: ƒ dispatchAction(fiber, queue, action) [[BoundThis]]: null [[BoundArgs]]: Array(2) length: 2 __proto__: Array(0)
先ほどnull
になってエラーの原因となっていたdispatcher
をみてみると、useState
が含まれていた。
readContext: ƒ (context, observedBits) unstable_isNewReconciler: false useCallback: ƒ (callback, deps) ... useState: ƒ (initialState) ...
dispatcher
に代入されているReactCurrentDispatcher.current
がどのように設定されているのかは、以下の記事がわかりやすかった。
renderWithHooks
関数は14976行目に定義されている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L14976
resolveDispatcher
が呼び出されたあとは、dispatcher.useState
が呼び出されている。
このuseState
は16271行目に定義されている。
useState: function (initialState) { currentHookNameInDev = 'useState'; mountHookTypesDev(); var prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV; try { return mountState(initialState); } finally { ReactCurrentDispatcher$1.current = prevDispatcher; } },
これはHooksDispatcherOnMountInDEV
のプロパティであり、15011行目でReactCurrentDispatcher
に代入されている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L15011
ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
useState
の内部ではReactCurrentDispatcher
の更新が行われ、15651行目で定義されているmountState
関数が呼び出されている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L15651
function mountState(initialState) { var hook = mountWorkInProgressHook(); if (typeof initialState === 'function') { // $FlowFixMe: Flow doesn't like mixed types initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; var queue = hook.queue = { pending: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: initialState }; var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue); return [hook.memoizedState, dispatch]; }
そこでhook.memoizedState
に引数で受け取ったinitialState
が代入され、返り値の配列の第一引数になっている。
第二引数のdispatch
は、16805行目のdispatchAction
をbindしたものが入っている。
これらの返り値を以下のように分割代入して用いる。
const [foo, setFoo] = useState(bar);
深追いできなかった部分は多かったが、以上でsetState
を読むのは終える。
ReactCurrentDispatcher
の更新が沼すぎるのでこれ以上Reactを読むかどうかは分からない、とても同じ人間が書いたとは思えない。
次回からはフロントの繋がりでBabelを読むか、Goのechoとかを読むかもしれない。
DOMの更新に関しては以下の記事がわかりやすかったのでこれを深めると良いと思っている。
Reactのコードを読む(2)
前回はReactDOM
についてみたので、今回はrender
でhello world
が描写されるところまでみていこうと思う。
まずは、ReactDom.render
を出力してみる。
<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/react@17/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> </head> <body> </div> <script> console.log(ReactDOM.render()); </script> </body> </html>
すると、以下のようなエラーが発生する。
Uncaught Error: Target container is not a DOM element. at Object.render (react-dom.development.js:26121) at index.html:11
エラーが発生しているのは26121行目で、!isValidContainer(container)
がtrue
になっている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L26121
function render(element, container, callback) { if (!isValidContainer(container)) { { throw Error( "Target container is not a DOM element." ); } } ...
isValidContainer
関数は25932行目にある。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L25932
function isValidContainer(node) { return !!(node && (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE || node.nodeType === COMMENT_NODE && node.nodeValue === ' react-mount-point-unstable ')); }
今回は引数のnode
がundefined
であるため、false
が返り、!isValidContainer(container)
はtrue
となる。
では、以下のコードで実際にhello world
を表示させた状態でみていく。
<div id="root"></div> <script> console.log(ReactDOM.render( "hello world", document.getElementById('root') )); </script> </body> </html>
render
関数の第二引数であるdocument.getElementById('root')
のnodeType
を出力すると、1
であった。
console.log(document.getElementById('root').nodeType); // 1
Node.nodeTypeが1
ということはつまりELEMENT_NODE
であるから、isValidContainer
関数はtrue
を返す。
続いて、26126行目をみてみる。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L26126
var isModernRoot = isContainerMarkedAsRoot(container) && container._reactRootContainer === undefined;
まずは、isContainerMarkedAsRoot
関数が定義されている10597行目をみてみる。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L10597
function isContainerMarkedAsRoot(node) { return !!node[internalContainerInstanceKey]; }
internalContainerInstanceKey
は、10583行目で生成されているrandomKey
を含む変数であり、10586行目に定義されている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L10583
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L10586
var randomKey = Math.random().toString(36).slice(2); ... var internalContainerInstanceKey = '__reactContainer$' + randomKey;
よって、最終的に例えばnode["__reactContainer$dgg96aihpip"]
の存在が調べられ、false
が返り、26110行目の条件分岐は通らない。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L26110
if (isModernRoot) { error('You are calling ReactDOM.hydrate() on a container that was previously ' + 'passed to ReactDOM.createRoot(). This is not supported. ' + 'Did you mean to call createRoot(container, {hydrate: true}).render(element)?'); }
続く26133行目では、関数の返り値を返している。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L26133
return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
legacyRenderSubtreeIntoContainer
関数は26024行目に定義されており、その中で26032行目にroot
が定義されている。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L26032
var root = container._reactRootContainer;
div #root
には_reactRootContainer
は存在せず、root
はundefined
になる。
よって、26035行目のif (!root)
を通り、26037行目でroot
が代入される。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L26037
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
legacyCreateRootFromDOMContainer
は25983行目に定義されており、26011行目で関数の返り値を返している。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L26011
return createLegacyRoot(container, shouldHydrate ? { hydrate: true } : undefined);
そのcreateLegacyRoot
は25929行目に定義されており、25930行目でインスタンスを返している。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L25930
return new ReactDOMBlockingRoot(container, LegacyRoot, options);
結果的に、root
は以下のような値になる。
_internalRoot: FiberRootNode callbackNode: null callbackPriority: 0 containerInfo: div#root context: null current: FiberNode {tag: 3, key: null, elementType: null, type: null, stateNode: FiberRootNode, …} entangledLanes: 0 entanglements: (31) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ....
fiberRoot
の値はroot._internalRoot
であり、以下のようになる。
callbackNode: null callbackPriority: 0 containerInfo: div#root context: null current: FiberNode {tag: 3, key: null, elementType: null, type: null, stateNode: FiberRootNode, …} entangledLanes: 0 entanglements: (31) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] eventTimes: (31) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] expirationTimes: (31) [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]
このcontainerInfo: div#root
には、document.getElementById('root')
で取得したDOMの情報が含まれている。
... outerHTML: "<div id="root"></div>" outerText: "" ownerDocument: document URL: "file:///Users/tagawahirotaka/Desktop/react-17-react-dom-17/index.html" ...
今までみてきたlegacyRenderSubtreeIntoContainer
は、最終的に26070行目で関数の返り値を返している。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L26070
return getPublicRootInstance(fiberRoot);
getPublicRootInstance
は25515行目で定義されており、25527行目でNodeを返している。
https://github.com/wafuwafu13/react-17-react-dom-17/blob/master/react-dom.development.js#L25527
return containerFiber.child.stateNode;
このcontainerFiber.child.stateNode
には、render
の第一引数で渡したhello world
がnodeValue
やtextContent
として含まれている。
nodeName: "#text" nodeType: 3 nodeValue: "hello world" ownerDocument: document parentElement: div#root parentNode: div#root previousElementSibling: null previousSibling: null textContent: "hello world"
このNodeが返されることにより、画面にhello world
が描写される。
途中の処理を結構飛ばしたので理解が曖昧だが、次はuseState
などをみていきたい。