Reactを題材にPureScriptを始める
はじめに
本記事では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
経由でpurescript
とspago
をインストールします。
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では42
と42.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.14
はInt
とNumber
の足し算になるのでエラーとなります。
これを計算するには、代わりに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]
これは、String
をChar
の配列と見なせることに思い至ると、文字列の結合と配列の結合に同じ記号を使うことは理にかなっています。
(不変)変数
JavaScriptでは変数の宣言時にvar
, let
, const
などを記述しますが、PureScriptでは単に変数名と値を=
で繋ぎます。
> answer = 42
これはJavaScriptで言うところのconst
宣言に相当します。
const answer = 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
等性
同じ型の値a
とb
があるとき、この等しさの比較には==
を使います。
> 1 == 1
true
> 1 == 2
false
JavaScriptとは異なり、===
はありません。
ただし、==
で型が異なる2つの値を比較しようとするとエラーとなります。
つまり、42 == 42.0
や42 == "42"
はエラーとなります。
不等性
等しくないときにtrue
を返したい場合は/=
を用います。
これは不等号の記号
> 4 /= 4
false
> 4 /= 5
true
一般に、a /= b
とnot (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
と表されます。
無名関数
関数は次のようにバックスラッシュ\
を用いて次のようにも定義できます。
これはギリシア文字
> 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]
はInt
とString
が混ざっているためエラーとなります。
また、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 # f
はf 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.purs
はoutput/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.js
やoutput/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 Number
やArray 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.js
をindex.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
ファイルを作成します。
次にparcel
をnpm
経由でインストールします。
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を使うためにreact
とreact-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
を使うことはできなくなるので注意です。
それでは早速appText
をindex.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(..))
これにより、Just
とNothing
を使うことができるようになります。
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コンポーネントの定義やフックの利用など)については反響があれば続きを書きたいと思います。
Discussion