お先にこちらの記事も併せてお読みください。
Reactを普段書いている人がElmにあまり抵抗感を持たずに始めれるように、全く同じ挙動をするReactとElmのコードを並べてコードの説明をしていきたいと思います。あまり初歩的すぎる例だと実際に使うイメージが付かないだろうと言うことで、React Hooksを使い副作用を含むコードを2つ用意しました。注意点としてReactを使う成果物を私自身が久しぶりで、React Hooksの使用が初めてであると言うことと、Elmのコードに近くなるように、普段Reactを使っている方からすると不自然に見えるかもしれないコードであることをご了承ください(あらかじめコードはTwitterで展開し様々なコード例をいただいているので、興味がある方は私のTwitterを遡ってみるとおもしろいかもしれません)。
乱数カウンタ
副作用を簡単に確かめたり説明するときは、よく手軽に副作用を発生させれる乱数カウンタを例として作ります。乱数の扱いさえ分かれば、HTTP通信やDBの操作などはインターフェースが異なるAPIというだけで基本は同じだと考えています。
React
useState
を用いて、初期値0から始まり今のカウンタの数値を保存する状態count
を用意しました。乱数は1-10の値が入りuseRef
とuseEffect
を用いてレンダリングが走るごとに更新されるようにしています。onClick
関数では、乱数をcount
に足し合わせて更新を掛けています。
import React, {useState, useEffect, useRef} from 'react';
const getRandomInt = (max) => 1 + Math.floor(Math.random() * Math.floor(max));
function App() {
const [count, setCount] = useState(0);
const ref = useRef(0);
useEffect(() => {
ref.current = getRandomInt(10);
});
return (<div>
<p>Random countup: {count}</p>
<button onClick={
() => setCount(count + ref.current)
}>random countup</button>
</div>);
}
export default App;
Elm
Modelと言うのは習慣的に使われるアプリケーションの状態を扱う型の別名です(TypeScriptで言うところの、type Model = number;
と同じです)。今回はReactの例のcount
になります。全く同一にするのであれば、type Model = { count: Int }
と言うレコード(オブジェクト)の形にも出来ましたが、コードを単純にするためにこうなりました。
次にview
関数を見ると、Reactのコード例で計算をしてしまっていましたが、RandomCountup
と言うメッセージ(指令)を飛ばすだけです。
update
関数では、RandomCountupに関する処理の分岐があります。update関数の戻り値は、タプルで(Model, Cmd Msg)
という形になります。Modelで計算され値によって次のレンダリングがされるかどうかが変わります。つまりsetCount
関数と同様の効果があります。しかし、RandomCountupのタプルの最初の値を見ると、modelのままで値が加算されていません。これは、副作用によって得られる値はElmの関数内では、直接取得できないためです。代わりにRandom.generate GotRandomInt (Random.int 1 10)
(実際コードでは、<|
がありますが、これは括弧の省略だと思ってください)と言うCmd Msgの値が返されています。GotRandomIntは、Elmランタイム(JavaScript)から乱数を受け取るためのMsgです。Random.generateは、MsgとRandom.Generatorを受け取って副作用を発生させる方法です。ReactのuseEffectに当たる部分です。ElmにおいてCmdと次の例で見せるSubと言う方法でしか、副作用を起こす方法はありません。
GotRandomInt
の分岐を見てみましょう。GotRandom nのnがElmランタイムから受け取った乱数の値です。タプルの最初では、model + n
とmodelに乱数を足し合わせて更新しています。タプルの二つ目の値つまり、Cmdは副作用をこれ以上起こす必要はないため、Cmd.none
となります。
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, p, text)
import Html.Events exposing (onClick)
import Random
main =
Browser.element { init = init, update = update, view = view, subscriptions = \_ -> Sub.none }
type alias Model =
Int
init : () -> ( Model, Cmd Msg )
init _ =
( 0, Cmd.none )
type Msg
= RandomCountup
| GotRandomInt Int
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RandomCountup ->
( model, Random.generate GotRandomInt <| Random.int 0 10 )
GotRandomInt n ->
( model + n, Cmd.none )
view : Model -> Html Msg
view model =
div []
[ p [] [ text <| "Random countup: " ++ String.fromInt model ]
, button [ onClick RandomCountup ] [ text "random countup" ]
]
おまけ
Twitterのフォロワーの方が、Elmに完全に寄せたReactのコードを書いてくれました。記事では私自身の言葉で説明できる形を保ちたかったため、そのまま私のコードを載せました。pxfncさんありがとうございました!
タイマー
今回お見せするのは、タイマーです。タイマーは定期的に処理が走るタイプの副作用です。実務で言えば、重い処理をバックグラウンド処理として走らせ、その結果を定期的に確認するなどの処理が書けるようになります。
React
time
は、0で初期化されmsの単位で管理される数値です。加えて、タイマーを止めたときに今のtimeの値を保存する数値stoppedTime
とタイマーを動かした時間を保存するlastStartedAt
値、この3つの状態でタイマーは表現されます。タイマーの描画や状態の更新は、requestAnimationFrame
関数を利用して行われます。その他の細かいロジックに関しては本質的ではないため、省略します。
import React, { useState, useEffect } from "react";
export default function App() {
const [stoppedTime, setStoppedTime] = useState(0);
const [time, setTime] = useState(0);
const [lastStartedAt, setLastStartedAt] = useState(null);
const calcTime = () => {
return lastStartedAt === null
? stoppedTime
: stoppedTime + new Date().getTime() - lastStartedAt;
};
const onClick = () => {
if (lastStartedAt === null) {
setLastStartedAt(new Date().getTime());
return;
}
setStoppedTime(calcTime());
setLastStartedAt(null);
return;
};
useEffect(() => {
const requestId = requestAnimationFrame(() => setTime(calcTime()));
return () => cancelAnimationFrame(requestId);
});
return (
<div>
<p>timer (sec): {Math.floor(time / 1000)}</p>
<p>timer (ms): {time}</p>
<p>
<button onClick={onClick}>
{lastStartedAt === null ? "start" : "stop"}
</button>
</p>
</div>
);
}
このReactコードはフォロワーのn_1215さんに書いていただきました。ありがとうございます!
Elm
こちらのコードはReactとほぼ同じ構造のため、差分だけを説明します。
ModelのうちlastStartedAtMaybe
はあるかないか(nullが含まれるかどうか)の型は、Maybe a
と言う型で表されます。aには任意の型が入るため今回は、Intとなります。この値は通常のIntと演算はできないため、必ずパターンマッチで分岐(値があるJust a
か、無いか Nothing)するか無い場合のデフォルト値を設定する必要があり、コンパイル時にチェックされます。TypeScriptのnullとのユニオンの型と同じ感覚で使えば良いと思います。
requestAnimationFrameのように定期購読が必要なコードは全て、subscription
関数にまとめられます。今回はそのままrequestAnimationFrameのイベントをハンドリングするonAnimationFrame
と言う関数があるため、そちらを使用しました。Sub Msg型はCmdと使い方のノリは変わりありません。定期的に実行されたときにupdateで動くおなじみのMsgを設定してあげるだけになります。今回は、Tick Time.Posix
と言うUnix時間を表す型を受け取るMsgが働いてくれます。
他にはrequestAnimationFrame以外で現在時刻を受け取りたいときは、副作用なので乱数のときと同じようにCmd Msgを発行してあげる必要があります。例えば、Task.perform SetLastStartedAt Time.now
の部分がその部分に当たります。Task.performはRandom.generateと同様に加工可能な現在時刻を加工し終わった後にCmd Msg
に変換してくれる関数になります。(リファレンスやそれに関して記事をいくつか書いているため気になる人は調べてみてください)。
module Main exposing (main)
import Browser
import Browser.Events exposing (onAnimationFrame)
import Html exposing (Html, button, div, p, text)
import Html.Events exposing (onClick)
import Task
import Time
main =
Browser.element { init = init, update = update, view = view, subscriptions = subscriptions }
type alias Model =
{ time : Int
, stoppedTime : Int
, lastStartedAtMaybe : Maybe Int
}
init : () -> ( Model, Cmd Msg )
init _ =
( { time = 0, stoppedTime = 0,lastStartedAtMaybe = Nothing }, Cmd.none )
type Msg
= Tick Time.Posix
| StartStop
| SetLastStartedAt Time.Posix
| SetStoppedTime Int Time.Posix
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
let
calcTime now lastStartedAt =
model.stoppedTime + Time.posixToMillis now - lastStartedAt
in
case msg of
Tick now ->
( { model
| time =
case model.lastStartedAtMaybe of
Just lastStartedAt ->
calcTime now lastStartedAt
Nothing ->
model.stoppedTime
}
, Cmd.none
)
StartStop ->
case model.lastStartedAtMaybe of
Just lastStartedAt ->
( model, Task.perform (SetStoppedTime lastStartedAt) Time.now )
Nothing ->
( model, Task.perform SetLastStartedAt Time.now )
SetStoppedTime lastStartedAt now ->
( { model
| stoppedTime =
calcTime now lastStartedAt
, lastStartedAtMaybe = Nothing
}
, Cmd.none
)
SetLastStartedAt now ->
( { model | lastStartedAtMaybe = Just <| Time.posixToMillis now }, Cmd.none )
view : Model -> Html Msg
view model =
div []
[ p [] [ text <| "timer (sec): " ++ (String.fromInt <| model.time // 1000) ]
, p [] [ text <| "timer (ms): " ++ String.fromInt model.time ]
, button [ onClick StartStop ]
[ text <|
case model.lastStartedAtMaybe of
Just _ ->
"stop"
Nothing ->
"start"
]
]
subscriptions : Model -> Sub Msg
subscriptions _ =
onAnimationFrame Tick
他の副作用を含むElmの例
上記の2つのコードが読めてしまえば、基本的にElmの副作用を含むコードを読むことができるようになります。Elm公式ではexamplesがいくつか公開されています。Random, Http, Timeがそれに該当するためよければご覧ください。
まとめ
この記事では単発での副作用と定期購読のための副作用を表現するコードをReactとElmでそれぞれ紹介しました。Reactを普段使う人がElmを使うイメージが掴むことができ、チャレンジする人が増えたら嬉しいなと思います。この記事を書くに当たって感じたことですが、記事の通りReactであろうがElmであろうが実現できることに変わりはありません(VueやAngularも言うまでもありませんね)。しかし、使うユーザの思考性は異なる部分があると感じました。Elmは私が書いたからこのようなコードになったわけではなく、Elmユーザが書いた場合、アルゴリズムを除いて間違いなく同じコードになります。これはCmdとSubの2つの方法でしか、副作用を表現できないためです。一方、Reactは僕とフォロワーでコードが十人十色でした。プログラミング自体を楽しみたいと言う方は、TypeScript+Reactの選択をすると良いかもしれません。一方、細かい書き分けの選択肢がない分Elmはプログラミング自体の幅を楽しみたい人にとっては苦痛かもしれませんが、逆にその幅が苦痛であったりアプリケーションの作成に集中したい方、プログラミング初心者の方にとってはElmを選択すると良いかもしれません。また、その特性だけにフォーカスした場合は円滑なプロダクトの進行はElmの方が良いと感じましたが、言語特性以外にもエコシステムやユーザ間の情報共有の円滑さを考えるとReactに分があると思います。しかしこれに関しては、ReactもElmも使えるユーザが増えると状況が変わってる話なので、是非Elmを触るユーザが増えていけば良いなと思いました。それでは良いReactとElmライフを!