この記事はIQ1の2まいめっ Advent Calendar 2018 の12月25日分です.
どーもIQ1のとりさんです.
近年,僕のIQ低下が問題となっています.
先日ついにプログラミングすら危うくなりました.JavaScriptうまく書けない...
主な困難は以下の点です.
- IQ1にnullは難しすぎる
- IQ1に動的型付けは難しすぎる
- IQ1に副作用は難しすぎる
これらを解決するには,そう!Haskellがあります.
しかし,Haskellに手を出すとまた問題とぶつかったのでした.
- IQ1に圏論は難しすぎる
というわけで今日はこんな僕でも書ける,IQ1な方々にオススメの言語,Elmを紹介します.
この記事ではプログラミング言語 Elm の特徴と,そのフレームワークの概要を紹介する.
Elmは丁寧なチュートリアルがあり,日本語化も進んでいる.
この記事では日本語化が追いついていない,Elm Architectureのサンプルを中心に解説をする.(注意:翻訳ではない)
フレームワーク以外はチュートリアルより大雑把な内容なので,読んだことのある方は読みとばして構わない.
インストールはチュートリアルが詳しいので任せるが,手っ取り早く動かしてみたい方は隣のウィンドウでEllinを開くとよいだろう.
-
特徴
- シングルページアプリケーションフレームワーク
- 静的型付け純粋関数型
- 単純な文法
-
フレームワークの概要
- 基本 Model, update, view
- 発展 Cmd, subscriptions
ElmはSPA(シングルページアプリケーション)を作ることに特化した言語である.
SPAとは Elline のように,単一のWebページのアプリケーションである.
ElmはJavaScriptにコンパイルされ,HTMLから読み込むことでウェブアプリケーションとして動作する.
HTMLを宣言的に記述するためのAPIが用意されている.
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]
例のように第1引数に属性を,第2引数にinner HTMLを与えるように記述できる.
(厳密にはカリー化されている)
Elmは静的型付け言語なので,関数や引数の型はコンパイル時に全てチェックされ,実行時の型例外は発生しない.
(なんと実行時エラーもない)
Elmの型はnull非許容なので,JavaScriptのように null
や undefined
に煩わされることはない.
さらに,関数の宣言は 型推論 が働くので
,プログラマの判断で型宣言(型注釈)を省略できる.
型の記述が面倒だという言い訳はできない.静的型付けから逃げるな.
純粋関数とは副作用(変数代入,入出力の類)を一切含まない関数のことである.
つまり 同じ引数には常に同じ結果が保証 される.
Elmにおいては,プログラマは純粋な関数を定義することでアプリケーションを記述する.
といってもHaskellのように,やれモナドだやれ圏だのと難しい概念が必要なほどには抽象化されていない.
ほんの少し,関数型言語のパラダイムを使うだけだ.
Elmは文法が単純で学習コストが低い.
ここではフレームワークの解説に先立って一部の文法を紹介する.
Elmでは関数は単一の式として定義される.
if-then-elseも式であり,C言語の条件演算子に似ている.
以下のように階乗を返す関数factを定義できる.
fact n =
if n <= 0
then 1
else n * fact (n - 1)
returnもセミコロンも使わず,関数を適用する際もパレン(小括弧)は必要なく要素を並べればよい.
(上のコードは計算の優先順位を示すのにパレンを使っている)
Elmの関数は引数リストをとらず,関数を返す関数(高階関数)として実現する.
以下のように順列を計算する関数permを定義できる.
perm n r =
fact n // fact (n - r)
関数定義の上に,型注釈を書くこともできる.
例えば perm
は整数を引数にとって,整数を引数にとって整数を返す関数,を返す関数なので,
perm : Int -> Int -> Int
perm n r =
fact n // fact (n - r)
と記述する.
Elmの型推論は優秀であるが,以下の理由でなるだけ型注釈は書いたほうが良い.
- コンパイルエラーのメッセージが分かりやすくなる
- 関数を使う際のドキュメントとなる
コンパイラは型が注釈と一致するか確認する.
また,型注釈はその関数を利用するプログラマに情報を提供する役割もある.
Elmでよく使われるデータ構造として
- リスト
- タプル
- レコード
を紹介する.
リストは順番を持ったデータを保持するデータ構造で,(同じ型の)複数の値を持つことができる.
多くの言語の配列のように非常によく使われる.
name : List String
name = ["Yudachi, "chakku", "poipoi"]
タプルは固定された個数の値を保持するデータ構造で,異なる型を保持できる.
典型的には関数から2つ以上の値を返す時などに用いられる.
pair : (Int, Bool)
pair = (42, True)
レコードはJavaScriptのオブジェクトと似ていて,名前(フィールド名)付きで値を保持する.
torisan : { name : String, iq : Int }
torisan = { name = "Torisan", iq = 1 }
yourIq = torisan.iq
(名前).(フィールド名)
で値を取り出すことができる.
Elmにはレコードを変換する非常に便利な構文があり,
torisanKai = {torisan | iq = 2}
と書くと, torisanKai
は torisan
の iq
を 2
にしたものとなる.
繰り返しになるがElmには副作用がないので,torisan.iq
は 1
のままである.
さて,やっと本題だ.
チュートリアルのサンプルコードを解説しつつ,フレームワークのデータフローを説明する.
まず最も簡単なボタンのサンプルを例に,フレームワークの基本となる,Model型,update関数,view関数を紹介する.
この例はチュートリアルに登場するシンプルなカウンタである.(このリンクからオンラインエディタで実行できる)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model = Int
init : Model
init =
0
-- UPDATE
type Msg = Increment | Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
model + 1
Decrement ->
model - 1
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]
上のコードはこれだけでプログラムとして成立している.
ダブルハイフンから始まる行はコメントである.
まずはモデルから説明する.
typeやtype aliasは型宣言であるが,詳しい説明は省略する.
大文字で始まるのが型で,小文字で始まるのが関数や引数の名前だということだけ知っていればよい.
ここではプログラムが保持するデータ構造としてModelという型を定義している.今回はカウンタなので定義は整数が1つだけ.
type alias Model = Int
これでモデルの定義はできた.
次はこれを更新する方法定義する必要がある. まずはUPDATEセクションに,UIから受け取るメッセージを定義する.
type Msg = Increment | Decrement
バー(|)は型の加算を示し,MsgはIncrementまたはDecrementである.
このようにしてMsg型はユーザがカウンタを増減するという機能をデータとして記述している.
update関数はこれらのメッセージを受け取ったとき何をするかを記述する.
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
model + 1
Decrement ->
model - 1
1行目の型注釈は,update関数がメッセージとモデルを受け取って(更新された)モデルを返すことを示している.
2行目からが関数の定義である.
詳細は割愛するが,Incrementメッセージに対してはmodelを1加算,Decrementメッセージに対しては1減算することが素直に記述されていることがわかるだろう.
次はHTMLを表示するviewだ.
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]
viewはモデルを引数にとって,メッセージを生成できるHTMLを返す.
Html型はMsg型で修飾されているが,これは丁度List IntがIntを扱うListであるように,Msg型のメッセージを生成するHtmlであることを表す.
HTMLの要素を表すdiv, buttonは引数にリスト([]で囲むとリストとなる)を2つとるAPIである
1つ目が属性,2つ目がinner HTMLである.
onClick属性がMsg型のIncrementとDecrementを生成することが何となくわかってもらえただろうか.
ElmはHTML5の全機能にアクセスできるAPIを備えている.
以上がElmの基本となる,Model, update, view パターンである.
ここまで読んでもらえれば,main関数が何を表しているか大体予測がついただろう.
Browser.sandboxはモデルの初期値を与えるinit,メッセージの処理を与えるupdate,モデルの表示とメッセージの生成法を与えるviewからプログラムを生成する.
以上のパターンのデータフローを図示するとこのようになる.
プログラムを作る際は以下のようなmodel,view,updateの骨組みから始めると良いだろう.
import Html exposing (..)
-- MODEL
type alias Model = { ... }
-- UPDATE
type Msg = Reset | ...
update : Msg -> Model -> Model
update msg model =
case msg of
Reset -> ...
...
-- VIEW
view : Model -> Html Msg
view model =
...
以前に「同じ引数には常に同じ結果が保証される」と述べたが,時刻,乱数,キー入力など,実行するときどきで異なる値が要求されるものが扱いたいこともある.
Elmはそのような要求にも,Elmのクリーンな世界を壊さずに答える方法を持っている.
それはコマンドとサブスクリプションだ.
コマンドは外部にリクエストを送るとき利用する機能だ.
ここでは,チュートリアルにあるサイコロと例にしてコマンドを解説する.
(オンラインエディタはここ)
import Browser
import Html exposing (..)
import Html.Events exposing (..)
import Random
-- MAIN
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
-- MODEL
type alias Model =
{ dieFace : Int
}
init : () -> (Model, Cmd Msg)
init _ =
( Model 1
, Cmd.none
)
-- UPDATE
type Msg
= Roll
| NewFace Int
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Roll ->
( model
, Random.generate NewFace (Random.int 1 6)
)
NewFace newFace ->
( Model newFace
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> Html Msg
view model =
div []
[ h1 [] [ text (String.fromInt model.dieFace) ]
, button [ onClick Roll ] [ text "Roll" ]
]
前回のBrowser.sandboxに代わりBrowser.elementを使う.
引数の中に今回使わないsubscriptions関数が登場するが,今は脇に置いておく.
コマンドを使うにあたって各所にCmd Msgという型が出てくるが,これもひとまず置いて,まずはUPDATEセクションに注目してもらいたい.
今回メッセージとして定義されているのは,サイコロを振るRollと,新しい面を表示するNewFaceである.
ここでちらっとview関数を見ると,
button [ onClick Roll ] [ text "Roll" ]
とあるため,ボタンをクリックしたときにRollメッセージが送られることがわかる.
updateはカウンタの例ではModelを返したが,今回はModelとCmdのペアを返す.
Rollメッセージの時モデルは更新されない(元のモデルをそのまま返す)が,Cmdに何やら乱数に関する記述が見つかる.これがコマンドだ.
Random.generate NewFace (Random.int 1 6)
Random.generateは乱数を要求するときの仕様を作る関数で,ここでは1~6の整数をNewFaceというメッセージで送り返すことを要求している.
コマンドの応答はメッセージに埋め込まれ,updateに返ってくる.
NewFace newFace ->
( Model newFace
, Cmd.none
)
この例では newFace
として受け取った乱数が新しいモデルとなる.
新たなコマンドには Cmd.none
が設定されており,これは続くリクエストがないことを表す.
(コマンドも型修飾されており,Cmd MsgはMsg型で応答を返すCmdであることを表す)
subscriptions関数については次のデジタル時計のサンプルで説明する.
データフローにCmdを追加すると図のようになる.
サブスクリプションは外部でイベントが起きた時に,プログラムに通知させる機能だ.
チュートリアルに登場するデジタル時計を例に解説する.(このリンクからオンラインエディタで実行できる)
import Browser
import Html exposing (..)
import Task
import Time
-- MAIN
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
type alias Model =
{ zone : Time.Zone
, time : Time.Posix
}
init : () -> (Model, Cmd Msg)
init _ =
( Model Time.utc (Time.millisToPosix 0)
, Task.perform AdjustTimeZone Time.here
)
-- UPDATE
type Msg
= Tick Time.Posix
| AdjustTimeZone Time.Zone
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Tick newTime ->
( { model | time = newTime }
, Cmd.none
)
AdjustTimeZone newZone ->
( { model | zone = newZone }
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every 1000 Tick
-- VIEW
view : Model -> Html Msg
view model =
let
hour = String.fromInt (Time.toHour model.zone model.time)
minute = String.fromInt (Time.toMinute model.zone model.time)
second = String.fromInt (Time.toSecond model.zone model.time)
in
h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]
今回のモデルはタイムゾーンとPOSIX時間だ.
POSIX時間はコンピュータでよく利用される時間の表現方法であるが,世界共通のためタイムゾーンを利用して現地時刻に変換する.
init関数で初めのモデルはタイムゾーンUTC,POSIX 0秒,コマンドとして現在地のタイムゾーンをリクエスト(Time.here)しAdjustTimeZoneメッセージで受け取れるように設定している.
APIの詳細が気になる人は elm/time を参照
UPDATEセクションは飛ばして先にview関数を確認する.
view関数では,modelのPOSIX秒とタイムゾーンを指定して,人間に分かりやすい時分秒を生成し表示する方法が記述してある.
view : Model -> Html Msg
view model =
let
hour = String.fromInt (Time.toHour model.zone model.time)
minute = String.fromInt (Time.toMinute model.zone model.time)
second = String.fromInt (Time.toSecond model.zone model.time)
in
h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]
見慣れない let ... in
という記述があるが,Elmはこのように関数の中で式に名前をつけておくことで可読性を向上させる.
さて,次はプログラム中でPOSIX秒を得るための仕掛け, subscription
関数を見ていく.
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every 1000 Tick
ここでは Time.every
関数を利用している.
every : Float -> (Time.Posix -> msg) -> Sub msg
第1引数は時刻を取得するインターバルをミリ秒で指定する.
今回は1000ミリ秒=1秒ごとに取得する設定だ.
第2引数はPosix時刻をメッセージに変換する関数を指定する.
今回はTickが設定されている.(Msg型の宣言では,TickはTime.Posixを伴ってMsgとなるので,Tick単体はTime.Posixを引数にとってMsgを返す)
このようにsubscriptionsを記述することで,フレームワークは1秒ごとに現在時間をメッセージとして通知する.
ここまで読めばupdate関数はもはや説明の必要はないだろう.
フレームワークから届くPosix時刻を伴うTickメッセージと,
タイムゾーンを伴うAdjustTimeZoneの捌き方が記述されている.
({model | time = newTime}はmodelのtimeをnewTimeに置き換えたデータを生成することを意味している)
以上がサブスクリプションの典型的な使い方だ.
キー入力を受け取りたい時なども同様にsubscriptionsで通知設定をし,updateでメッセージを受け取る.
サブスクリプションをデータフローに追加すると図のようになる.
ここまでElmチュートリアル,コマンドとサブスクリプションをまとめましたがいかがでしたか.
Elmの静的型付けの力は我々IQが1のうっかりさんにぴったりではないでしょうか.
Elmには他にも,パターンマッチ,Elm Portsなどの素晴らしい機能があります.
ぜひ使っていきましょう.
開発の仕方についてはテストを先に書くべきという考え方もあるようです.
年末は自分の開発スタイルをゆっくり見直したいです.
それではみなさん,よいお年をお迎えください.