🌟

Reactを題材にPureScriptを始める

2021/09/26に公開

はじめに

本記事ではPureScriptの前提知識を仮定せずに、Reactを使ってHello, world!を行います。

本記事の前半でPureScriptをインストールし、基礎文法の一部を紹介します。
後半ではPureScriptを用いてReactの仮想DOMを定義してブラウザに描画します。

本記事を読み進めていく上で関数型プログラミングに精通している必要はありません。

逆に、使用経験があるほうが望ましいのは次の通りです。(必須ではありません。)

  • React
  • バンドルツール(Webpack等)
  • TypeScript

なお、本記事ではしばしばJavaScriptのコードと比較してPureScriptを説明しますので、JavaScriptの知識は前提とします。

また、記事中の各コマンドはWindowsユーザー向けの記載になっていますので、Windows以外の方は適宜読み替えてください。

PureScript + Reactの動機付け

React公式ドキュメントの副作用フックにもある通り、Reactでは副作用を慎重に取り扱う必要があります。
取り扱い方を間違えると厄介な不具合の原因になります。

また、フックには守らなければならない特別な規則があります。
フックを呼び出してもいい場所には制限があり、例えばif文の中などでフック呼び出しを行うと実行時エラーとなります。
したがって、これを徹底するためにESLintのプラグインなどを導入する必要があります。

PureScriptでは副作用を安全に取り扱う方法が元々用意されているため、仮に副作用の取り扱いを間違えても実行時ではなくコンパイル時にエラーとして検出してくれます。これにより、潜在的な不具合の数を早期に減らすことができます。

Reactでは副作用を扱う際にuseEffectフックを使う必要があります。
そもそも、「副作用とは何か?」という問いに正確に答えられる人がどれだけいるでしょうか。
アプリケーションに追加したい処理があったとして、それをuseEffectフック内に書くべきかそうでないかを判断しなければなりません。
つまり、基本的にはあらゆる処理に対してそれが副作用を伴うかどうかを常に考えなければなりません。

一方、PureScriptでは副作用はEffectとしてコード中に現れるので、型を見るだけで判別できます。
安全なコードを書く難易度という意味で、PureScriptは他の代替手段を使う(つまり、直接JavaScriptからReactを利用したり、TypeScriptからReactを利用したりする)よりもむしろ初心者向けです。
どのような部分に気を付けながらコードを書くべきか?ということをPureScriptコンパイラが教えてくれます。

また、PureScriptではフックの規則についても安全に取り扱う方法が用意されています。
これはESLintプラグインのようなReactのための固有の機能として実装されているわけではありません。
PureScriptが持つ強力な型システムによって、複雑なフックの規則もライブラリの1つとして記述できます。

PureScriptのインストール

Node.jsがインストールされていない場合は先にNode.jsをインストールします。
このとき、Node.jsに付属してnpmも一緒にインストールされるはずです。

npm経由でpurescriptspagoをインストールします。

npm install --global purescript spago

※グローバルにインストールすることをお勧めします。

バージョンを表示してインストールできたかどうか確認できます。

>purs --version
0.14.4

>spago --version
0.20.3

pursはPureScriptコンパイラです。
spagoはPureScriptのパッケージマネージャを含む開発ツールです。

PureScriptを使ってみる

新しい言語を試す際、最もお手軽なのはREPLを使うことです。

spago repl

一連のコンパイル情報が出力され、最終的に次の表示になります。

PSCi, version 0.14.4
Type :? for help

import Prelude

>

> の後に続けて1 + 2と入力し、Enterキーを押します。

> 1 + 2
3

> 

1 + 2の評価結果である3が出力され、再度> が表示されます。
このように、入力・評価・出力を繰り返すのがREPLです。

REPLをやめるにはCtrlキーを押しながらDキーを押します。

REPL便利コマンド

REPL中に:?と入力するとヘルプを確認できます。

中でも、学習中に重要なコマンドは:typeです。
このコマンドの後に半角スペースを開けて値を指定すると、その値の型が出力されます。

> :type 3.14
Number

> :type "Hello, world!"
String

PureScriptでは値aの型Xであることを、2つのコロン:を用いてa :: Xと表します。
したがって、先ほどの:typeによると、3.14 :: Number"Hello, world!" :: Stringです。

また、REPL中で複数行を入力するには:pasteコマンドを使います。
これにより、複数行モードに移行します。
複数行モード中は> の代わりにが表示されます。
複数行モードを終了するにはCtrlキーを押しながらDキーを押します。

> :paste
… multiline =
…   let 
…     a = 42
…     b = 37
…     c = a - b
…   in
…     c * c
…
> 

ドキュメント

Pursuitが充実しています。
特に次の2つのモジュールは基本となりますのでよく参照することになると思います。

PrimモジュールはPureScriptコンパイラに直接埋め込まれていて、インポートせずに使えるモジュールです。
ただし、Primは本当に原始的な機能しか備えておらず、四則演算すら行うことはできません。
そのため、通常はPreludeモジュールと合わせて使います。
基本的な四則演算+, -, *, /Preludeモジュールをインポートすると使えるようになります。
なお、spago replを用いてREPLを開始するとPreludeモジュールは自動的にインポートされます。

JavaScriptとの違い

引き続きREPLを用いつつJavaScriptとの違いについて見ていきます。

コメント

JavaScriptとは異なり、コメントには//ではなく--を使います。

> 1 + 2 -- three
3

範囲コメントは{- -}です。

> 1 + {- add -} 2
3

Int

JavaScriptで数値を扱う型はNumber型のみです。
PureScriptではNumber型の他にも、32ビット符号付き整数の型Intがあります。
小数点無しの数値はInt扱い、小数点ありの数値はNumber扱いとなります。
つまり、PureScriptでは4242.0で型が異なります。

> :type 42
Int

> :type 42.0
Number

JavaScript同様にNumber型にはInfinityなどの値もあります。
例えば、Number型によるゼロ除算はInfinityを返します。

> 1.0 / 0.0 
Infinity

> (-1.0) / 0.0
-Infinity

異なる型同士での四則演算を行うことはできません。
例えば、42 + 3.14IntNumberの足し算になるのでエラーとなります。
これを計算するには、代わりに42.0 + 3.14と記述することでNumber同士の足し算をします。

> 42.0 + 3.14
45.14

Char

JavaScriptで文字列を扱う型はString型のみです。
PureScriptではChar型もあります。これはUnicodeにおけるコードユニットを表します。
ダブルクォーテーション"で囲むとString扱い、シングルクォーテーション'で囲むとChar扱いとなります。

> :type "A"
String

> :type 'A'
Char

文字列結合

文字列の結合には+ではなく<>を用います。

> "Hello, " <> "world!"
"Hello, world!"

PureScriptでは配列の結合(concat)にも演算子が用意されており、これにも<>を使います。

> [1, 2, 3] <> [4, 5]
[1,2,3,4,5]

これは、StringCharの配列と見なせることに思い至ると、文字列の結合と配列の結合に同じ記号を使うことは理にかなっています。

(不変)変数

JavaScriptでは変数の宣言時にvar, let, constなどを記述しますが、PureScriptでは単に変数名と値を=で繋ぎます。

> answer = 42

これはJavaScriptで言うところのconst宣言に相当します。

const answer = 42;

変数を使って84 \div 42を計算します。

> 84 / answer
2

変数を宣言する際に、型と一緒に宣言することができます。

secondAnswer :: Int
secondAnswer = 42

REPLで試す場合は、:pasteによる複数行モードで入力します。

> :paste
… secondAnswer :: Int
… secondAnswer = 42
…

一般にPureScriptのコードを書く際は、型と一緒に宣言することが推奨されます。
ただし、REPL中は「コードの下書き」的な意味合いが強いため、宣言に強くこだわる必要はありません。

if

JavaScriptのif文によく似た、if ... then ... else ...という構文があります。
JavaScriptとは異なり、if中の条件式にカッコ( ){ }は必要ありません。
ただし、elseを省略することはできません。
PureScriptのif A then B else CはJavaScriptの三項演算子A ? B : Cに相当します。

if ~~~ then ... else ...~~~部分にBoolean型の値を指定します。
Boolean型は2つの値true, falseを持つ型です。

> if true then "Yes" else "No"
"Yes"

> if false then "Yes" else "No"
"No"

これは文ではなく式ですので、結果を変数で受けることができます。

reply :: String
reply = if true then "Yes" else "No" -- "Yes"

else if

JavaScriptとは異なり、else ifという構文はありませんが、if ... then ... else ...を組み合わせるとelse ifが自然と現れます。

> if false then "Yes1" else if true then "Yes2" else "No"
"Yes2"

これは次のように解釈されます。

if false then "Yes1" else (if true then "Yes2" else "No")

複数組み合わせる際は、改行して適切なインデントと共に記述すると読みやすくなります。

if false then
  "Yes1"
else if true then
  "Yes2"
else
  "No"

REPL中で試すには、複数行モードで入力します。
※複数行モードを終了するにはCtrlキーを押しながらDキーを押します。

> :paste
… if false then
…   "Yes1"
… else if true then
…   "Yes2"
… else
…   "No"
…
"Yes2"

否定、論理積、論理和

JavaScriptとは異なり、否定には!ではなくnotを用います。
notの後には半角スペースを入れます。

> not true
false

> not false
true

かつ&&とまたは||はJavaScriptと同様です。

> true && false
false

> true || false
true

等性

同じ型の値abがあるとき、この等しさの比較には==を使います。

> 1 == 1
true

> 1 == 2
false

JavaScriptとは異なり、===はありません。
ただし、==で型が異なる2つの値を比較しようとするとエラーとなります。
つまり、42 == 42.042 == "42"はエラーとなります。

不等性

等しくないときにtrueを返したい場合は/=を用います。
これは不等号の記号\neqに似ています。

> 4 /= 4
false

> 4 /= 5
true

一般に、a /= bnot (a == b)は同じ結果を返します。

<=>=<>はJavaScript同様に使えます。

関数

PureScriptでは次のように関数を定義します。

> increment n = n + 1

これはJavaScriptで言う次の記述に相当します。

function increment(n) {
    return n + 1;
}

関数に引数を与えるには次のようにします。

> increment 42
43

これはJavaScriptで言う次の記述に相当します。

increment(42)

関数の型を調べてみましょう。

> :type increment
Int -> Int

一般に、型Aの値を引数に取り、型Bの値を返す関数の型はA -> Bと表されます。

先ほどの否定notを思い出してください。
PureScriptではこれもただの関数です。

not :: Boolean -> Boolean

つまり、not trueは関数notの引数にtrueを渡しているだけです。

また、関数を型を一緒に宣言する場合は次のように書きます。

increment :: Int -> Int
increment n = n + 1

複数の引数を持つ関数

複数の引数を持つ関数は次のように定義します。

> calc x y = x + 2 * y

関数に引数を与えるには次のようにします。

> calc 3 4
11

関数の型を調べてみましょう。

> :type calc
Int -> Int -> Int

一般に、型Aの値と型Bの値を引数に取り、型Cの値を返す関数の型はA -> B -> Cと表されます。

A1の値, 型A2の値, ..., 型Anの値の計n個の引数を取り、型Bの値を返す関数の型はA1 -> A2 -> ... -> An -> Bと表されます。

無名関数

関数は次のようにバックスラッシュ\を用いて次のようにも定義できます。
これはギリシア文字\lambdaに似ています。

> incrementArrow = \n -> n + 1

これはJavaScriptで言うところのアロー関数に近い構文です。

const incrementArrow = n => n + 1;

バックスラッシュによる関数の定義を用いると、次のように関数に名前を与えることなく使うことができます。

> (\x -> x * x) 5 
25

PureScriptでは、関数適用に( )が不要であることを思い出してください。
ちなみに、先ほどの記述はJavaScriptでは次の記述に相当します。

(x => x * x)(5);

なお、引数が複数ある関数もバックスラッシュを用いて定義することができます。

> calc = \x y -> x + 2 * y

配列

JavaScriptと同様に、[ ],を用いて配列を表します。

> :type [1, 2, 3, 4, 5]
Array Int

> :type [1.0, 2.0, 3.0, 4.0, 5.0]
Array Number

一般に、「型Xの配列」の型はArray Xと表されます。
JavaScriptとは異なり、1つの配列に異なる型の値を混ぜることはできません。
例えば、[22, 32, "42", 52]IntStringが混ざっているためエラーとなります。

また、PureScriptには参照の概念は無く、配列の等しさは全要素が等しいかどうかで判定されます。

> [1, 2] <> [3, 4, 5] == [1, 2, 3, 4, 5]
true

PureScriptにはインスタンスメソッドの概念は無いため、Array.prototype.mapを使うことはできません。
ただし、これに相当する関数mapが用意されています。
mapは第1引数に関数、第2引数に配列を指定します。

例えば次のように使います。

> increment n = n + 1
> map increment [1, 2, 3, 4, 5]
[2,3,4,5,6]

> map (\x -> x * x) [1, 2, 3, 4, 5]
[1,4,9,16,25]

> map (\str -> str <> "!") ["Apple", "Banana", "Chocolate"]
["Apple!","Banana!","Chocolate!"]
パイプライン演算子

どうしてもJavaScriptの語順で書きたい場合は#を使います。

[1, 2, 3, 4, 5] # map (\x -> 2 * x)

これはJavaScriptの語順に一致します。

[1, 2, 3, 4, 5].map(x => 2 * x)

PureScriptのmapは本来2引数の関数のはずですが、
先ほどの例では1つの引数\x -> 2 * xしか与えていません。
この挙動を理解するにはカリー化と部分適用の知識が必要であり、本記事の対象外ですので説明は割愛します。

また、x # ff xと同じです。
パイプライン演算子と呼ばれることがあります。

> 5 # (\n -> n + 1)
6

レコード

JavaScriptで言うところのプレーンオブジェクトはPureScriptではレコードと呼ばれます。
JavaScriptと同様に、レコードは{ }:,を用いて表します。

> :type { id: 5, name: "Alice" }
{ id :: Int
, name :: String
}

レコードの各値にアクセスする方法はJavaScriptと同様に.を使います。

> bob = { id: 3, name: "Bob" }
> bob.id
3

> bob.name
"Bob"

PureScriptには参照の概念は無いため、レコードの等しさは各キーとその値が等しいかどうかで判定されます。

> { id: 5, name: "Alice" } == { id: 5, name: "Alice" }
true

プロジェクト作成

新しくフォルダを作り、cdコマンド等で移動します。そして、次のコマンドを実行します。

spago init

これにより、srcフォルダ等のファイルが生成されます。
特に、src/Main.pursファイルにPureScriptコードが記載されています。

次のコマンドを実行することで、PureScriptコードがJavaScriptコードにコンパイルされます。

spago build

JavaScriptコードはoutputフォルダに生成されます。
src/Main.pursoutput/Main/index.jsに変換されます。

Hello World

src/Main.pursを次のように書き換えます。

module Main where

import Prelude

import Effect (Effect)
import Effect.Console (log)

main :: Effect Unit
main =
  log "Hello, PureScript!"

編集を保存したらspago runを実行します。

すると、mainが実行され、Hello, PureScript!が出力されるはずです。

output/Main/index.jsoutput/Effect.Console/foreign.jsを直接見ると分かる通り、PureScriptのlog関数は最終的にJavaScriptのconsole.logに相当する処理を行います。

モジュールとインポート

JavaScriptではファイル名を指定してインポートを行いますが、PureScriptではモジュール名を使ってインポートを行います。
モジュール名はそれぞれのファイルの1行目で宣言し、module ... whereという構文を使用します。
モジュール名はファイル名に一致するように命名します。

モジュールをインポートするにはimport ... ( ... )を用います。
例えば、標準のEffect.Consoleモジュールからlogをインポートするには次のように記述します。

import Effect.Console (log)

これはJavaScriptにおける次の記述に相当します。

import { log } from "Effect.Console";

インポートしたいものが同一モジュールに複数ある場合、それをカンマ,で区切ります。

import ModuleName (x, y, z)

また、名前を指定して個別にインポートするだけではなく、asで別名を付けて一括でインポートすることもできます。

import Effect.Console as Console

これはJavaScriptでは次の記述に相当します。

import * as Console from "Effect.Console";

別名Consoleを付けてインポートした場合、logにアクセスするにはConsole.logと記述します。

  module Main where
  
  import Prelude
  
  import Effect (Effect)
- import Effect.Console (log)
+ import Effect.Console as Console
  
  main :: Effect Unit
  main = do
-   log "Hello, PureScript!"
+   Console.log "Hello, PureScript!"

第3のインポート記法として単にimportとモジュール名を書く方法もあります。

import Prelude

この記法はPreludeモジュールに対してのみ使用することを推奨します。

log関数

REPLを用いてlog関数の型を確認してみましょう。

> import Effect.Console (log)
> :type log
String -> Effect Unit

logは型Stringの値を引数にとり、型Effect Unitの値を返す関数です。
ここで、配列の型がArray NumberArray Stringで表されたことを思い出してください。
Effect Unitも同様にこれひとまとまりで1つの型です。

log関数を文字列に適用すると型Effect Unitの値が得られます。
これもREPLで確認してみましょう。

> :type log "Hello, world!"
Effect Unit

したがって、mainの型はEffect Unitとなります。

JSの実行

先ほどはspago runコマンドを用いてPureScriptコードを実行しました。
このコマンドでは、PureScriptコードはJavaScriptコードに変換され、Node.jsによって実行されます。
JavaScriptファイルを生成するにはspago buildを実行します。

生成したJavaScriptファイルを実行してみましょう。

node output/Main/index.js

何も起きないはずです。

というのも、PureScriptで定義したmainは、JavaScriptに変換されてmainという名前のただの関数となるからです。
一般に、PureScriptにおける型Effect Unitの値は、JavaScriptでは引数も返り値も持たない関数() => { /* 処理 */ }になります。
したがってこの関数を呼び出すまで処理が実行されることはありません。

ドミノ倒しに喩えると、PureScriptはドミノを並べる(main関数を定義する)ところまでを行います。
実際にドミノを倒す(main関数を実行する)のはJavaScriptで行います。

そこで、プロジェクトルートにindex.jsファイルを新規作成し、次のように記述します。

const Main = require("./output/Main/index");
Main.main();

ビルド後のJavaScriptファイルの相対パスを指定してCommonJSとしてインポートし、そのmain関数を実行するだけのコードです。
それではnodeコマンドで実行してみましょう。

node index.js

出力結果:

Hello, PureScript!

無事に出力されるはずです。

ES Module

index.jsindex.mjsにリネームし、内容を次のように書き換えてES Module対応させます。

import { main } from "./output/Main/index.js";
main();

node index.mjsを実行すると先ほどと同じように動作するはずです。

ブラウザでの実行

今度はブラウザのコンソールに出力してみましょう。
先ほど作成したindex.mjsをHTMLから読み込むだけです。
Webpackに精通している方はWebpackを使うと良いでしょう。
バンドルツールにこだわりの無い方や、あまり詳しくないという方はParcelを使うことをお勧めします。

`webpack.config.js`の設定例

webpack, webpack-cli, webpack-dev-server, html-webpack-pluginをインストールします。

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  entry: "./index.mjs",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  plugins: [ new HtmlWebpackPlugin() ],
  devServer: {
    port: 1234,
  },
};

開発サーバーを立てるにはnpx webpack serveを実行します。

本記事ではParcelを使います。
まず、npm initコマンド等でpackage.jsonファイルを作成します。

次にparcelnpm経由でインストールします。

npm install --save-dev parcel@next

更に、index.htmlファイルを次のように編集して保存します。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>PureScript App</title>
</head>
<body>
    <script type="module" src="index.mjs"></script>
</body>
</html>

最後に、次のコマンドを実行します。

npx parcel index.html

すると、http://localhost:1234/に開発サーバーが立つのでブラウザでアクセスします。

開発者用コンソールを開くとHello, PureScript!と出力されているはずです。

ホットリロード

開発サーバーを起動している間にMain.pursを変更すると、それに合わせてホットリロードが行われます。

試しにsrc/Main.pursの内容を次のように書き換えます。

  module Main where
  
  import Prelude
  
  import Effect (Effect)
  import Effect.Console (log)
  
  main :: Effect Unit
  main = do
-   log "Hello, PureScript!"
+   log "reload!"

ファイルを保存すると、ブラウザに変更が反映されるはずです。

Reactの導入

Reactを使うためにreactreact-domをインストールします。

npm install --save react react-dom

次に、Reactの描画先となるdivタグをindex.htmlに追加します。
JavaScriptで拾いやすくするためにIDも設定します。

  <!DOCTYPE html>
  <html>
  <head>
      <meta charset="UTF-8">
      <title>PureScript App</title>
  </head>
  <body>
+   <div id="purescript-app"></div>
    <script type="module" src="index.mjs"></script>
  </body>
  </html>

index.mjsも次の内容に書き換えます。

import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(
    React.createElement("div", null, "Hello, React!"),
    document.getElementById("purescript-app")
);

これにより、ブラウザのコンソールではなく画面にHello, React!と表示されるようになります。

PureScriptと連携する

src/Main.pursを次のように書き換えます。

module Main where

appText :: String
appText = "Hello, PureScript!"

これにより、output/Main/index.jsでは main関数が無くなり、代わりにappTextがエクスポートされるようになります。
つまり、spago runを使うことはできなくなるので注意です。

それでは早速appTextindex.mjsで使ってみましょう。

  import React from "react";
  import ReactDOM from "react-dom";
+ import { appText } from "output/Main/index.js";

  ReactDOM.render(
-     React.createElement("div", null, "Hello, React!"),
+     React.createElement("div", null, appText),
      document.getElementById("purescript-app")
  );

以上により、PureScriptで定義した文字列をブラウザで表示することができました。

PureScriptのReactパッケージをインストールする

PureScriptでReactを使うためにPureScriptのパッケージをインストールします。
PureScriptのパッケージはnpmではなくspagoからインストールします。
次のコマンドを用いてreact-basic, react-basic-dom, react-basic-hooksの3つのパッケージをインストールします。

spago install react-basic react-basic-dom react-basic-hooks

各パッケージの詳細はPursuitから参照できます。

Pursuitを参照する上での注意点として、react-domというパッケージもありますが、これは基本的にreact-basic-domとは異なるので注意してください。
本記事で扱うのは後者のreact-basic-*パッケージです。

PureScriptで仮想DOMを定義する

src/Main.pursを次のように書き換えます。

module Main where

import React.Basic.DOM as R
import React.Basic.Hooks as React

view :: React.JSX
view = R.div_ [ R.text "Hello, PureScript!!" ]

これに合わせてindex.mjsも次のように書き換えます。

  import React from "react";
  import ReactDOM from "react-dom";
- import { appText } from "output/Main/index.js";
+ import { view } from "./output/Main/index.js";

  ReactDOM.render(
-     React.createElement("div", null, appText),
+     view,
      document.getElementById("purescript-app")
  );

これにより、PureScriptでReact.JSXを定義し、ブラウザに描画することができました。

React.JSXはその名に反してJSX構文とは関係ありません。
単にReact.createElementの返り値を表す型だと考えて良いでしょう。

React DSL

先ほど用いたR.textは、文字列を引数にとり、型React.JSXの値を返す関数です。

text :: String -> React.JSX

R.div_は、「React.JSXの配列」すなわちArray React.JSXを引数にとり、型React.JSXの値を返す関数です。
引数には子要素の配列を指定します。

div_ :: React.JSX Array -> React.JSX

したがって、先ほどのviewの定義は特別な構文を使っているわけではなく、
単に配列を作成したり、関数を適用したりしているだけです。
そして、この型React.JSXの値をReactDOM.renderに渡すことで描画させることができます。

タグにclassなどの属性を付ける場合はdiv_関数ではなく、div関数を使います。
div関数はArray React.JSXの代わりにレコードを引数にとります。
childrenキーに子要素の配列を指定するとdiv_関数と同様の挙動になります。

view :: React.JSX
view = R.div { children: [ R.text "Hello, PureScript!" ] }

ここで、例えばdivタグにクラス名を指定する場合はclassNameキーを使います。

view :: React.JSX
view = R.div { className: "content"
             , children: [ R.text "Hello, PureScript!" ] }

これは次のような記述に相当します。

<div class="content">Hello, PureScript!</div>

レコードのキーとして指定できる属性はタグ毎に異なります。例えばdivタグに指定できる属性はReact.Basic.DOM#Props_divに定義されています。

React.Basic.DOMモジュールには、divだけではなく様々なHTMLタグに対応する関数が定義されています。
例えば次のように記述します。

view :: React.JSX
view = R.div
  { className: "content"
  , children:
    [ R.ol
      { className: "is-lower-alpha"
      , children:
        [ R.li_ [ R.text "Apple" ]
        , R.li_ [ R.text "Banana" ]
        , R.li_ [ R.text "Chocolate" ]
        ]
      }
    ]
  }

これは次のような記述に相当します。

<div class="content">
    <ol class="is-lower-alpha">
        <li>Apple</li>
        <li>Banana</li>
        <li>Chocolate</li>
    </ol>
</div>

重要なのは、childrenキーに指定するのはただのReact.JSXの配列であるという点です。
したがって、例えば文字列の配列からReact.JSXの配列を生成してそのままchildrenに指定することができます。

試しにPreludeモジュールに定義されているmap関数を使い、
Array StringからArray React.JSXを生成して使ってみます。

favoriteFoods :: Array String
favoriteFoods = [ "Apple", "Banana", "Chocolate" ]

view :: React.JSX
view = R.div
  { className: "content"
  , children:
    [ R.ol
      { className: "is-lower-alpha"
      , children:
        map (\food -> R.li_ [ R.text food ]) favoriteFoods
      }
    ]
  }
コード全文
module Main where

import Prelude

import React.Basic.DOM as R
import React.Basic.Hooks as React

favoriteFoods :: Array String
favoriteFoods = [ "Apple", "Banana", "Chocolate" ]

view :: React.JSX
view = R.div
  { className: "content"
  , children:
    [ R.ol
      { className: "is-lower-alpha"
      , children:
        map (\food -> R.li_ [ R.text food ]) favoriteFoods
      }
    ]
  }

スタイル

直接スタイルを指定したい場合、タグのレコードのstyleキーに型CSSの値を指定します。
CSSの値はR.css関数から作成します。この関数もレコードを引数にとり、CSS値を返します。

R.div
  { style:
    R.css
      { color: "red"
      , fontSize: "20px"
      }
  , children:
    [ R.text "This is red."
    ]
  }

※PureScriptのレコードで表す際、CSSのプロパティ名はkebab-caseではなくcamelCaseで指定しています。

これは次のような記述に相当します。

<div style="color: red; font-size: 20px;">This is red.</div>
コード全文
module Main where

import Prelude

import React.Basic.DOM as R
import React.Basic.Hooks as React

view :: React.JSX
view = R.div
  { className: "content"
  , children:
    [ R.ol
      { className: "is-lower-alpha"
      , children:
        map (\food -> R.li_ [ R.text food ]) favoriteFoods
      }
    ]
    <>
    [ R.div
      { style:
        R.css
          { color: "red"
          , fontSize: "20px"
          }
      , children:
        [ R.text "This is red."
        ]
      }
    ]
  }

イベント

イベントなどに応じてコードを実行したい場合はそれをタグのレコードに記述します。
例えば、クリックイベントを拾う場合のキーはonClickです。
そのキーには、型EventHandlerの値を指定します。

※イベントの命名規則や処理等はReactの公式ドキュメントを参照してください。

さて、今回はクリックされたことをコンソールに出力することとします。
そのためにEffect.Consoleモジュールのlog関数を用います。

log :: String -> Effect Unit

Effect Unitの値から型EventHandlerの値を生成するには、
React.Basic.Eventsモジュールのhandler_関数を使います。

handler_ :: Effect Unit -> EventHandler

例えば、buttonタグを作成し、クリックされたときにclickedとコンソールに出力してみます。

R.button
  { onClick: handler_ (Console.log "clicked")
  , children: [ R.text "Click me!" ]
  }

これにより、クリックするたびにコンソールに"clicked"と出力されます。

コード全文
module Main where

import Prelude

import Effect.Console as Console
import React.Basic.DOM as R
import React.Basic.Events (handler_)
import React.Basic.Hooks as React

favoriteFoods :: Array String
favoriteFoods = [ "Apple", "Banana", "Chocolate" ]

view :: React.JSX
view = R.div
  { className: "content"
  , children:
    [ R.ol
      { className: "is-lower-alpha"
      , children:
        map (\food -> R.li_ [ R.text food ]) favoriteFoods
      }
    ]
    <>
    [ R.div
      { style:
        R.css
          { color: "red"
          , fontSize: "20px"
          }
      , children:
        [ R.text "This is red."
        ]
      }
    , R.button
      { onClick: handler_ (Console.log "clicked")
      , children: [ R.text "Click me!" ]
      }
    ]
  }

関数に分割する

view関数が肥大化してきたので、まとまり毎の関数に分割してみましょう。

まず、食べ物の文字列を受け取って、それをliタグに変換する関数renderFoodを作成します。

renderFood :: String -> React.JSX
renderFood food =
  R.li_ [ R.text food ]

次に、文字列の配列を受け取って、それをulタグに変換する関数renderFoodsを作成します。
これは先ほど作った関数renderFoodを使って実装できます。

renderFoods :: Array String -> React.JSX
renderFoods foods =
  R.ol
    { className: "is-lower-alpha"
    , children: map renderFood foods
    }

赤い文字で表示する部分も関数に分割します。

renderBigRedText :: String -> React.JSX
renderBigRedText text =
  R.div
    { style:
      R.css
        { color: "red"
        , fontSize: "20px"
        }
    , children: [ R.text text ]
    }

これらはただのPureScriptの関数ですので、viewで使うことができます。

view :: React.JSX
view = R.div
  { className: "content"
  , children:
    [ renderFoods favoriteFoods
    , renderBigRedText "This is red."
    , R.button
      { onClick: handler_ (Console.log "clicked")
      , children: [ R.text "Click me!" ]
      }
    ]
  }
コード全文
module Main where

import Prelude

import Effect.Console as Console
import React.Basic.DOM as R
import React.Basic.Events (handler_)
import React.Basic.Hooks as React

favoriteFoods :: Array String
favoriteFoods = [ "Apple", "Banana", "Chocolate" ]

renderFood :: String -> React.JSX
renderFood food =
  R.li_ [ R.text food ]

renderFoods :: Array String -> React.JSX
renderFoods foods =
  R.ol
    { className: "is-lower-alpha"
    , children:
    map renderFood foods
    }

renderBigRedText :: String -> React.JSX
renderBigRedText text =
  R.div
    { style:
      R.css
        { color: "red"
        , fontSize: "20px"
        }
    , children: [ R.text text ]
    }

view :: React.JSX
view = R.div
  { className: "content"
  , children:
    [ renderFoods favoriteFoods
    , renderBigRedText "This is red."
    , R.button
      { onClick: handler_ (Console.log "clicked")
      , children: [ R.text "Click me!" ]
      }
    ]
  }

changeイベント

inputタグを追加し、入力されるたびに発火されるイベントを拾ってみます。
ReactではonInputではなくonChangeですので注意してください。

  view :: React.JSX
  view = R.div
    { className: "content"
    , children:
      [ renderFoods favoriteFoods
      , renderBigRedText "This is red."
      , R.button
        { onClick: handler_ (Console.log "clicked")
        , children: [ R.text "Click me!" ]
        }
+     , R.input
+       { type: "text"
+       , onChange: handler_ (Console.log "changed")
+       }
      ]
    }

イベントの中身を拾うには、handler_の代わりにhandlerを使います。

handlerは2つの引数を取る関数で、最終的にEventHandlerを返します。
第1引数には、Reactの合成イベントeから何を取得するかを指定します。
第2引数には、第1引数で取得した値を使ってどのような処理を行うかを指定します。

onChangeイベントで入力値を拾ってそれをコンソールに出力するには次のように記述します。

  view :: React.JSX
  view = R.div
    { className: "content"
    , children:
      [ renderFoods favoriteFoods
      , renderBigRedText "This is red."
      , R.button
        { onClick: handler_ (Console.log "clicked")
        , children: [ R.text "Click me!" ]
        }
      , R.input
        { type: "text"
-       , onChange: handler_ (Console.log "changed")
+       , onChange: handler targetValue (traverse_ (\str -> Console.log ("changed: " <> str)))
        }
      ]
    }

実用上はonChangeに対してhandler, targetValue, traverse_をセットで使うことになります。
順を追って説明していきます。

まず、handlerの第1引数にtargetValueを指定しています。
これはJavaScriptにおいて、Reactの合成イベントeに対してe.target.valueを取得する処理に相当します。
このライブラリでは値e.target.valueの型はMaybe Stringと表されています。

MaybeはPureScriptでは広く使われる型の1つで、処理に失敗するかもしれない場合によく使われます。

`Maybe`について

REPLでMaybeを使ってみましょう。
まずData.MaybeモジュールからMaybe(..)をインポートします。

> import Data.Maybe (Maybe(..))

これにより、JustNothingを使うことができるようになります。

Justは値を持つことを表すMaybeを作成する関数です。

> Just 42
(Just 42)

> :type Just 42
Maybe Int

> Just "Hello"
(Just "Hello")

> :type Just "Hello"
Maybe String

Nothingは値を持たないことを表します。

> Nothing :: Maybe Int
Nothing

> Nothing :: Maybe String
Nothing

Maybeは要素を1つ持つ場合(Just)と、要素を持たない場合(Nothing)に分けられます。
言い換えるとMaybeは要素を高々1つ持つ配列Arrayと見なすことができます。
この観点で、結合<>を使えます。

> ["Hello"] <> []
["Hello"]

> (Just "Hello") <> Nothing
(Just "Hello")

> [] <> ["World"]
["World"]

> Nothing <> (Just "World")
(Just "World")

なお、Just同士の連結は要素同士の連結になります。

> ["Hello"] <> ["World"]
["Hello","World"]

> (Just "Hello") <> (Just "World")
(Just "HelloWorld")

Maybeはあくまでも要素を1つまでしか持たない点に注意してください。

第2引数には、targetValueの結果であるMaybe Stringから、Effect Unitへの関数を指定します。
Maybe Stringから中の文字列を取り出すために、今回はData.Foldableモジュールのtraverse_関数を用いています。
これは配列の各要素に対して指定の処理を順番に実行します。

コード全文
module Main where

import Prelude

import Data.Foldable (traverse_)
import Effect.Console as Console
import React.Basic.DOM as R
import React.Basic.DOM.Events (targetValue)
import React.Basic.Events (handler, handler_)
import React.Basic.Hooks as React

favoriteFoods :: Array String
favoriteFoods = [ "Apple", "Banana", "Chocolate" ]

renderFood :: String -> React.JSX
renderFood food =
  R.li_ [ R.text food ]

renderFoods :: Array String -> React.JSX
renderFoods foods =
  R.ol
    { className: "is-lower-alpha"
    , children:
    map renderFood foods
    }

renderBigRedText :: String -> React.JSX
renderBigRedText text =
  R.div
    { style:
      R.css
        { color: "red"
        , fontSize: "20px"
        }
    , children: [ R.text text ]
    }

view :: React.JSX
view = R.div
  { className: "content"
  , children:
    [ renderFoods favoriteFoods
    , renderBigRedText "This is red."
    , R.button
      { onClick: handler_ (Console.log "clicked")
      , children: [ R.text "Click me!" ]
      }
    , R.input
      { type: "text"
      , onChange: handler targetValue (traverse_ (\str -> Console.log ("changed: " <> str)))
      }
    ]
  }

まとめ

PureScriptのインストールおよび基礎文法の一部を確認し、react-basic-*パッケージを用いてReactの仮想DOMをPureScriptコード上で扱う方法について紹介しました。

更なる内容(Reactコンポーネントの定義やフックの利用など)については反響があれば続きを書きたいと思います。

GitHubで編集を提案

Discussion