Chanomic Blog

Elmメモ - ランダムな位置に円を描画する

(last modified:
)

categories:

乱数の練習に。

準備

プロジェクト用のディレクトリを適当に作り、そこで以下のコマンドを実行。

$ elm init

必要なモジュールを入れる。

$ elm install elm/svg
$ elm install elm/random

Main.elmを作成し、最低限の文を定義しておく。

module Main exposing (..)

import Browser
import Svg exposing (..)
import Svg.Attributes as SA exposing (..)
import Svg.Events as SE exposing (..)
import Random

円の描画

こんな感じの円を描画する。

1

SVGでは次のように書く。

<svg width="100px" height="100px">
  <g transform="translate(50, 50)">
    <circle r="10" fill="white" stroke="black" />
    <text text-anchor="middle" dominant-baseline="central">1</text>
  </g>
</svg>

円の情報で必要なのは次の4つ:

そこで円は次のように定義する。

type alias Circle =
  { r: Float
  , x: Float
  , y: Float
  , text: String
  }

Elmでは宣言的にSVGやHTMLを書けるので、SVGの文法とほとんど似た構造でかける。直感的で嬉しい。

viewCircle : Circle -> Svg Msg
viewCircle { r, x, y, text } =
  Svg.g
    [ SA.transform <| translateAttr x y
    ]
    [ Svg.circle
        [ SA.r (String.fromFloat r)
        , SA.fill "white"
        , SA.stroke "black"
        ]
        []
    , Svg.text_
        [ SA.textAnchor "middle"
        , SA.dominantBaseline "central"
        ]
        [ Svg.text text
        ]
    ]

translateAttr : Float -> Float -> String
translateAttr x y =
  "translate("
  ++ String.fromFloat x
  ++ ","
  ++ String.fromFloat y
  ++ ")"

他は定型通りに書く。

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


type alias Model =
  { circles : List Circle
  }


init : () -> (Model, Cmd Msg)
init _ =
  ({ circles = 
      [ Circle 10 10 10 "1"
      , Circle 10 20 20 "2"
      , Circle 10 30 20 "3"
      , Circle 10 40 20 "4"
      , Circle 10 20 60 "5"
      , Circle 10 90 20 "6"
      ]
   }
  , Cmd.none
  )


type Msg
  = Dummy

update : Msg -> Model -> (Model, Cmd Msg)
update _ model = (model, Cmd.none)


subscriptions : Model -> Sub Msg
subscriptions _ = Sub.none


view : Model -> Svg Msg
view { circles } =
  svg
    [ SA.style "border: 1px solid #000; margin: 10px;"
    , width "600px"
    , height "600px"
    ]
    (List.map viewCircle circles)

(補足) レコード

Circleの定義はレコードだから、次のように定義するべきだろう。

{ r = 10
, x = 100
, y = 20
, text = "1"
}

しかし型エイリアスとしてレコードが定義された場合、次のようにCircleを定義することも可能。

Circle 10 100 20 "1"

Haskellのフィールドラベルに近いものを感じる。

乱数を試す

Elmでほとんど乱数を使ったことがないので、色々試す。

init : () -> (Model, Cmd Msg)
init _ =
  ({ circles = []
   }
  , Random.generate GetRandomNumber (Random.int 1 6)
  )

type Msg
  = GetRandomNumber Int

update : Msg -> Model -> (Model, Cmd Msg)
update msg model = 
  case msg of 
    GetRandomNumber n -> 
      let _ = Debug.log "Random" n
      in
        (model, Cmd.none)

ブラウザのデバッグコンソールを開くと、以下の文が出力される。

Random: [1-6のどれか]

Random.generate

乱数を作るcommandを返す。第2引数には乱数生成器を指定する。ここではRandom.int 1 6としている。これは、[1,6]Int型の乱数を作る生成器。乱数は第1引数のMsg型変数で受け取る。

様々な形の乱数生成器を作るための関数が用意されている。その一部については次で扱う。

(補足) Debug.log

いわゆるprintデバッグをするために使われる関数。

-- JSにおけるconsole.log(`Label: ${value}`)と同義
Debug.log "Label" value

Elmの仕様上、手続き的に書けないので、次のようにletをうまく利用して書く。

{- Elmでは
   Debug.log "Random" n
   return (model, Cmd.none)
   のように書けない。
-}
let _ = Debug.log "Random" n
in
  (model, Cmd.none)

リストの乱数生成器

randomList : Int -> Random.Generator (List Int)
randomList n =
  Random.list n <| Random.int 1 6

init : () -> (Model, Cmd Msg)
init _ =
  ({ circles = []
   }
  , Random.generate GetRandomNumbers (randomList 10)
  )

type Msg
  = GetRandomNumbers (List Int)

update : Msg -> Model -> (Model, Cmd Msg)
update msg model = 
  case msg of 
    GetRandomNumbers list -> 
      let _ = Debug.log "Random" list
      in
        (model, Cmd.none)

ブラウザのデバッグコンソールを開くと、以下の文が出力される。

Random: [1-6の乱数リスト]

Random.list

乱数のリストを作るための乱数生成器を返す。第1引数にリストの長さ、第2引数に乱数生成器を指定する。

Circleの乱数

Circleにおいて、x、y座標だけがランダムであるような乱数を作る。

randomCircleLackedText : Random.Generator (String -> Circle)
randomCircleLackedText =
  let rf = Random.float 0 600
  in
    Random.map2 (\x y -> Circle 10 x y) rf rf
  

randomCircles : Int -> Random.Generator (List Circle)
randomCircles n =
  let randomCirclesGenerator = Random.list n <| randomCircleLackedText
  in
    Random.map
      (List.indexedMap (\i c -> c (String.fromInt i)))
      randomCirclesGenerator


init : () -> (Model, Cmd Msg)
init _ =
  ({ circles = []
   }
  , Random.generate GetRandomCircles (randomCircles 10)
  )

type Msg
  = GetRandomCircles (List Circle)

update : Msg -> Model -> (Model, Cmd Msg)
update msg model = 
  case msg of 
    GetRandomCircles circles -> 
      ({ model | circles = circles }, Cmd.none)

Random.map2

2つの乱数生成器に何かしらの処理を施して、新たな乱数生成器を作る関数。

以下では、[0,600]Float乱数を2つ使って、それをCircleのx、yとしたものを作るようにしている。

ただし、textについてはまだ設定できないので、引数は3つにとどめておく。すると、乱数として作られるのはCircleではなく厳密にはString -> Circleとなることに注意。データコンストラクタは関数みたいなものだから、このようにカリー化ができる。

randomCircleLackedText : Random.Generator (String -> Circle)
randomCircleLackedText =
  let rf = Random.float 0 600
  in
    Random.map2 (\x y -> Circle 10 x y) rf rf

Random.map

1つの乱数生成器に何かしらの処理を施して、新たな乱数生成器を作る関数。

以下では、randomCircleLackedTextで作られた乱数にtextを付加している。ここではリストの添字をtextとしている。

randomCircles : Int -> Random.Generator (List Circle)
randomCircles n =
  let randomCirclesGenerator = Random.list n <| randomCircleLackedText
  in
    Random.map
      (List.indexedMap (\i c -> c (String.fromInt i)))
      randomCirclesGenerator

少し話がそれるが、map系の関数は概ね次のように処理されると認識している。

  1. M XMを取り外す → Xになる。
  2. Xに関数を適用 → Yとなる。
  3. YMを取り付ける → M Yになる。

今回のRandom.mapの場合は次のように処理される。

  1. Random.Generator (List (String -> Circle))Random.Generatorを取り外す → List (String -> Circle)になる。
  2. List (String -> Circle)に関数を適用 → List Circleになる。
  3. List CircleRandom.Generatorを取り付ける → Random.Generator (List Circle)になる。

ただし上の振る舞いがmapすべてではない。例えばMaybe.mapは上の動きに微妙に当てまらない。

感想

使い始めのときは「わざわざcommandにして乱数を作るの面倒だな」と思っていたが、実際にはcommandに関するコードをあまり書くことはない。乱数生成においてコードの大半を占めるのは乱数生成器作りで、こちらの理解にかなり時間がかかった。一度慣れてしまうと、結構柔軟に様々な型の乱数が作れて便利だな、と思った。

参考