2021/9/2 Halogenの型だけ見て概念の俯瞰をする
#ログ系
うまくいくかわからんmrsekut.icon
うまくいけばタイトルも変えて整理しよう
方針
Halogen Component全体を俯瞰したい
親子を別々でコードを提示し、それぞれ概念と型を同時に説明する
型以外の実装はできるだけ無視する
Halogenの親子の通信の仕方は3つある
query
親→子
reactで言う関数のprops
関数とは限らない #??
reactのイメージと言うよりは、
親が子にqueryを送る
通信するイメージ?
input
親→子
reactで言う値のprops
output
子→親
event
これらは根幹の概念で、色々な型に出現する
この名詞が使われていたらこれを指すものと考えればいい
Slotが出てくるまではほぼReactと同じなので難しくないmrsekut.icon
子
code:child.purs(hs)
type Slot p = ∀ query. H.Slot query Void p
_home :: Proxy "home"
component :: ∀ q i o m. H.Component q i o m
component =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval H.defaultEval
}
where
render :: ∀ state action m. state -> H.ComponentHTML action () m
componentの型
q, i, o,mはいずれも開いている
親から何かを受け取る場合には、qやiを指定する
ここでは開いているので、何も受け取らないことを意味する
oは、親へ何かを返す時に指定する
renderは、StateのみからComponentHTMLを生成する純粋関数
だから、親からのinputを使用したい場合には、initialState内でstateに変換しておく必要がある
actionはStateを変更する操作なので、stateがないなら必然的に不要
H.ComponentHTML
H.ComponentHTML型
action, slot, m
子がいないならslotは()になる
子というか、子componentか #??
親
code:parent.purs(hs)
type ChildSlots =
( home :: Home.Slot Unit
)
render :: ∀ m. State -> H.ComponentHTML Action ChildSlots m
H.mkComponentの取るrecordの型は2回go to definitionすれば確認できる
code:purs(hs)
type ComponentSpec state query action slots input output m =
{ initialState :: input -> state
, render :: state -> HC.HTML (ComponentSlot slots m action) action
, eval :: HalogenQ query action input ~> HalogenM state action slots output m
}
initialStateは、親からのinputを引数にして、子自信のstateを使う
親からのinputがない場合は、ワイルドカードにして_、定義すればいい
renderについて
複数の子を持つときは ChildSlotsで子のslotをrow型で定義して、renderの返り値の型に含める
render :: ∀ m. State -> H.ComponentHTML Action ChildSlots mを見るだけで以下のことがわかる
State
なにかしらの状態を持っている
Action
その状態を操作することもできる
ChildSlots
子に、componentを持っている
HH.slot_関数はななに
子componentを呼ぶ時に必要になる
code:purs(hs)
HH.div_ HH.slot_ _button 0 button { label: "Click Me" }
code:purs(hs)
HH.slot Home._home unit Home.component unit absurd
buttonという独自Comonentを要素としてrenderingするためにHH.slot_を使っている
_button
slotを指定するタグ的な関数
0
後述
button
子component
{..}
子componentの引数Input
slotの最後の引数
子からのoutputがあるならここで指定
ない場合は、slot_を使えばいい
code:purs(hs)
_button a :: forall query. H.Slot query Void a -- 子で定義
type Slots = ( button :: _button Int ) -- 親で定義
ここでのaの型が、HH.slot関数の第2引数に対応する
reactで言う、listにわたすkeyみたいなイメージかな
同じ子同士を識別するために使う
その子が1つしか出現しないとわかっているならUnitでも書いておけばいい
list的なやつの場合は、
Intで連番で区別するか、ユニークな文字列(e.g. ID)とかを与えるとか
eval
3つがlifecycleの話で、2つは別の話をしている
initialize :: Maybe action
lifecycleの話
そのComponentが生成された時に実行するActionを指定する
reactで言う、初回のuseEffect
receive :: input -> Maybe action
親から得たInputを処理する
https://purescript-halogen.github.io/purescript-halogen/guide/05-Parent-Child-Components.html#input
親がrenderingされる度に、子にinputが渡ってくる
inputが渡ってくると、inputはstopしない
無限に親から渡ってくるという意味か #??
だからdefaultEvalではconst Nothingして、何もしないようにしている
reactで言う、2回目以降のuseEffect
finalize :: Maybe action
lifecycleの話
そのComponentが破棄された時に実行するActionを指定する
reactで言う、useEffectのreturn
↑これら3つはいずれも、「○○のときにこのActionを実行する」というのを指定する
defaultEvalでは、全部Nothingになっている
どのタイミングでも、何もActionを実行しませんよ、と宣言している
handleAction :: action -> HalogenM state action slots output m Unit
自信のなかで発火した何らかのactoinが実行された時にそれを処理する
reduxで言うreducer
TEAのupdate
1つのactionを処理している中で、別のactionを呼んでもいい
handleQuery :: forall a. query a -> HalogenM state action slots output m (Maybe a)
こういう面倒臭さがある
ここに書いている例をReactと比較した
1秒毎に子を再renderingする
↓まったくButtonではないのにButtonと命名している部分はスルーしてくださいmrsekut.icon
reactの場合はsimple
code:tsx
function Parent() {
const count, setCount = useState(0);
useEffect(() => {
setTimeout(() => {
setCount((c) => c + 1);
}, 1000);
}, count);
return <Button label={count} />;
}
function Button({ label }) {
return <div>{label}</div>;
}
halogenは割と記述量が増える
code:purs(hs)
parent :: forall query input output m. MonadAff m => H.Component query input output m
parent =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
where
initialState :: input -> ParentState
initialState _ = { count: 0 }
render :: ParentState -> H.ComponentHTML ParentAction Slots m
render { count } =
HH.div_ HH.slot_ _button unit button { label: show count }
handleAction :: ParentAction -> H.HalogenM ParentState ParentAction Slots output m Unit
handleAction = case _ of
Initialize -> do
{ emitter, listener } <- H.liftEffect HS.create
void $ H.subscribe emitter
void
$ H.liftAff
$ Aff.forkAff
$ forever do
Aff.delay $ Milliseconds 1000.0
H.liftEffect $ HS.notify listener Increment
Increment -> H.modify_ \st -> st { count = st.count + 1 }
button :: forall query output m. H.Component query ButtonInput output m
button =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, receive = Just <<< Receive
}
}
where
initialState :: ButtonInput -> ButtonState
initialState { label } = { label }
render :: ButtonState -> H.ComponentHTML ButtonAction () m
render { label } = HH.button_ HH.text label
handleAction :: ButtonAction -> H.HalogenM ButtonState ButtonAction () output m Unit
handleAction = case _ of
Receive input ->
H.modify_ _ { label = input.label }
これでも型定義を端折って書いているmrsekut.icon
特筆すべきは、button側のreceiveの箇所
1秒ごとに親からlabelを受け取るが、それを毎度表示する実装をしないといけない
initialStateは初回の初回の話なので、それ以降の処理はReceiveで別途定義する必要がある
reactの場合は、ここでのinitialStateとreceiveを同時に書いているようなもの
親から受け取ったinputをいったんstateに変換しないと、子の中で使えないので、こういう操作が発生してしまう
ここのだるさはちょっと看過できない..mrsekut.icon
output
https://purescript-halogen.github.io/purescript-halogen/guide/05-Parent-Child-Components.html#output-messages
親が子をsubscriptionするイメージ
親>モーダルという状況で、モーダルが閉じられた時に親にeventを送ったりする