DEV Community

e_ntyo
e_ntyo

Posted on • Edited on

コモナドスケッチ ~ 1. DOMのコラージュ ~

これはなに?

PureScriptでSPAを書いていく連載記事で、hiruberutoさんの モナドのまほう シリーズのオマージュです。本家はマインクラフトみたいなブラウザゲームをPureScriptで書いていくやつでめっちゃ面白いです✨

なお、「PureScriptが何なのか、どういう利点があるのか」等のメタな内容についてはこの記事で解説しません。(途中でJSフレームワークやECMAScriptとPureScriptを比較することはあるかもしれませんが)。hiruberutoさんの純粋関数型スクリプト言語PureScriptのはじめかたなどが参考になります🙌

tl; dr

  • slamdata/purescript-halogenを使うとモナディックに、そして型安全にSPAが構築できてすごい
    • 今回は主にこのpurescript-halogenの解説になります。需要があれば別途Halogenの解説記事を書くかもしれません📝
  • purescript-halogenでフルスタックなSPAを書くのであれば、vladciobanu/purescript-halogenがとても参考になる
    • ルーティング, global stateの管理, 外部APIとのやり取り, ダイアログの表示など、本家のexamplesではカバーされていない内容が含まれている
    • 大変充実した内容なのですが、抽象度がかなり高い上、HaskellのReaderTパターンやモナド変換子についての前提知識が必要です。私のような初心者はまずHalogenのドキュメントとサンプルのソースコードを一通り読むことをオススメします。そしてこの連載も読んでください!!(Halogenの日本語記事はまだほぼありません。)

今回やること

連載をやろうと思い立ったところまでは良かったのですが、作るSPAが思い浮かびませんでした。とりあえずは以前Angularで書いたオタクのハンドルネームを入れるとその人のアウトプットをシュッと収集できるやつ↓の移植を進めていきます。

第一回の今回は、purescript-halogenを使ってインターフェース部分↓のみをつくります。

今後の連載ではユーザ(user1, user2, ...)ごとにアウトプットのタイムラインができて、スタイルも当てていき、最終的にはもとよりカッコよくてランタイムエラーのないアプリケーションに生まれ変わる予定です✨

ソースコードはこちらです。

やっていく

parcel-bundlerで開発環境を用意する

SPA開発となれば、最低限bundlerとホットリロードが効くdev-serverがあってほしいものです。ethul/purs-loader を使うことでWebpackを使っても良いでしょうし、今後特に細かくカスタマイズするつもりがないのであればParcelを使うと楽で良いでしょう。justinwoo氏のEasy PureScript bundling with Parcelが参考になりました。

なお、これは残念なお知らせですが、PureScriptでは多くの関数がカリー化されていることで、たいしたこともしていないのにバンドル後のJSファイルのサイズがとてつもなく大きくなってしまいます。

以前rollup.jsでカリー化を無理やりやめさせて(どうやって?)サイズを小さくするプラグインがあるという話をインターネットのどこかで目にした記憶があるのですが、調べても見つかりませんでした…

purescript-halogenでUIを構築する

次にSPA開発でほしいものというと、やはりUIライブラリ(を内臓するフレームワーク)でしょう。PureScriptのUIライブラリについてはhiruberutoさんがPureScriptのUIライブラリまとめでまとめてくださっています。

ここで挙げられている中で私が最近気になっているのは、既にProductionで使われているライブラリ2つです。1つはpurescript-halogenで、slamdataというスタートアップが開発し、自社サービスに利用しています。特徴を先のまとめ記事から引用すると、

  • 中身の仮想DOM実装は、PureScript製のpurescript-halogen-vdom
  • Fluxやelm-htmlみたいにアクションを投げて状態を更新するタイプ
  • 状態更新や子のコンポーネントへのクエリまでを含めた計算構造をFreeモナドで定義するガチ勢
  • 要素と属性の組み合わせが正しいかどうかまで静的に型付けする機構がある
  • slamdataという会社のひとたちが中心に開発している。Halogenを使った製品を実際に提供しているようなので、実績としては間違いなくこの中で一番上
  • 最もサンプルやドキュメントが揃っていてコントリビュータもユーザも多い
  • ルーターやCSSを型安全に書くライブラリやなども平行して開発されている(他のUIライブラリからでも使えるが)
  • SVGのサポートがまだないことがおそらく唯一にして最大の欠点。本体とは別に、svgサポートを作っている人はいます (purescript-halogen-svg)

と、かなり良さげな印象です。

先に書いたとおり今回はこのpurescript-halogenを使っていくわけですが、最近LumiというスタートアップがPureScriptを採用し、やはり自作のライブラリを公開しています。purescript-react-basicです。PureScriptの採用とライブラリ誕生の経緯は公式の記事にありますが、主に既存Reactコンポーネントの再利用が目的であると書かれています。

purescript-halogen入門

purescript-halogenでは、React.jsのようにコンポーネントのツリーでUIを構築します。今回はざっくりと以下のようなコンポーネントツリーを想定します。

UserAdd コンポーネントは<input type="text">要素を含み、オタクのハンドルネームを入力するために使います。UserList コンポーネントはUserTimeLine コンポーネントのリストです。移行前の画面でいうところの、ユーザのアウトプットが並んでいるカラム1つがUserTimeLineに、カラム全体の集合がUserList に相当します。

コンポーネントにはそれぞれQuery,State, Input, Outputを定義することができます。それぞれどんなものか、かなりざっくり書くとこうです。

  • Query: 代数的データ型で、コンポーネントの状態(State)を変更するコマンドを列挙したもの。
    • i.e.) Counterコンポーネントなら、Increment, Decrement
  • State: React.jsのそれ。Stateが更新される度にView(DSLで定義する。SPAならHTMLのはず。)も更新される。
  • Input: 親子関係にあるコンポーネントについて、親から子に渡すデータ。
    • 親のStateが更新されるとInput も自動で更新される。
  • Message: 親子関係にあるコンポーネントについて、子から親へ送るデータ。AngularでいうEvent

UserAdd コンポーネントを例にとってみてみます。

module UserAdd where

import Prelude

import Data.Maybe (Maybe(..))
import Data.Symbol (SProxy(..))
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP

type State = { userID :: Maybe String }
initialState = { userID: Nothing } :: State

data Query a
  -- request
  = GetUserID (String -> a)
  -- action
  | UpdateUserID String a
  | AddUserID a

type Slot = H.Slot Query Message

_userAdd = SProxy :: SProxy "userAdd"

data Message = AddedUserID String

userAdd :: forall m. H.Component HH.HTML Query Unit Message m
userAdd =
  H.component
    { initialState: const initialState
    , render
    , eval
    , receiver: const Nothing
    , initializer: Nothing
    , finalizer: Nothing
    }
  where

  render :: State -> H.ComponentHTML Query () m
  render state =
      HH.div_
      [
        HH.input
          [
            HP.type_ HP.InputText,
            HP.placeholder "@e_ntyo",
            HE.onValueChange (HE.input UpdateUserID)
          ]
      ,
        HH.button
          [ HE.onClick (HE.input_ AddUserID) ]
          [ HH.text "追加" ]
      ]

  eval :: Query ~> H.HalogenM State Query () Message m
  eval = case _ of
    GetUserID reply -> do
      _userID <- H.gets _.userID
      case _userID of
        Nothing -> pure (reply "")
        Just id -> pure (reply id)
    AddUserID next -> do
      _userID <- H.gets _.userID
      case _userID of
        Nothing -> do
          H.raise $ AddedUserID ""
          pure next
        Just id -> do
          H.raise $ AddedUserID id
          pure next
    UpdateUserID id next -> do
      H.modify_ (_ { userID = Just id })
      pure next
Enter fullscreen mode Exit fullscreen mode

State には { userID :: Maybe String } という型(レコード型といいます)を定義しています。つまり<input>要素からセットされたユーザIDがセットされているか、何もないかの状態を取るということです。

Query には、requestとよばれるタイプのクエリと、actionとよばれるタイプのクエリのデータ型が定義されています。どちらもコンポーネントのStateを変更しうるコマンドを表しているのですが、requestのほうはその名の通り、そのコマンドを評価する際になんらかの情報を返すことができます。

Queryを評価する関数はevalで、Queryは代数的データ型なのでパターンマッチでいい感じに書くことができます(もちろん抜け漏れがあった場合はコンパイラが教えてくれます)。evalの型はQuery ~> H.HalogenM State Query () Message mで、これはQueryH.HalogenM State Query () Message mというモナドに変換すると読むことが出来ます。~>は関手の付け替えを表しているようで、圏論では自然変換(Natural Transformation)というそうですが私は圏論はさっぱりなので割愛します🙇

ここで注目するべきは以下の点です。

  • action のクエリである AddUserIDUpdateUserID の評価では、それぞれ親コンポーネントにMessageを送信したりStateに変更を加えたりしたあと、pure nexta型の値を持ち上げたものを返している
    • UpdateUserID では、a型の値に加えてユーザIDを表すString型の値も引数に取っている。
    • AddUserID が送信したMessageContainerコンポーネントにてハンドルされ、その際にStateが更新されてリアクティブにUserListInputで定義したデータが渡る。
      • これらの処理や定義は、それぞれのコンポーネントのファイルに書いてあります。
  • request のクエリである GetUserID の評価では、pure (next id) として、next に加えて String 型のユーザIDも返している。

各リクエストの評価はdo構文の中で行われており、純粋な計算とこのモナドの中で許されている処理以外はコンパイルエラーとなります。これはかなり頼もしいというか安全です。

また、感の良い方はお気づきかもしれませんが、H.HalogenM State Query () Message mmは基底のモナドが選べることを意味しています。主にAff(PureScriptにおける非同期処理で使うモナド)が使われるようで、理由は最終的にルートコンポーネントを受け取って処理するrunUI関数ではmAffになっていなければならないというのと、Affを選ぶとEffectでできること(lograndomなど)もできて嬉しいというところです。詳しくはこちらにあります。AffではなくEffectを選んだ場合はhoistという関数で型を合わせるようですが、使ったことがなくドキュメントもまだないためよくわかっていません。

render関数は、Stateを受け取ってhtmlを返す関数です。htmlは独自のDSLで記述し、hiruberutoさんの記事でもあったとおり、ちゃんと適切な属性やイベントを設定しないとコンパイルエラーになります。Halogenに対応したtyped cssのライブラリもあり、今後はこれを使っていく予定です。

なお、Halogenは内部ではvdomを使っているため、これが毎回バカ真面目に描画されるのではなく、ちゃんと差分だけ更新されたりすると思います。そのためにリスト系のコンポーネントではReact.jsでいうkeyみたいなものを定義したりします。今回はこの辺りの解説は割愛します。

こうしたコンポーネントを定義していくことで、相当安全にUIが構築できそうな雰囲気です。もちろんコンポーネントのInput, Messageだけでデータをやり取りしていくことは難しいため、tl; drで紹介したサンプルのようにglobal stateを扱うための機構が今後必要になります。

次回予告

この連載の一番の楽しみはサブタイトルを考えることです。DOMのコラージュってなんだよ…なんだと思いますか?私にはわかりません。

次回以降はHalogen以外のライブラリも駆使して、移行作業をバシバシ進めていきます。

  • slamdata/purescript-affjax + justinwoo/purescript-simple-jsonでユーザのアウトプットタイムラインを実装する
    • Angularで書いていた時はrxjsでポーリングを実装できたのですが、PureScriptではどうやるのがスマートでしょうか。地道にsetIntervalで書いていくしかないのか…
  • thomashoneyman/purescript-halogen-formlessでユーザ名の入力をバリデーションする
    • フォームといっても別に<input>1つだけなのですが、面白そうなので使ってみたいです
  • typed css
    • これはもう取り組んでいて、いいね…という感じです。

次回以降も、きっと読みに来てくださいね!最後までお付き合いいただきありがとうございました💓

Top comments (0)