purescript-react-basic-hooksはIxMonadでHooksの順序を規定する
React Hooksには「毎回のrenderingにて、呼び出されるhookの順序は同じでなければならない」というルールがある
これによって、順序の正しさをコンパイラが静的に保証してくれる
元のReact HooksではTypeScriptを使えどHooksの順序は静的に検証はできない
結論
IxMonadなどの型のおかけで
ということがわかる
IxMonadについて軽くおさらい
IxMonadは、通常のMonadの一般化である
通常のMonadと違って、monadic computationで出力の型を変えることができる
通常のMonadの定義はこんな感じ
hsclass Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
IxMonadの定義はこんな感じ
hsclass IxMonad m where
return :: a -> m i i a
(>>=) :: m i j a -> (a -> m j k b) -> m i k b
例えば (>>=)
の第1引数の型が、 m a
から m i j a
のように変わっている
monadic computationの実行前の i
と、実行後の j
があることで、順序を規定できる
何が嬉しいのか?の一例が、このReact Hooksの順序規定である
Effect stack
こういう型をdocs内では、「stack of effects」と呼んでいる
refここでは「Effect stack」と呼ぶ

モナド変換子の文脈の「Monad stack」と同じイメージ

例えば以下のようなcustom hooksを見てみる
purs(hs)useHoge :: ∀ a. Render a (UseState Int (UseEffect Unit a)) Int
useHoge = React.do
useEffectAlways do -- UseEffect
pure mempty
cnt /\ setCnt <- useState 0 -- UseState
pure cnt
内容はどうでもいいので、hooksの呼び出し順序と、型にのみ着目する

まず最初に useEffectAlways
hookを呼んで、次に useState
hookを呼んでいる
この場合、Effect stackは UseState Int (UseEffect Unit a)
となる
この型は、上記コードの1行目を見ても分かる通り、 useHoge
自体の型にも現れる
この型シグネチャのまま、 useEffectAlways
と useState
の位置を入れ替えると型エラーになる
このコードを少し観察すると以下のことがわかる
useHoge
は、 Render
IxMonadである(後述)
useHoge
は、 Render
IxMonad用のdo式( React.do
)を使って定義されている
各Hooksは、monadic computationである
他の例も見てみる
purs(hs)usePiyo :: ∀ a. Render a (UseRef Int (UseState Int (UseEffect Unit (UseRef Int a)))) Int
usePiyo = React.do
rendersRef <- useRef 1 -- UseRef
useEffectAlways do -- UseEffect
pure mempty
cnt /\ setCnt <- useState 0 -- UseState
rendersRef <- useRef 1 -- UseRef
pure cnt
内容はどうでもいいので、hooksの呼び出し順序と、型にのみ着目する

useRef
→ useEffectAlways
→ useState
→ useRef
という順序でhooksを呼んでいる
この場合、Effect stackは UseRef Int (UseState Int (UseEffect Unit (UseRef Int a)))
となる
見たら分かる通り、呼び出されたhookの順番で上へ上へとstackが積まれていることがわかる
purs(hs)newtype Render :: Type -> Type -> Type -> Type
newtype Render x y a = Render (Effect a)
2つのindexとしての幽霊型 x
, y
を持っている
x
が、この Render
開始前のhooksのEffect stack
y
が、この Render
終了後のhooksのEffect stack
Render
の指しているscopeに注意しないと頭がバグる

個々のhooksもRender型で定義するし、
component
を作る際にも Render
型が使われる
この場合の Render
は、「componentのrendering」と同じものを指す
purs(hs)instance IxApplicative Render where
ipure a = Render (pure a)
instance IxBind Render where
ibind (Render m) f = Render (Prelude.bind m \a -> case f a of Render b -> b)
instance IxMonad Render
Render
型は、通常のMonadのinstanceにもなっている
ref実装自体は全く同じ
purs(hs)instance TypeEquals x y => Applicative (Render x y) where
pure a = Render (pure a)
instance TypeEquals x y => Bind (Render x y) where
bind (Render m) f = Render (Prelude.bind m \a -> case f a of Render b -> b)
instance TypeEquals x y => Monad (Render x y)
これによって、rendringの前回と今回とで呼び出すhooksの種類と順序が同じであることを静的に検証できる
useState
などの個々のhookは、 Hook
型を使って定義される
Hook
型は、 Render
型のエイリアスとして定義されている
purs(hs)type Hook (newHook :: Type -> Type) a
= forall hooks. Render hooks (newHook hooks) a
hooks
型は、このhookを実行する直前のEffect stack
newHook
は、 hooks
を引数にとって、 newHook hooks
を返す型
今回のhookを、元のEffect stackの先頭に追加する
だから最初に呼ばれたhookがstackの一番下に来るんやね

Render
がIxMonadのinstanceになっていないと、この「積んでいく操作」ができない
つまり、通常のMonadでは、この「積んでいく操作」ができない
なぜなら、monadic computationの実行前後で型が変わっているから
hooks
型から、 newHook hooks
型に変わっている
具体例を挙げると例えば、 UseEffect Unit Int
から、 UseState Int (UseEffect Unit Int)
に変わっている
個々のHookの型はこんな感じで定義されている
ref refpurs(hs)useState :: forall state.
state
-> Hook (UseState state) (state /\ ((state -> state) -> Effect Unit))
purs(hs)useEffect :: forall deps.
Eq deps
=> deps
-> Effect (Effect Unit)
-> Hook (UseEffect deps) Unit
これらがIxMonad Render
に対するmethodに相当する
対応\ | | Stateモナド | IxStateモナド | Render(今回の話) |
| | | | |
型 | | State s a | State si so a | Render x y a |
method | | put, get | iput, iget | useState, useEffect, etc. |
型エラーになる例
Effect stackが異なれば必然的に型エラーを得られる
こういうcustom hookを作って
purs(hs)-- Effect stackは、UseEffect Unit (UseRef Int (UseState Int a))
useHoge = React.do
cnt /\ setCnt <- useState 0
rendersRef <- useRef 1
useEffectAlways do
pure mempty
pure $ cnt /\ setCnt
purs(hs) cnt /\ setCnt <- if odd x
then useState 10
else useHoge -- type error!
「ifの中でhooksを使ってはいけない」というルールを破っている例
理想的な挙動にはならないが型エラーも出ない例
purs(hs) cmt /\ setCnt <- if x > 10
then useState 10
else useState 5
残念ながらこれは型エラーにならない
何故なら、両方とも UseState Int
をEffect stackに積むので、型レベルで見るとEffect stackは同じものになるから
「人間の想像した挙動と異なる」だけで、「これが仕様」と思えば、型エラーを出さないほうがある意味正解なのかもしれない

例えばこれでもエラーになる
custom hooksの型によって、内部で呼び出すhooksの順番と個数が規定されるので型レベルプログラミング的になる
purs(hs)useHoge :: ∀ a. Render a (UseEffect Unit (UseRef Int (UseState Int a))) (Int /\ ((Int -> Int) -> Effect Unit))
useHoge = React.do
cnt /\ setCnt <- useState 0
-- rendersRef <- useRef 1
useEffectAlways do
pure mempty
pure $ cnt /\ setCnt
でも、実際Custom Hook作る時に、
「内部で使うHooksの個数と順序を先に決め」てから定義するとは思えないので、
後付けで型を指定することになるんだろうな

つまり、IxMonadなどの型のおかけで
ということがわかる
IxMonadの関係のない余談だが、
このエラーとかは、そもそも書けない
purs(hs)mkCounter = do
component "Counter" \initialValue -> React.do
count /\ setCount <- useState initialValue
setCount (_ + 2) -- type error!!
pure $ R.button { .. }
React.do
の中では Render
モナドしか使えないが、 setCount
の返り値は Effect
なので。
やったね

これも余談だが、IxMonadは、通常のdo記法が機能しない
通常のdo記法はMonadの bind
の糖衣構文であり、IxMonadはMonadの一般化なのでその枠を超えているため。
そこで、新しくRender IxMonad用の bind
を使うようにしている
hsで同じことをする場合は、RebindableSyntax拡張が必要になるがpursでは不要っぽい
purs(hs) import Prelude hiding (bind, discard)
import Prelude (bind) as Prelude
import React.Basic.Hooks.Internal (bind)
purs(hs)bind :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b
bind = ibind
discard :: forall a b x y z m. IxBind m => m x y a -> (a -> m y z b) -> m x z b
discard = ibind
いまいちこの辺わかっていない
#??
HookError1.purs(hs)module Hoge where
import Prelude
import Data.Int (odd)
import Data.Tuple.Nested (type (/\))
import Effect (Effect)
import React.Basic.DOM as R
import React.Basic.Events (handler_)
import React.Basic.Hooks (Component, Render, UseEffect, UseState, component, useEffectAlways, useState, (/\))
import React.Basic.Hooks as React
mkCounter :: Component Int
mkCounter = do
component "Counter" \initialValue -> React.do
x /\ setX <- useState initialValue
cnt /\ setCnt <- if odd x
then useState 10
else useHoge
pure
$ R.button
{ onClick: handler_ do
setX (_ + 1)
, children:
[ R.text $ "Increment: " <> show x ]
}
useHoge :: ∀ a. Render a (UseState Int (UseEffect Unit a)) (Int /\ ((Int -> Int) -> Effect Unit))
useHoge = React.do
useEffectAlways do
pure mempty
cnt /\ setCnt <- useState 0
pure $ cnt /\ setCnt