フロントエンドを関数型言語で開発したい事はよくありますよね。
関数型AltJSには、以下のようなものがあります。
- ClojureScript
- Elm
- ScalaJS
- PureScript
- Reason
- BuckleScript
- GHCJS
- ...
今回はPureScriptを試してみましょう。
PureScript
詳しくは公式サイトを読んで下さい。
「Haskellっぽい何か」ですね。
Haskellっぽさは GHCJS(Haskellそのもの) > PureScript(かなりHaskell) > Elm(みためHaskell)
って感じでしょうか(私見)。
インストール
PureScriptを入れるには、大きく分けて3つの方法があります。
- Hackage経由で入れる
- npmで入れる
- バイナリを持ってくる(または自分でコンパイル)
npmで入れるのが一番安定しているように思えます。
$ npm install -g purescript
haskell stackを使って入れる事もできます。
$ stack install purescript
これで purs
コマンドが使えるようになります。
プロジェクト管理
PureScriptのプロジェクト管理(パッケージ管理)の仕組みには、2つの方法があります。
- psc-packageを使う
- pulp + bowerを使う
ライブラリとかを見るとpulp + bowerを前提にしているものが多そうに見えたので、こちらを使ってみましょう。
pulpもbowerも、npmで入れる事ができます。
$ npm install -g pulp bower
使い方は、だいたい次のような感じです。
# プロジェクト作成
$ pulp init
# パッケージ追加。管理はbowerに準じる
# purescript系ライブラリはpurescriptのプレフィックスが付いている
$ bower i purescript-hogehoge --save
# ビルド
$ pulp build
# バンドル
$ pulp browserify --optimise --to dist/bundle.js
フレームワーク
近年のフロントエンドはJavaScriptフレームワークと無縁ではいられません。
PureScriptで使えるフレームワークについては、既にまとめ記事があります。感謝
この中から、1番Githubのスター数が多そうで、実際のプロダクト(開発元の会社)でも使われているHalogenを、試しに使ってみましょう。
サンプルアプリ
module Main where
import Prelude
import Effect (Effect)
import Data.Maybe (Maybe(..))
import Data.Int (fromString)
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
type State = { x :: Int, y :: Int }
data Field = X | Y
data Query a = Update Field String a
calc :: forall m. H.Component HH.HTML Query Unit Void m
calc =
H.component
{ initialState: const initialState
, render
, eval
, receiver: const Nothing
}
where
initialState :: State
initialState = { x: 0, y: 0 }
render :: State -> H.ComponentHTML Query
render s =
HH.div_
[ HH.input
[ HP.type_ HP.InputText
, HP.value $ show s.x
, HE.onValueChange $ HE.input $ Update X
]
, HH.text " + "
, HH.input
[ HP.type_ HP.InputText
, HP.value $ show s.y
, HE.onValueChange $ HE.input $ Update Y
]
, HH.text $ " = " <> (show $ s.x + s.y)
]
eval :: Query ~> H.ComponentDSL State Query Void m
eval (Update xy s next) =
let
n = case fromString s of
Just n -> n
Nothing -> 0
in do
_ <- H.modify $ case xy of
X -> (_ { x = n })
Y -> (_ { y = n })
pure next
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI calc unit body
よくある足し算をするアプリですね。
上から見ていってみましょうか。
データ型とか
type State = { x :: Int, y :: Int }
data Field = X | Y
data Query a = Update Field String a
アプリ内で使うデータ型を予め定義しています。
Stateはアプリの状態ですね。Int型の値2つを持っています。
足し合わせる数を入れる2つのフィールドの状態に対応しています。
Fieldはアプリ内のInputフィールドの種類を表しています。
2つあるフィールドを、それぞれX、Yとしましょう。
Queryはアプリの更新要求ですね。Field型とString型の値を引数に取ります。
何のフィールド(XかY)がどんな値(数値を期待)かを運ぶ為のデータ型です。
最後の a
型の値については、Halogenが利用する「何かしら」の値です。
コンポーネント
calc :: forall m. H.Component HH.HTML Query Unit Void m
calc =
H.component
{ initialState: const initialState
, render
, eval
, receiver: const Nothing
}
Halogenはコンポーネント指向のUIライブラリです。
コンポーネントにも色々種類があるようですが、単純なコンポーネントを作ってみます。
Halogen.Component型は5つの型引数を取ります。
HTML型はコンポーネントをレンダリングする時に使う型で、だいたいは上のHTML型を使うようです。
Query型は、コンポーネントを更新する際に利用される型です。コンポーネントの更新情報は、この型の値の中に押し込めます。
Unit型を取っているのは Input values
と呼ばれる型引数で、コンポーネントが子になる時、親から情報を伝播される時に利用するものです。今回は何かの子コンポーネントではないので、特に情報が来る事はなく、Unit型なのですね。
Void型を取っているのは Output messages
と呼ばれる型フィールドで、Queryが処理される際に、親コンポーネントに更新を伝える(raiseする)為に利用するらしいです。まさしくInputの逆ですね。同じく、今回は子コンポーネントではないのでVoid型を指定しています。
こう見ると、Halogenは先祖子孫コンポーネント間で情報のバケツリレーをしていくのだとわかりますね。
最後の m
型ですが、これはコンポーネントのステートに依存しない状態(IOなど)を管理するために利用するモナドらしいです。
というわけで、 component関数でコンポーネントを作ります。初期化関数(initialState)、レンダリング関数(render)、更新関数(eval)、親コンポーネントから情報を受け取った時用関数(receiver)、をそれぞれ渡します。
今回は親コンポーネントが無いので、初期値受け取る事も、値を受け取る事もありません。というわけで、const関数(引数を2つ取り、2つ目の引数を無視して1つ目の引数を返す関数)を使って決まった値を返しているのですね。
では、whereの中を見ていきましょう。
初期値
initialState :: State
initialState = { x: 0, y: 0 }
初期値です。
両方のフィールドに0を入れておきます。
レンダリング
render :: State -> H.ComponentHTML Query
render s =
HH.div_
[ HH.input
[ HP.type_ HP.InputText
, HP.value $ show s.x
, HE.onValueChange $ HE.input $ Update X
]
, HH.text " + "
, HH.input
[ HP.type_ HP.InputText
, HP.value $ show s.y
, HE.onValueChange $ HE.input $ Update Y
]
, HH.text $ " = " <> (show $ s.x + s.y)
]
関数型言語の力強さを活用したDSLですね。
Stateを受け取り、HTMLをレンダリングします。
Halogen.HTML.Events系の関数を利用すると、更新用関数evalを呼ぶ事ができます。引数には更新用のデータを吐き出す関数を入れてあげましょう。
例えばonValueChange関数の場合、String型を引数に取る関数を渡してやるといい感じにしてくれるようです。
状態の更新
eval :: Query ~> H.ComponentDSL State Query Void m
eval (Update xy s next) =
let
n = case fromString s of
Just n -> n
Nothing -> 0
in do
_ <- H.modify $ case xy of
X -> (_ { x = n })
Y -> (_ { y = n })
pure next
eval関数で状態を更新します。
状態の更新はStateモナド(MonadState)を利用して行います。
モナドを扱うので、do式を使うと便利です。Queryの最後の値をpureで包んで返してやると、更新された状態を元に新たにレンダリングをしてくれるというわけです。
ここでは、まずフィールドの文字列を数値にパースします(できなかった場合0とします)。
そして、更新されたフィールドがXなのかYなのかを識別し、modifyメソッドを使ってそれぞれのフィールドを更新します。
使う
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI calc unit body
メイン関数ですね。
使う2
コンパイルして動かしてみましょう。
$ pulp browserify -O -t dist/bundle.js
HTMLも用意します。
<!doctype html>
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<script src="dist/bundle.js"></script>
</body>
</html>
では、適当にサーバを立てて表示してみましょう。
動きましたか?
まとめ
PureScriptとHalogenを使い、コンポーネント1つだけの小さなWebアプリケーションを作ってみました。
この手のAltJS、JavaScriptフレームワークの本当の良さは、もっと大きくて複雑なアプリケーションを作ってみないと分からないところがあるかと思います。
ですが小さなサンプルでも、状態の取り扱い方法やイベントの流れなど、言語やフレームワークの雰囲気はなんとなく掴めるかなぁと思っています。
そんなわけで、PureScriptやHalogenに興味を抱いて頂ければ幸いです。
※ 2018年10月29日追記
v0.12 に対応しました。
@noolbar さん、編集リクエストありがとうございます!