はじめに
最近気になっている、これから盛り上がる、もしくは盛り上がり始めている2つの言語でミニマルなCRUDをするデモを作ってみました。全体を説明しようとすると非常に長くなってしまうため、かいつまみながら言語やコードの説明をしていきたいと思います。Elmは一年近く触り続けていますが、Elixirは今回始めて触ったため、誤った説明がありましたらご指摘お願いします。
環境
今回使用した環境を以下に列挙します。今回選んだ言語の特徴として、何かに特化したDSLに近い特徴を持つ言語を選択しました。特化した言語は、汎用言語のようにどんな処理でも書くことは出来ませんが、その代わりに得意な分野ではライブラリ・フレームワーク等を入れずとも言語の特徴自身で様々なことができます。今回は、簡単な例ではありますが、Webサーバとフロントエンドのプログラミングをするのにライブラリを、ほとんど使っていないことがわかります。
バックエンド
ElixirはErlangにRubyライクな文法を採用した言語で、並列処理に特化した言語でアクターモデルをプログラミングの主体に置いています。Erlangの仮想マシン(BEAM)上で動きErlangの財産が使えます。今回Elixirを使ってみて、Erlang/OTPを使えること自体も素晴らしかったのですがDBMSが何の設定も無く2種類(ETS, Mnesia)も使え、REPL上で即試せるということが衝撃的でした。大抵の言語では環境構築が憂鬱になるのですが、Elixirは何も躓くことなくプログラミングに移れたことが好印象でした。
フロントエンド
Elmは見ての通り標準のライブラリで済んでしまいました。また、Reduxの元となる言語で、The Elm Architectureと言うフロントエンドアプリケーションを書くための構造を提供し、それに則るだけで綺麗にプログラミングできるのが特徴です。また、Bulmaという、CSSのみに依存するだけでマテリアルデザインができてしまうフレームワークも気に入っていて採用しました。BootStrapを普段採用している皆さんも是非試してみてください。今回、ページ全体のPadding以外、CSSを一行も書いていません(コンポーネントに状態が無いためSPA向きです。)!
-
CSS
-
Bulma
- FlexboxをベースにしたCSSのみで完結するフレームワーク
-
Bulma
コード解説
ここからはコードを解説していきます。文法の説明は既存の文献に任せるとして、やってみて素晴らしい!と思った部分をかいつまんで説明していきます。
バックエンド
今回バックエンドのコードでは、3つのモジュールからなります。
- Application
- メインモジュール、スーパバイザの役割を持つ
- Router
- HTTPのルーティングを行うモジュール
- Repository
- DBMSと通信するためのモジュール、GenSever
Elixirは今回、始めて触れた言語だったためPlugを利用したルーティング処理はつまずきながらも見よう見まねでなんとかなりましたが、Repositoryをどう使用するかが悩みの種でした。オブジェクト指向言語ではないためシングルトンオブジェクトを作り、ルータに差し込むわけにはいきません。そこで役に立ったのはOTPのGenServerでした。GenServerはクライアント/サーバの良くありがちなパターンを実装するためのもので、これを利用することでRepositoryのためのアクターを簡単に作ることができ、このRepositoryアクターとRouterのアクターをApplicationの子供としてしまうことで、実に簡単に悩みを解消してしまいました!いつもフレームワークのDBについての実装ベストプラクティスを探って解決する自分にとって、言語の標準機能であっさり解決できてしまうのは非常に感動を覚えました。
これがApplicationのモジュールです。Webサーバを立ち上げ、Routerモジュールに実装を任せ、RepositoryのGenServerをSupervisorのworkerとして起動し、最後にApplication自身をSupervisorとして起動しています。
defmodule Crud.Application do
use Application
def start(_type, _args) do
children = [
Plug.Adapters.Cowboy.child_spec(:http, Crud.Router, [], port: 8000),
Supervisor.Spec.worker(Crud.Repository, [])
]
opts = [strategy: :one_for_one, name: Crud.Supervisor]
Supervisor.start_link(children, opts)
end
end
Repositoryはstart_link
関数でDB(mnesia)の起動とテーブルの定義、stateの初期化(id = 1)をおこないます。前半部のas_entity_list
からkill
までが、他モジュール(Router)から使われるインターフェースの役割をしています。GenServer.cast
という関数を呼び出しているのがわかります。その関数は、後半部のhandle_call
関数をシンボルを使いパターンマッチ呼び出しをしています。基本的にはmnesiaの関数を呼び出しテーブルを書き換え、最後にstateの書き換えがあれば、その処理をおこないます。ScalaのAkkaを触れていた為、同じ感覚で実装ができました。Akkaを触れていない人でもオブジェクト指向のインターフェースと内部実装をおこなう感覚でできると思います。並列処理がおこなえるメッセージパッシングスタイルなオブジェクト指向と捉えるのが良いと思います。
defmodule Crud.Repository do
use GenServer
def init(args) do
{:ok, args}
end
def start_link() do
:mnesia.start()
:mnesia.create_table(Person, attributes: [:id, :name, :job])
:mnesia.add_table_index(Person, :id)
GenServer.start_link(__MODULE__, %{id: 1}, name: __MODULE__)
end
def as_entity_list() do
GenServer.call(__MODULE__, :as_entity_list)
end
def store(person) do
GenServer.cast(__MODULE__, {:store, person})
end
def update(id, person) do
GenServer.cast(__MODULE__, {:update, id, person})
end
def delete(id) do
GenServer.cast(__MODULE__, {:delete, id})
end
def kill do
GenServer.cast(__MODULE__, :kill)
end
####################
def handle_call(:as_entity_list, _from, state) do
persons = :mnesia.dirty_match_object({Person, :_, :_, :_})
persons_map =
Enum.map(persons, fn {Person, id, name, job} -> %{id: id, name: name, job: job} end)
|> Enum.sort_by(fn person -> person.id end)
{:reply, persons_map, state}
end
def handle_cast({:store, person}, state) do
:mnesia.dirty_write({Person, state.id, person["name"], person["job"]})
{:noreply, %{id: state.id + 1}}
end
def handle_cast({:update, id, person}, state) do
:mnesia.dirty_write({Person, id, person["name"], person["job"]})
{:noreply, state}
end
def handle_cast({:delete, id}, state) do
:mnesia.dirty_delete({Person, id})
{:noreply, state}
end
end
RouterはPlug Routerを利用することでRubyのSinatraライクに記述することができました。plug()関数を利用することで機能を差し込んで行く形でパワフルで便利だと感じました。一方plug()の順番等を間違えると突然死んでしまうので、そこは仕様を把握しないといけないため不親切と感じました。また、先ほど作ったRepositoryが既にApplication Supervisorの子要素、つまり兄弟同士のため、インターフェースを呼び出すだけでDBの書き込み、読み込みが出来てしまうのが本当に便利だと感じました。
defmodule Crud.Router do
use Plug.Router
alias Crud.Repository, as: Repository
plug(:match)
plug(CORSPlug, origin: ["http://localhost:3000"])
plug(
Plug.Parsers,
parsers: [:json],
pass: ["application/json"],
json_decoder: Poison
)
plug(:dispatch)
# [ {id: 1, name: "Mike", job: "WD"}, {id: 2, name: "John", job: "PG"} ]
get "/persons" do
params = Repository.as_entity_list()
send_resp(conn, 200, Poison.encode!(params)) |> halt
end
# {name: "John", job: "SE"}
post "/persons" do
Repository.store(conn.body_params)
send_resp(conn, 201, "") |> halt
end
# {id: 1, name: "Mike", job: "WD"}
put "/persons/:id" do
Repository.update(String.to_integer(id), conn.body_params)
send_resp(conn, 200, "") |> halt
end
delete "/persons/:id" do
Repository.delete(String.to_integer(id))
send_resp(conn, 200, "") |> halt
end
match(_, do: send_resp(conn, 404, "Not Found!") |> halt)
end
最後に、標準でパワフルな並列処理がわかりやすいアクターモデルで出来てしまうElixirでしたがエラーのほとんどがパターンマッチ失敗のエラーで、詳しい情報が出ないため型が欲しいな・・・と思ってしまったこととErlangとElixir共にドキュメントが充実しておらず、非常に苦労が強いられました。私自身もこういった記事等で少しでも協力していければな、と思いました。
フロントエンド
Elmは普段記事を書いているため、多くの解説などはおこないません。改めて触った感想ですが、Elixirを終えてから触った分、型のサポートが非常に嬉しく感じました。どうしてもWebAPIと通信する部分では実行時エラーが起きがちですが、そもそもサーバの実装ミスが多かったため、Elm側の問題ではないケースがほとんどでした。
実際のHTTPリクエストをおこなう箇所は以下のようなコードで、シンプルかつ(多少ボイラプレート気味ですが)型安全にJSON <-> 内部データとの変換がおこなえるのが嬉しい箇所でした。
getPersons : Cmd Msg
getPersons =
let
url =
"http://localhost:8000/persons"
in
Http.send GetPersonsResponse (Http.get url decodePersons)
decodePerson : Decode.Decoder Person
decodePerson =
Decode.map3 Person
(Decode.at [ "id" ] Decode.int)
(Decode.at [ "name" ] Decode.string)
(Decode.at [ "job" ] Decode.string)
viewはコンポーネント分けなどはせず、単なる関数として分割できるため、シンプルに見通しが良い結果となりました。
view : Model -> Html Msg
view ({ persons, name, job, editID, editName, editJob, error } as model) =
div [ class "container" ]
[ addForm name job
, errorMsg error
, personList persons editID editName editJob
]
errorMsg : ErrorMsg -> Html Msg
errorMsg addError =
case addError of
Just e ->
div [ class "notification is-danger" ] [ text e ]
Nothing ->
empty
addForm : Name -> Job -> Html Msg
addForm name job =
div [ class "columns" ]
[ div [ class "column is-one-quarter" ] [ input [ class "input", type_ "text", value name, placeholder "Name", onInput NewName ] [] ]
, div [ class "column is-one-quarter" ] [ input [ class "input", type_ "text", value job, placeholder "Job", onInput NewJob ] [] ]
, div [ class "column" ] [ a [ class "button is-medium is-success", onClick NewPerson ] [ text "Add" ] ]
]
personList : List Person -> EditID -> Name -> Job -> Html Msg
personList persons editId editName editJob =
table [ class "table" ]
[ thead []
[ th [] [ text "ID" ]
, th [] [ text "Name" ]
, th [] [ text "Job" ]
, th [] []
, th [] []
]
, tbody [] <|
List.map
(\person ->
personItem editId editName editJob person
)
persons
]
personItem : EditID -> Name -> Job -> Person -> Html Msg
personItem editID editName editJob { id, name, job } =
let
item =
tr []
[ td [] [ text <| toString id ]
, td [] [ text name ]
, td [] [ text job ]
, td [] [ a [ class "button", onClick <| EditStart id name job ] [ text "Edit" ] ]
, td [] [ a [ class "delete is-large", onClick <| DeletePerson id ] [] ]
]
in
case editID of
Just eid ->
if id == eid then
tr []
[ td [] [ text <| toString id ]
, td [] [ input [ class "input", type_ "text", value editName, onInput EditName ] [] ]
, td [] [ input [ class "input", type_ "text", value editJob, onInput EditJob ] [] ]
, td [] [ a [ class "button is-primary", onClick EditEnd ] [ text "Confirm" ] ]
, td [] [ a [ class "delete is-large", onClick <| DeletePerson id ] [] ]
]
else
item
Nothing ->
item
empty : Html Msg
empty =
text ""
viewを描画するためのアプリケーション全体の状態、Modelは以下のような定義になっています。type alias
を利用することで、後から見直しても型で混乱することはありません。
type alias Model =
{ persons : List Person
, name : Name
, job : Job
, editName : Name
, editJob : Job
, editID : EditID
, error : ErrorMsg
}
init : ( Model, Cmd Msg )
init =
( { persons = []
, name = ""
, job = ""
, editName = ""
, editJob = ""
, editID = Nothing
, error = Nothing
}
, getPersons
)
type alias Person =
{ id : ID, name : Name, job : Job }
type alias ID =
Int
type alias PostedPerson =
{ name : Name, job : Job }
type alias Name =
String
type alias Job =
String
type alias ErrorMsg =
Maybe String
type alias EditID =
Maybe ID
ViewからMsgと呼ばれるイベントをキャッチして、Modelを書き換えるupdate関数は、少し長くなったため、記事では割愛させていただきます。
近年、肥大化しがちなフロントエンド実装を型安全に、かつ、何のライブラリ・フレームワークに依存すること無く書けるのは非常に強力ではないでしょうか。今回はWebAPIとの通信が主でしたが、ビジネスロジックが多く入り込むような場面ではテストが書きやすく、さらに強力な言語機能が役立ちます。近頃では一番の推し言語になります。ぜひ触ってみてください。