乱数の練習に。
プロジェクト用のディレクトリを適当に作り、そこで以下のコマンドを実行。
$ elm init
必要なモジュールを入れる。
$ elm install elm/svg
$ elm install elm/random
Main.elm
を作成し、最低限の文を定義しておく。
1
2
3
4
5
6
7
| module Main exposing (..)
import Browser
import Svg exposing (..)
import Svg.Attributes as SA exposing (..)
import Svg.Events as SE exposing (..)
import Random
|
円の描画#
こんな感じの円を描画する。
SVGでは次のように書く。
1
2
3
4
5
6
| <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つ:
そこで円は次のように定義する。
1
2
3
4
5
6
| type alias Circle =
{ r: Float
, x: Float
, y: Float
, text: String
}
|
Elmでは宣言的にSVGやHTMLを書けるので、SVGの文法とほとんど似た構造でかける。直感的で嬉しい。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| 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
++ ")"
|
他は定型通りに書く。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| 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
の定義はレコードだから、次のように定義するべきだろう。
1
2
3
4
5
| { r = 10
, x = 100
, y = 20
, text = "1"
}
|
しかし型エイリアスとしてレコードが定義された場合、次のようにCircle
を定義することも可能。
Haskellのフィールドラベルに近いものを感じる。
乱数を試す#
Elmでほとんど乱数を使ったことがないので、色々試す。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| 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.generate#
乱数を作るcommandを返す。第2引数には乱数生成器を指定する。ここではRandom.int 1 6
としている。これは、[1,6]
のInt
型の乱数を作る生成器。乱数は第1引数のMsg型変数で受け取る。
様々な形の乱数生成器を作るための関数が用意されている。その一部については次で扱う。
(補足) Debug.log#
いわゆるprintデバッグをするために使われる関数。
1
2
| -- JSにおけるconsole.log(`Label: ${value}`)と同義
Debug.log "Label" value
|
Elmの仕様上、手続き的に書けないので、次のようにletをうまく利用して書く。
1
2
3
4
5
6
7
8
| {- Elmでは
Debug.log "Random" n
return (model, Cmd.none)
のように書けない。
-}
let _ = Debug.log "Random" n
in
(model, Cmd.none)
|
リストの乱数生成器#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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.list#
乱数のリストを作るための乱数生成器を返す。第1引数にリストの長さ、第2引数に乱数生成器を指定する。
Circleの乱数#
Circle
において、x、y座標だけがランダムであるような乱数を作る。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| 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
となることに注意。データコンストラクタは関数みたいなものだから、このようにカリー化ができる。
1
2
3
4
5
| 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としている。
1
2
3
4
5
6
7
| 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
系の関数は概ね次のように処理されると認識している。
M X
のM
を取り外す → X
になる。X
に関数を適用 → Y
となる。Y
にM
を取り付ける → M Y
になる。
今回のRandom.map
の場合は次のように処理される。
Random.Generator (List (String -> Circle))
のRandom.Generator
を取り外す → List (String -> Circle)
になる。List (String -> Circle)
に関数を適用 → List Circle
になる。List Circle
にRandom.Generator
を取り付ける → Random.Generator (List Circle)
になる。
ただし上の振る舞いがmap
すべてではない。例えばMaybe.map
は上の動きに微妙に当てまらない。
使い始めのときは「わざわざcommandにして乱数を作るの面倒だな」と思っていたが、実際にはcommandに関するコードをあまり書くことはない。乱数生成においてコードの大半を占めるのは乱数生成器作りで、こちらの理解にかなり時間がかかった。一度慣れてしまうと、結構柔軟に様々な型の乱数が作れて便利だな、と思った。