LoginSignup
31
33

More than 3 years have passed since last update.

React使いのためのElm入門(React Hooks編)

Last updated at Posted at 2020-01-12

お先にこちらの記事も併せてお読みください。

Reactを普段書いている人がElmにあまり抵抗感を持たずに始めれるように、全く同じ挙動をするReactとElmのコードを並べてコードの説明をしていきたいと思います。あまり初歩的すぎる例だと実際に使うイメージが付かないだろうと言うことで、React Hooksを使い副作用を含むコードを2つ用意しました。注意点としてReactを使う成果物を私自身が久しぶりで、React Hooksの使用が初めてであると言うことと、Elmのコードに近くなるように、普段Reactを使っている方からすると不自然に見えるかもしれないコードであることをご了承ください(あらかじめコードはTwitterで展開し様々なコード例をいただいているので、興味がある方は私のTwitterを遡ってみるとおもしろいかもしれません)。

乱数カウンタ

副作用を簡単に確かめたり説明するときは、よく手軽に副作用を発生させれる乱数カウンタを例として作ります。乱数の扱いさえ分かれば、HTTP通信やDBの操作などはインターフェースが異なるAPIというだけで基本は同じだと考えています。

Image from Gyazo

React

useStateを用いて、初期値0から始まり今のカウンタの数値を保存する状態countを用意しました。乱数は1-10の値が入りuseRefuseEffectを用いてレンダリングが走るごとに更新されるようにしています。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さんありがとうございました!

タイマー

今回お見せするのは、タイマーです。タイマーは定期的に処理が走るタイプの副作用です。実務で言えば、重い処理をバックグラウンド処理として走らせ、その結果を定期的に確認するなどの処理が書けるようになります。

Image from Gyazo

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ライフを!

31
33
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
33