コモナドスケッチ ~ 1. DOMのコラージュ ~
Tags: purescript, functional
Published: 2018-10-23 23:12:40

---
title: コモナドスケッチ ~ 1. DOMのコラージュ ~
published: true
description: PureScriptでSPAを書いていく連載記事
tags: PureScript, FP
---

## これはなに？

PureScriptでSPAを書いていく連載記事で、[hiruberuto](https://qiita.com/hiruberuto)さんの [モナドのまほう](https://qiita.com/hiruberuto/items/5321d8cebce7b87851f6) シリーズの[オマージュ](https://ja.wikipedia.org/wiki/%E3%81%B2%E3%81%A0%E3%81%BE%E3%82%8A%E3%82%B9%E3%82%B1%E3%83%83%E3%83%81_(%E3%82%A2%E3%83%8B%E3%83%A1)#%E5%90%84%E8%A9%B1%E3%83%AA%E3%82%B9%E3%83%88)です。本家はマインクラフトみたいなブラウザゲームをPureScriptで書いていくやつでめっちゃ面白いです✨

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

## tl; dr

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

## 今回やること

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

![](https://camo.githubusercontent.com/39d233b953ada2c6d4e1e0551d6ffcfc857ce99e/68747470733a2f2f692e696d6775722e636f6d2f4935387963306d2e706e67)

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

![](https://i.imgur.com/ldjs3XS.gif)

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

ソースコードは[こちら](https://github.com/entyo/WatchingU/tree/purs)です。

## やっていく

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

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

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

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

### purescript-halogenでUIを構築する

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

ここで挙げられている中で私が最近気になっているのは、既にProductionで使われているライブラリ２つです。１つは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](https://github.com/lumihq/purescript-react-basic)です。PureScriptの採用とライブラリ誕生の経緯は[公式の記事](https://medium.com/fuzzy-sharp/purescript-and-haskell-at-lumi-7e8e2b16fb13)にありますが、主に既存Reactコンポーネントの再利用が目的であると書かれています。

#### purescript-halogen入門

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

![](https://i.imgur.com/OfLqwuV.png)

`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` コンポーネントを例にとってみてみます。

```haskell
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
```

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

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

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

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

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

また、感の良い方はお気づきかもしれませんが、`H.HalogenM State Query () Message m`の`m`は基底のモナドが選べることを意味しています。主に`Aff`(PureScriptにおける非同期処理で使うモナド)が使われるようで、理由は最終的にルートコンポーネントを受け取って処理する`runUI`関数では`m`が`Aff`になっていなければならないというのと、`Aff`を選ぶと`Effect`でできること(`log`や`random`など)もできて嬉しいというところです。詳しくは[こちら](https://github.com/slamdata/purescript-halogen/blob/master/docs/3%20-%20Handling%20effects.md)にあります。`Aff`ではなく`Effect`を選んだ場合は[`hoist`](https://pursuit.purescript.org/packages/purescript-halogen/docs/Halogen.Component#v:hoist)という関数で型を合わせるようですが、使ったことがなく[ドキュメントもまだない](https://github.com/slamdata/purescript-halogen/tree/master/docs#the-halogen-guide)ためよくわかっていません。

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

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

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

## 次回予告

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

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

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

次回以降も、きっと読[みに来てくださいね](https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q13152924193)！最後までお付き合いいただきありがとうございました💓