Elm でランダムにテキストを表示させる

所用で(?)シンプルな web アプリを作ることになり、Elm で書いてみることにした。

要件としてはルーレットのような感じで文章をシャッフルしてランダムに表示するもの。もう少し作り込む予定だけれどひとまず載せておこう。

とりあえずの感想

  • 見よう見まねで書いていると値は immutable ということを忘れがちだった。
  • エラーメッセージが親切なのでギリギリやっていけるという感じ。
  • バージョンが上がるごとに結構いろいろ API が変わっているようで、ブログ等の情報が参考にならない場合も多い印象。
  • update の処理を連鎖させるのはどうしたらいいのか、よくわからなかった。
    • 結局 Task.perform で繋ぐ感じでやっているが正しいのか不明。
  • 公式のフォーマッター elm-format があるので助かる。
    • でもインデントがずれていると動かなかったりするので Go の gofmt と比べると少々頼りない気がする。

もし、JavaScript で書いたとしたら、オンラインで公開されている参考にできるサンプルの数が多いので、おそらく半分くらいの時間でできたのではないかと思う。

まぁこうやって新しい概念に触れて、チャレンジしてみるのも大事だからね。

コード

Elm はシンタックスハイライトに対応していないか、そうか。

(190404 追記)
言語指定を Haskell にすることでそれっぽい感じでハイライトされるようになった。

module Main exposing (Model, Msg(..), init, main, subscriptions, update, view)

import Array
import Browser
import Html exposing (..)
import Html.Events exposing (..)
import Random
import Task
import Time



-- MAIN


main =
    Browser.element
        { init = init
        , update = update
        , subscriptions = subscriptions
        , view = view
        }



-- MODEL


type alias Model =
    { sentences : List String
    , index : Int
    , sentence : String
    , shuffling : Bool
    , count : Int
    }


init : () -> ( Model, Cmd Msg )
init _ =
    ( { sentences = sampleSentences
      , index = 0
      , sentence = ""
      , shuffling = False
      , count = 0
      }
    , Cmd.none
    )



-- https://www.thetoptens.com/random-sentences/


sampleSentences : List String
sampleSentences =
    [ "I am so blue I'm greener than purple."
    , "I stepped on a Corn Flake, now I'm a Cereal Killer."
    , "Everyday a grape licks a friendly cow."
    , "Llamas eat sexy paper clips."
    , "Banana error."
    , "Don't touch my crayons, they can smell glue."
    , "There's a purple mushroom in my backyard, screaming Taco's!"
    ]


maxCount : Int
maxCount =
    20



-- UPDATE


type Msg
    = Pick
    | GetIndex Int
    | Tick Time.Posix
    | CheckCount Time.Posix
    | Shuffle Time.Posix


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Pick ->
            ( { model | shuffling = True }
            , Random.generate GetIndex (randomIndex model.sentences)
            )

        GetIndex i ->
            let
                s =
                    pickByIndex model.sentences i
            in
            ( { model | index = i, sentence = s }
            , Cmd.none
            )

        Tick newTime ->
            let
                c =
                    if model.shuffling then
                        model.count + 1

                    else
                        model.count
            in
            ( { model | count = c }
            , Task.perform CheckCount Time.now
            )

        CheckCount newTime ->
            ( resetStatus model
            , Task.perform Shuffle Time.now
            )

        Shuffle newTime ->
            ( shuffleSentence model
            , Cmd.none
            )


randomIndex : List String -> Random.Generator Int
randomIndex sentences =
    Random.int 0 <| List.length sentences - 1


pickByIndex : List String -> Int -> String
pickByIndex sentences index =
    let
        default =
            "this is default sentence."

        arr =
            Array.fromList sentences
    in
    Maybe.withDefault default (Array.get index arr)


resetStatus : Model -> Model
resetStatus model =
    if model.count == maxCount then
        let
            s =
                pickByIndex model.sentences model.index
        in
        { model | shuffling = False, count = 0, sentence = s }

    else
        model


shuffleSentence : Model -> Model
shuffleSentence model =
    let
        randomId =
            modBy (List.length model.sentences) model.count
    in
    let
        randomSentence =
            pickByIndex model.sentences randomId
    in
    if model.shuffling then
        { model | sentence = randomSentence }

    else
        model



-- SUBSCRIPTIONS


subscriptions : Model -> Sub Msg
subscriptions model =
    Time.every 40 Tick



-- VIEW


showBool : Bool -> String
showBool bool =
    if bool then
        "True"

    else
        "False"


view : Model -> Html Msg
view model =
    div []
        [ h1 [] [ text model.sentence ]
        , h2 [] [ text (String.fromInt model.count) ]
        , h2 [] [ text (showBool model.shuffling) ]
        , button [ onClick Pick ] [ text "Pick" ]
        ]

Ellie というオンライン開発環境(いわゆる playground 的なやつ)にも保存してみたけれど、表示されないかもしれない。