Runner in the High

技術のことをかくこころみ

人の作ったWebアプリケーションのコードを見るときに注目しているところ

普段見ているものをなんとなく書き出してみた。

インターフェイス

あえてやってないとか、レイヤ的にやる必要がないというケースもある。しかし、ある程度の規模のソフトウェアには大抵インターフェイスが現れる。インターフェイスがないコードはユニットテストもないことが多い。したがって、インターフェイスが現れないコードは責務分離が行われてない可能性を感じたりする。

言語機能上インターフェイスがない動的型付け言語の場合には、ダックタイピングを意識したコードが書かれているかをチェックする。ダックタイピングでなくとも、例えばRubyだったら抽象クラスと実装クラスの分離が行われているかを見たりする。

バリデーションロジック

すべてのバリデーションが、フレームワークの機能で実装されてたりしないかをチェックする。MVCとかクリーンアーキテクチャ的な実装であれば、それぞれのレイヤでどういうバリデーションをしているのかを覗いてみる。コントローラで全部のバリデーションが行われているときもある。

また、バリデーションエラーをどうハンドリングしているかも確認する。DBエラーをそのままプレゼンター層に返していたり、ドメイン層に外部レイヤの言葉とかデータが出てくると「オッ」となったりする。

トランザクション

トランザクションをどの範囲で貼っているのかをチェックする。そもそもトランザクションが貼られていないときは、なんらかの理由があるかを調べてみる。トランザクションの実装がドメインからどれくらい分離できているかを見てみる。

トランザクションが貼られている部分に対するリクエストのトラフィックを見てみる。なぜそのトランザクションが結果整合じゃないのかを考えてみる。

Userクラス

大体、どんなアプリケーションでもUserみたいな名前のクラスが存在する。とりあえずおもむろにそれを覗いてみる。1000行とかあったりすることもあるので、そのときはまず深呼吸する。もしかしたら本当にそれだけ長くなる理由があるのかもしれない。

Userが薄い場合にはUserを継承してそうなクラスを探してみる。薄すぎるな〜と思ったときには、Userの実装をもとに値オブジェクト的なものを探してみる。あったら実装を読んでみる。どういうクラス設計になっているか覗いてみる。

POXO

これは極めて強引な意見だが、できのいいソフトウェアは大体POXO*1がちゃんと作られている。RubyならPOROだし、JavaならPOJOになる。特に、ドメインにあたるレイヤのコードがPOXOじゃないときは、その理由を探ってみる。

すべてのクラスが何かを継承していたり、なにかに依存している場合には「なんでだろう?」を考えてみたりする。

〇〇サービス

ディレクトリ構造を全部展開してHogeServiceみたいなのがないか探してみる。するとだいたい見つかるので、ゆっくりその中身を読んでみる。

ステートフルなのか、ステートレスなのか。ドメインサービスなのか、アプリケーションサービスなのか判断を試みる。どっちでもなさそうな場合には、なんらかのドメインに所属させれないか考えてみたりする。

〇〇リポジトリ

とりあえずDDDのリポジトリを期待して中身を覗いてみる。インターフェイスであることを期待しているところ、いきなりSQL文が現れたりすることもある。

インターフェイスになっている場合には、どこまで永続化装置を抽象化できているか見てみる。追加と削除のインターフェイスだけになっていたらかなり驚く。満足したらどこでDIしているのかを探してみる。フレームワークの機構を使っているのか、シンプルなコンストラクタDIなのかをチェックする。

レイヤごとの実装

MVCであればそれぞれにどういうコードが書かれているのかを見てみる。Mの中に永続化装置やプレゼンター実装の具体的な単語が出てくると「オッ」となる。クリーンアーキテクチャであればFlow of Controlをどう作っているか、アダプタ実装ごとのプラガビリティがインターフェイスでどれだけ抽象化できているか、をじっくり眺めてみる。

静的型付言語であれば、型を使ってドメインをどれくらい守れているかを見てみる。

*1:Plain-Old-Xxx-Objectの略。Xxxには任意のプログラミング言語の名前が入る。

ElmでBrowser.elementを使いつつルーティングを自前で作る

一般的にElmでルーティングを行うSPAを作る場合にはBrowser.applicationを使って、組み込みのルーティングの機構を使うことになる*1。しかし、一方でルーティングの仕組みを持たないBrower.elementBrowser.documentでも、ルーティングをJavaScriptサイドで自前実装する方法がある。

elementを使いつつルーティングを自作したいユースケースとして、ReactやVue.jsと統合してElmを使いたいケースが挙げられる。applicationやdocumentを使うと特定のDOMのみにElmアプリケーションをマウントすることができないため、他のフレームワークと共存させることができない。

なお、elm/browserリポジトリにも「Browser.elementでルーティングをするにはどうすればよいか」を説明した詳しいドキュメントがある。 github.com

実装してみる

さて、ではどんな感じでルーティング部分を自作するか。この記事では楽をするべくルーティングの実装にnanorouterhistoryを使う。

nanorouterはURLのマッチング機構だけをもつ小さなルーティングモジュールである。これを使えばURLのパスやパラメタのパーサをわざわざ正規表現で作る必要がなくなる。また、イベントエミッタ的なインタフェースをしているのでElmのPortがもつPub/Sub的な雰囲気との相性がよい。

historyは言わずとしれたHistory APIの抽象化ライブラリ。これを使っておけばブラウザ間の差異だとかそういうのを気にしなくていい。

ここから紹介する実際のコードは以下のギッハブリポジトリに置いてある。 github.com

index.js

基本的にパスの変更が起きた場合に、JSサイドでは変更をPort経由でElmアプリケーションへ送信したり、Elmアプリケーションから受け取ったパスを用いてhistoryで遷移を実行したりするだけである。

import { Elm } from "./App.elm"
import { createBrowserHistory } from 'history'

const history = createBrowserHistory()
const app = Elm.App.init({ 
  flags: history.location.pathname,
  node: document.querySelector('main'),
})

history.listen((location, _) => {
  app.ports.onUrlChanged.send(location.pathname)
})

app.ports.replaceInternal.subscribe(path => {
  history.push(path)
})

Route.elm

ルーティングに関連するロジックを凝集したのが、このRouteモジュールになる。

port module Route exposing
    ( Route(..)
    , decode
    , link
    , replace
    )

import Html exposing (Html, a, text)
import Html.Attributes exposing (href)
import Html.Events as Events
import Json.Decode as Decode


type Route
    = Top
    | PageA
    | PageB


decode : Decode.Value -> Route
decode value =
    value
        |> Decode.decodeValue decoder
        |> Result.withDefault Top


link : msg -> String -> Html msg
link msg label =
    a [ href "#", onClick msg ] [ text label ]


replace : Route -> Cmd msg
replace route =
    replaceInternal (routeToString route)

内部関数であるonClicklink関数の中で使っている。これはaタグで画面遷移の発火をブロックするためのカスタムのonClickイベントである。これがないとブラウザのロードが走ってしまう。

stringToRouterouteToStringはRoute型と文字列のパスを突合するためのマッピング関数である。JavaScript側とPortを介して文字列でパス情報がやりとりされるため、このようなマッピングを行う関数が必要になる。

replaceInternalを公開関数にしていないのは、直接文字列を受け取れてしまうのを避けるためである。あえてreplace関数でラップして、引数にRoute型を受け取るようにすることで、ルーティングの組み合わせが型から判別できる。文字列だとなんでも指定できる分、使う側からすると前提がなくて難しい。パスにスラッシュがつくのかどうか、なども気にしてなくてはいけなくなる。

-- Internals


onClick : msg -> Html.Attribute msg
onClick msg =
    Events.custom "click"
        (Decode.succeed
            { message = msg
            , stopPropagation = True
            , preventDefault = True
            }
        )


stringToRoute : String -> Decode.Decoder Route
stringToRoute value =
    case value of
        "/pageA" ->
            Decode.succeed PageA

        "/pageB" ->
            Decode.succeed PageB

        _ ->
            Decode.succeed Top


routeToString : Route -> String
routeToString route =
    case route of
        Top ->
            "/"

        PageA ->
            "/pageA"

        PageB ->
            "/pageB"


decoder : Decode.Decoder Route
decoder =
    Decode.string |> Decode.andThen stringToRoute


port replaceInternal : String -> Cmd msg

App.elm

initialModel関数ではFlagの値をValue型で受け取り、初期のRoute型のデータを計算する。計算されたRoute型のデータをchangeRouteTo関数に食わせることで、最初のModelが生成される。

基本的には画面遷移は、このModel計算の流れを繰り返すことになる。

port module App exposing (main)

import Browser
import Html exposing (Html, div, text)
import Json.Decode as Decode
import Pages.A as PageA
import Pages.B as PageB
import Route



-- model


type Model
    = Top
    | PageA PageA.Model
    | PageB PageB.Model


initialModel : Decode.Value -> ( Model, Cmd Msg )
initialModel flag =
    ( changeRouteTo (Route.decode flag), Cmd.none )


changeRouteTo : Route.Route -> Model
changeRouteTo route =
    case route of
        Route.Top ->
            Top

        Route.PageA ->
            PageA PageA.init

        Route.PageB ->
            PageB PageB.init

Msg型のバリアントふたつはほとんどBrowser.applicationで使うものと近い。

-- update


type Msg
    = URLChanged Decode.Value
    | ChangeURL Route.Route


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        URLChanged nextRoute ->
            ( changeRouteTo (Route.decode nextRoute), Cmd.none )

        ChangeURL route ->
            ( model, Route.replace route )

上で定義されているURLChangedはインバウンドなPortの受けメッセージになる。ここはFlagと同じでValue型で受け取る。

受け取ったValue型のデータをデコードしてそこからModel型をつくる流れはinitialModel関数とほぼ同じであることがわかる。

-- port


subscriptions : Model -> Sub Msg
subscriptions _ =
    onUrlChanged URLChanged


port onUrlChanged : (Decode.Value -> msg) -> Sub msg

画面遷移のリンクはRouteモジュールで定義したlink関数で作ることができる。

-- view


view : Model -> Html Msg
view model =
    case model of
        Top ->
            div []
                [ text "This is Top"
                , div [] [ Route.link (ChangeURL Route.PageA) "PageA" ]
                , div [] [ Route.link (ChangeURL Route.PageB) "PageB" ]
                ]

        PageA pageModel ->
            PageA.view pageModel

        PageB pageModel ->
            PageB.view pageModel

あえてBrowser.elementを使って自分でElmアプリケーションのルーティングを作ってみると「SPAにおけるルーティングとはなんぞや」というものがすごくクリアに見えてくる。これはReactだろうがVue.jsだろうが同じである。

ルーティングはアプリケーションに対するブラウザからの入力であり、アプリケーションからブラウザに対する出力という副作用である。Elmアプリケーションは、外部から受け取ったデータをもとにModelを都度計算するというひとつの関数のように振る舞う。こういう分かりが生じてくるのが、関数型プログラミングのおもしろさだ。

*1:Browserモジュールが提供している初期化関数の種類に関してはJinjorさんのQiitaの記事が詳しい https://qiita.com/jinjor/items/245959d2da710eda18fa

Elmアプリケーションにおける多言語対応パターン

特段テクい話ではないが最近話す機会があったのでこちらでもメモ。

まず、以下のように対応している言語のパターンを表現した型を全画面共通で使える型として用意しておく。

type Lang 
    = Ja 
    | En

あとは各ページごとに画面で表示するラベルのインターフェイスとなるレコード型を用意しておき、言語別に出し分ける関数を作る。 これをview関数の中で呼び出せばよい。 非常にシンプル。

type alias Phrases = 
    { title : String
    , cancel : String
    , submit : String
    } 


toPhrases : Lang -> Phrases
toPhrases lang =
    case lang of
        Ja ->
            { title = "タイトル"
            , cancel = "キャンセル"
            , submit = "提出"
            }

        En ->
            { title = "Title"
            , cancel = "Cancel"
            , submit = "Submit"
            }

最後に言っておくと、Languageモジュールみたいな日英の多言語情報をまとめたぶっこみモジュールを作るのは強くオススメしない。 共通で使う文言ならまだしも、ある画面でしか使っていない文言を全部ひとつにまとめたくなった場合には、本当にまとめる意味があるのか考えるべきだ。

というわけで、画面モジュールごとに上で言うところのPhrasestoPhrasesをペアで用意するのがよい。

SCIMなどの外部連携インターフェイスがアプリケーションの仕様を侵食する件

今回はちょっとエンタープライズなハナシ。

そもそもSCIMってなに

このQiitaの記事が詳しい。
要はマスターデータを連携するためのWebAPIのインターフェイスの規格的なもの。 qiita.com

SCIMインターフェイスはつらい

B2Bのプロダクトだとマスターデータの連携を行う目的でSCIMインターフェイスの口を提供するケースがあるが、SCIMはビジネスルールを侵食する

まずSCIMにはスキーマとして次の仕様があるが、SCIMインターフェイスを実装するアプリケーションは基本的にスキーマごとの仕様を実装することがほぼ必須になる。

  • Groupスキーマの仕様
    • Groupの階層化(あるGroupが子となるGroupを複数持つことができる)
    • Userの所属(Groupに複数のUserが所属できる)
  • Userスキーマの仕様
    • 物理/論理削除

「必須になる」と書いているのは、アプリケーションはスキーマ単位で取捨選択はできるが、スキーマにおける仕様を取捨選択することはできないからである。たとえばGroupスキーマをサポートしながら、Userの所属のみを採用し、Groupの階層化をサポートしない、というのはほぼ不可能*1になる。

実際のAzureADでのケースを挙げると、AzureDAサイドでグループの階層化が行われている場合には、SCIMのグループ更新のリクエストに階層化されたグループの情報が乗ってくる。アプリケーション側は階層情報をどこかで必ず保持しなければいけない。リクエストを読み捨ててしまうと、続いて不定期でやってくるAzureADからの永続化チェックのレスポンスを作れないからである。AzureAD側が満足するレスポンスを返せないと、永遠にリクエストが飛んでくることになるし、永続化に失敗しているとみなされればAzureADの場合には「検疫モード」に移行し、連携処理がしばらく止められてしまい顧客に迷惑をかける。

これは、SCIMインターフェイスがアプリケーションから「〇〇の機能は提供していない」みたいなリクエストを受け取る選択肢を提供していないためだ。したがって、SCIMを使うからには常にSCIMのルールに乗らなければいけない。これが「プロダクトのドメインSCIMの仕様に侵食される」と述べた理由である。

たとえばグループ削除のポリシーについて

もっと踏み込んだ話をする。 たとえば、SCIMではグループの削除がインターフェイスとして存在しているため、SCIMをカバーする場合には我々サイドのアプリケーションもグループの削除をサポートせねばならない。

また、SCIMのグループは階層化もサポートしているため、階層化されているグループの削除のポリシーも考えることになる。 簡単に考えても、階層化されているグループを削除する場合には次の4パターンがある。

  1. 親グループが消えたら子グループも消える
  2. 親グループが消えても子は残るがもとの親の親に紐づく(この仕様は子が親をひとつしか持たない場合のみ有効)
  3. 親グループが消えた場合には子は親を持たないグループになる
  4. 子グループを持つ親は削除できないようにする

さて、この4つの選択肢を出したとしてもSCIMでAzureADをサポートする場合にはc以外の選択肢を持つことができない。なぜならAzureADがcだからである! 我々が違う選択をしてくても、SCIMを使っている限りはSCIMに寄り添う以外不可能。そういうことなのである。

もちろんAzureADに対応しないという選択もできるが、シェアを考えると現実的ではない。

どうすればいいのか

自前のWebAPIのみを提供するという選択肢がある。これであれば自分たちで仕様を決められるのでSCIMに引きずられることはない。

しかし自前のWebAPIとなると、今度は顧客側に対応の工数を求めることになり、B2Bサービスなどは受注や導入開始などのリードタイムを長くする可能性がある。 B2Bアプリケーションにおいて「導入の容易さ」は大きな武器であり、ここを犠牲にするのは確実におすすめできない。

SCIMの利点はマスタデータを提供する多くのサービス(Okta, GSuite, AzureAD, etc)が大抵の場合すでにインターフェイスを実装しているため、使う側からすると導入コストがゼロに等しくなるというアドバンテージがある。 ビジネスの観点からするとここには勝てない。

顧客との力関係やその他もろもろの都合が合うのであれば「SCIMなどの外部連携インターフェイスを使わない」という選択肢も検討できるだろうが、本当にそれができるかはまた別問題である。

余談

AzureADとのSCIM対応は仕様を知ったり実装をする点でも実は結構大変... なのだが、それはまた別記事で書こうと思う。

*1:もちろん、バックエンド側でSCIMとの連携情報保持でしか使わないテーブルを用意するだけに留めるというのは可能。しかし、逆に言うと最低限それは絶対にやらなければならない。なにもしないは不可。

Elmでコンシステント・ハッシュ法を扱うパッケージを作って得たモジュール設計の学び

数ヶ月前にElmでコンシステント・ハッシュ法を扱うためのelm-consistent-hashingというモジュールを公開した。

github.com

こんな感じで使える。詳しくはテストを見るといいと思う。

let
    ch =
        ConsistentHashing.new Replica.default (Node.new "node1")
            |> ConsistentHashing.add (Node.new "node2")
            |> ConsistentHashing.add (Node.new "node3")
            |> ConsistentHashing.add (Node.new "node4")

    nextNode =
        ConsistentHashing.getNode (Key.new value) ch
in
-- ...

コンシステント・ハッシュ法についてはWikipedia以外にもこのQiitaの記事も詳しい。

qiita.com

コンシステント・ハッシュ法というと分散システムで使われるイメージが強いので、作った自分もフロントエンドで使い所があるのかは謎だが、いずれElmがサーバーサイドをやれるようになったらきっと広く使われるようになるだろう。

このモジュールは初めての公開モジュールだったが、作るにあたっていくつか工夫した部分がある。

パフォーマンスを考えたコレクション型の選択

Elmはデフォルトのコレクション([]とかで作られるやつ)がList型になっているが、Listは内部では線形リストになっている。

elm-consistent-hashingでも最初はなにも考えずに内部の仮想ノードのコレクションをList型でよしとしてしたが、コンシステント・ハッシング法では次に割り当てるノードを計算する際にソート済みの仮想ノードから検索を行うため、検索は二分探索のほうが効率が良くなる。

仮想ノードは基本的に数が多いので、ここの検索効率はいい感じにしておきたい。 したがってランダムアクセスに優れるArrayに置き換え、内部の実装は以下のようにした。ギリギリ末尾再帰になっていないのがちょっと悲しい。

{-| Looks up the nearest key by recursive Binary Search.
This is way better in performance using linear search with List in case of many keys.
-}
findInternal : Int -> Int -> Maybe Node.Node -> Key.Key -> Keys -> Maybe Node.Node
findInternal beginIndex endIndex currentNode key (Keys keys) =
    if beginIndex > endIndex then
        currentNode

    else
        let
            median =
                beginIndex + (endIndex - beginIndex) // 2
        in
        keys
            |> Array.get median
            |> Maybe.andThen
                (\( medianNode, medianKey ) ->
                    if Key.toString medianKey < Key.toString key then
                        findInternal (median + 1) endIndex (Just medianNode) key (Keys keys)

                    else
                        findInternal beginIndex (median - 1) (Just medianNode) key (Keys keys)
                )

なお、ElmにおけるListとArrayの違いはこのQiitaの記事が詳しい。 https://qiita.com/philopon/items/1a74e434520a06327064#arrayqiita.com

Elmの素晴らしいところはListモジュールがそもそも添字アクセスの関数を標準では提供していないところだ。 そもそもListは線形リストだから添字アクセスするものではない、だから添字アクセスしたければArrayを使え、という思想が標準モジュールの設計から伝わってくる。

とはいえ、デフォルトのコレクションがList型なのでArray型を使うには必ずデータを総なめするコストがかかる。 場合によってはこの計算量は無視できないので、敢えてListで添字アクセスをするという選択肢のほうがよい場合もある。 正直、このあたりはアプリケーションやらモジュールの性質によるので一概に答えを出すのは難しい。

たとえばelm-consistent-hashingにおいては、ノードを追加するappend関数内部でノード追加後のソートを実行するためにArray型になっているものを一旦List型に変換し直している。 しかし、モジュールデザインとして「ノードの追加というのはそう頻繁に行われないだろう」と想定して、敢えてノード追加時の計算コストを許容してArrayを採用している。

{-| Internal data structure of Keys is Array, but as an interface, List might be more generic, handy to use.
Plus, `append` is less likely to be called by users, so here probably won't be any performance issue.
-}
append : List ( Node.Node, Key.Key ) -> Keys -> Keys
append keys (Keys currentKeys) =
    keys
        |> (++) (Array.toList currentKeys)
        |> List.sortBy (Key.toString << Tuple.second)
        |> Array.fromList
        |> Keys

non-emptyなインターフェイス

new関数はもともとは初期化時に追加するノードをListで受け取るようなインターフェイスになっていた。

let
    ch =
        ConsistentHashing.new 
            Replica.default 
            [ Node.new "node1"
            , Node.new "node2"
            , Node.new "node3"
            , Node.new "node4"
            ]
in
-- ...

しかし、このインターフェイスでは空のノード配列を初期化時に渡せてしまう。

ノードが存在しない状態をこのようにして作れることは嬉しくない。 なぜなら常にノードが空である場合を常に考慮しないといけなくなるため、必然的にほとんどの操作関数の戻り値をMaybe型にせざるを得なくなってしまう。

したがって、初期化時のnew関数が必ずひとつのノードを取るようにした。

{-| Creates a new ConsistentHashing data
`new` function only takes one initial node at first. This interface is like non-empty list which aims to ensure one node always available at least for distribution.
If you want, you can add some more nodes one by one using `add` function.
-}
new : Replica.Replica -> Node.Node -> ConsistentHashing
new replica initialNode =
    -- ...

こうすることでノードが空でない状態というのが約束できるため、ConsistentHashing型のデータが存在する場合にはノードも必ずひとつは存在することが決まりMaybe型の登場回数を抑えられる。

このような「空でない状態」を表現するには、別の方法として幽霊型をつかう手もある。実際に幽霊型を使って「空でないリスト」を表現する方法は以下のQiitaの記事が詳しい。

qiita.com

一方でノードの削除時にはMaybeを返すようにしている。

{-| Removes a node
This function returns Nothing if all nodes were removed.
`remove` function internally run linear removing of all replicated virtual nodes so that it possibly results in huge computation cost in case you give a big number of Replica.
-}
remove : Node.Node -> ConsistentHashing -> Maybe ConsistentHashing
remove node (ConsistentHashing { replica, nodes, keys, head }) =
    -- ...

最後の一つを残してすべてのノードを削除することができないようなインターフェイスにしてもよかったが、new関数における「常にひとつは生きたノードが存在している」に対して、remove関数でノードを削除する際に「常にひとつのノードが生きている」というのは、なにかしらの大規模な障害が起きるケースではありえないこともあるだろうと考えてMaybeを選んだ。

大事故が起きたときにはすべてのノードがダウンしている可能性もあるし、その場合にはすべてのノードが削除され、割り当てることはできない状態なのでNothingとなる。

モジュール設計の勘所

自分が今回elm-consistent-hashingのインターフェイス設計をする上で意識したのは、ひとりのユーザーとして自分がこのモジュールを使う際にどうなっていたら嬉しいのか、どういうインターフェイスだったらバグを生みにくくできるか、のふたつ。

これは公開モジュールのみならず普段のプロダクト開発においてチームのメンバだけで使うモジュールの設計だとしても同じだし、もはやElmに限った話でもない。 計算量とインターフェイスというふたつの軸がメインだったが、個人的にはいつもこれをかなり意識してモジュールを設計している。

純粋関数型データ構造

純粋関数型データ構造

エンジニアHubで「Elm入門と実践」を寄稿した

エンジニアHubさんというあの全日本的に有名なWebメディアでElmの記事を執筆しました。

employment.en-japan.com

これまでにTypeScriptの記事とかはあったものの、関数型AltJSの記事はたぶん今回のElmが初めてです。

なんだかんだ国内でも少しづついろんな盛り上がりがでてきているこのタイミングでエンジニアHubさんにElmの記事を書けたというのはとても神がかり的なタイミングだったな〜と思っております。 本当にすごくいい経験になったし、これでElmに興味を持つ人が増えてくれれば感無量です。

記事で使われているサンプルアプリケーションのソースコードGithubで公開しました。ぜひElmアプリケーションを開発する際には参考にして頂けるかなと。

github.com

記事の背景

今回の記事は入門というタイトルが付いているものの、実際の記事の意図としては文中にもある通り、TODOアプリ的な小さなアプリケーションから、リチャード・フェルドマンのelm-spa-exampleのような極めてリアルなプロダクトに近いElmコードの間を埋める"ラダー"のような立ち位置の記事にしたい、というものがありました。

編集の方から最初に執筆のお話を頂いたときも、やはりインターネットにおけるElmの学習リソースの偏り、つまり「書き始めるのは簡単だけれど、ある程度規模のあるアプリケーションを作るためのノウハウがまだ見つけられない」というのはすごくあるなと自分でも感じていて、それを埋められるような記事を書けたらいいだろうな、と考えながらトピックを選択しました。

もちろん、これはElmに限った話ではないんですが、それでもやはりReactやVue.jsと比べて、いかにElmアプリケーションを"大きく"育てていくかというトピックはまだまだ少ないとは僕も感じています。 そんな背景を踏まえた上で、自分の記事がElmを学ぶエンジニアたちの手助けとなるラダーになってくれれば嬉しいです。

モデルのデータをどこで計算するのか問題

余談ですが、記事を書いている際、弊社のエンジニアのレビューでサンプルアプリケーションの設計に少し悩んだこと部分がありました。 それは、画面で表示されるデータをモデルからどのタイミングで計算するのかというところです。

今回自分が寄稿したエンジニアHubのECカートアプリは、update関数がモデルに保持しているデータをもとに次のデータを計算してモデルに格納しています。 たとえば、サンプルコードにおけるCart型は以下のような定義になっており、taxesやshipping等の値はproductIdsによって従属的に決定される、という形です。

type Cart
    = Cart
        { productIds : ProductIds -- これがshippingやtaxesの値を決定する
        , shipping : Int
        , taxes : Int
        , subTotal : Int
        , total : Int
        }

モデルに計算結果が予め格納されていることのメリットは

  • update関数に計算を凝集できるためview関数をシンプルにできる
  • モデルの状態から画面が予測しやすいためデバッガビリティが高い

というものがあり、今回のサンプルコードでは個人的な推しとしてこのスタイルを採用しています。 実際に文中でもこんなことを書きました。

適切に設計されたアプリケーションのModelはViewのコードに現れます。Viewの中でModelのデータを複雑に計算しているとしたら、それはModelのデータ構造の設計を見直す必要があるでしょう。 Modelのデータに文字を付け加えたり、case文でパターンマッチによるHTMLの出し分けをしている程度になっているのが、Viewの責務としては理想です。

Elm入門と実践 - 買い物カートを作ってアーキテクチャ「TEA」を学ぶ - エンジニアHub|若手Webエンジニアのキャリアを考える!

しかし、ElmにおいてモデルというのはいわゆるSingle Source of Truthにあたるものであるため、たとえばモデルにデータAという値が格納されている場合、その値から計算される別のデータBという値が同じようにモデルに格納されているのは、実質データAだけで計算可能な値を重複して格納しているためSSoTに違反しており、結果として誤ったデータを格納する原因になるのではないかという指摘です。

たとえば、サンプルコードにおけるCart型は以下のような定義になっており、taxesやshipping等の値はproductIdsによって従属的に決定されますが、フィールドとして存在していることでコーディングミスなどで誤った値が入る可能性があるということです。 ではどうするのが良いかと言うと、これを以下のようにproductIdsだけにします。

type Cart
    = Cart ProductIds

もともと存在していたshippingやtaxesなどの値は、別途view関数などの中で計算します。Lazyなどを使っていれば実質ProductIdsが変わっていなければDOM操作は行われないため計算コストはさほど問題ありません。

正直、これには決まった答えがないのでどちらのスタイルを採用するかというのはチームで話し合ってキメをやっていくしかない、いわゆる要はバランス™としか言えないのが事実です。 個人的にはどっちを採用してもいいかなとは思っています。合意がとれればそれでOK、という感じで。

結論

Elmって最高だね!

基礎からわかる Elm

基礎からわかる Elm

  • 作者:鳥居 陽介
  • 発売日: 2019/02/27
  • メディア: 単行本(ソフトカバー)

渡米してDeveloperWeek 2020で登壇した

アメリカのDeveloperWeek2020というイベントに参加して登壇発表した。

izumisy.work

改めて考えると、社会人になってから初の渡米&初のアメリカでの登壇。
ヨーロッパもいいけど、やはりテックカンファレンスといえばアメリカだよね。

DeveloperWeekとはなんなのか

f:id:IzumiSy:20200224151346p:plain

上の記事でも書いたが、オークランドで開催される規模の大きなエンジニア向けのカンファレンス。 5日間のスピーカーセッション、展示会そして2日間のハッカソンが開催され、文字通り1週間を通してテッキーなイベントが満載、という感じ。

意外にも歴史があり、なんと2013年から継続して開催されている。なので、今年で約8年間開催していることになる。 カンファレンスというのは規模が大きくなればなるほどスポンサーを見つけたり参加者をコンスタントに維持したりするのが難しくなりがちなので、 そんな中で継続して開催されているのは、それだけですごいと思う。

f:id:IzumiSy:20200228113115p:plain
CFPに通るとこんな感じのメールが届く

日本からオークランド

今回の登壇では前回のelm Europeと同様、イベント開始1日前にオークランドへ到着。

現地にはアメリカ時間で朝10時くらいに到着したが、実は登壇の緊張からか機内で過ごす10時間ものあいだ一睡もできていなかった。 そんなこんなで、到着初日はホテルにチェックインしてそうそう、昼から夕方までガッツリ寝てしまい、あまりいい感じにジェットラグを修正できなかった。

翌日のイベント初日は、特に眠くもなかったせいで朝4時に起きてしまった。 軽く周辺を散歩した後、朝6時半くらいに宿泊しているホテルの食堂でシリアル、ヨーグルト、スクランブルエッグなどの朝飯をガッツリ食べ、会場へ向かうことに。

f:id:IzumiSy:20200224183156j:plain
早朝のオークランド。会場となったマリオットホテルはチャイナタウンの近くだったので、比較的アジアンな雰囲気の町並み。歩いていけるくらい近くにブルーボトルコーヒーもあってよかった。

会場の雰囲気

これは海外のカンファレンスあるあるなのだが、日本と比べて圧倒的に朝が早い。

DeveloperWeekも同じで、会場のレジストレーション・ブースは朝8時からオープンという気合の入りっぷり。意外と朝早くに行ったにも関わらず、ロビーからかなり並んでいてびっくりした。

f:id:IzumiSy:20200212111657j:plain:h500
レジストレーション・ブースの様子

f:id:IzumiSy:20200212113151j:plain:h600
SPEAKER!

こんな感じで、参加者が首からかけているバッジで各参加者区分がひとめで分かるようになっている。 スピーカー意外にはスポンサー、ブース出展者、プレミアムパス、スタッフなどの区分があるようだった。

海外のカンファレンスの参加者というのは大半が会社から金を出してもらって参加している。 そのせいなのかは分からないが、参加者の大半が一番高いプレミアムパスを身に着けていた。

スピーカーセッションが行われるステージは会場全体で大小合わせて5-7つほどあり、常にすべての会場でなんらかのセッションが行われている形になっていた。 1セッションあたりが大体25分、長いものだと50分程度になるため、必然的にある時間帯で聞きたいセッションを排他的にひとつ選ばないといけなくなる。 個人的には聞きたいセッションが同じ時間になっていることも多く、これはかなり難儀した。

f:id:IzumiSy:20200212120020j:plain
自分が登壇したGrand Ballroom Aというステージ。このステージで主にJavaScript関連のトークが行われていた。

f:id:IzumiSy:20200212175356j:plain
最も大きいMain Stageと呼ばれる登壇会場。ここではキートークから、対談形式のトークなど人がたくさん集まりそうなセッションが開催されていた。

スピーカーにも比較的大御所の参加者が多く、 有名所では Atlassian, Paypal, Spotify, Amazon あたりの世界的に著名なテック企業から様々なスピーカーが参加しているという感じ。 トークの内容も多様で、MLOpsからマイクロサービス、チーム開発からReactの実装パターンまで多岐にわたる。

f:id:IzumiSy:20200213091010j:plain
Expoホールのマップ。このホールは2日目から開場し、朝から晩までずっと騒がしかった。

自分のトークに関して

自分のセッションではElmに関してちょろっと説明し、実際に弊社でElmを使ったプロダクト開発をやってどんな感じであるかをざっくり話した。

トークの資料そのものは関数型プログラミングカンファレンス2019で使ったものにもう少しJS的なエッセンスを足したものである。 資料自体はすでに英語で作成されたものだったので、リライトには特に困らなかった。 若干関数型プログラミングを知っている前提の内容が多かったため、もう少し純粋なフロントエンドな人たち向けに書き換えたものを用意した。

izumisy.work

機材トラブル

今回のカンファレンスは、なんと登壇の際に会場に自分のPCを持ち込むことができないというルールがあった。 そのため、事前にスライドを提出しておき当日は会場に備え付けの(あるいは公式が用意した)ラップトップを使わねばならない。

自分は登壇資料をGoogleスライドで作成していたのだが、当初提出可能なフォーマットにGoogleスライドが含まれていなかったため、事前に運営へGoogleスライドは使うことができるのか」と問い合わせていた。 数日して返ってきた運営の回答は「問題ないが、GoogleスライドのURLを貼り付けたPPTファイルを提出してくれ。」とのことだった。 なるほど、そんなURLが貼り付けられただけの資料をどのように使うのかは分からないが、とりあえずOKということで雑に提出することにした。

正直、この時点で少し当日なにか起きるかもしれないという可能性を10%ほど感じていた。 というのも、自分はトークの内容をすべてスピーカーノートに英語で全部書いておき、発表中はそれを読むだけでいいようにしているからだ。 そんなわけで、会場の機器がどういう設定になっているか分からないが、もしスピーカーノートが見れないと大変なことになってしまう。

しかし、今回は実際にその10%の確率で予想していたことが起きてしまった

(自分のセッションが始まり、ステージ上で準備中)
ぼく 「あれ、スピーカーノートが見れないんですけど」
スタッフ 「え、まじで?」

SafariGoogleスライドのスピーカーノートを開いた状態で全画面化すると、なぜかメインのモニタが真っ黒になってしまう。 なんてこった、これではトークの内容がわからないので話せない。

スタッフにヘルプを要求すると、スタッフもただならぬ事態に動揺を隠せない様子。 しばらくするとワラワラと他のスタッフも駆けつけてくる。 みんな映像機器とガチャガチャといじってくれるものの、おそらく誰もまともに会場設備のセッティングを理解していないことが伝わってくる。

ぼく 「こうなったら、スマホを見ながら話します。これでも見れるんで。」
スタッフ 「それは... やりやすい?(Is it comfortable for you?)」
ぼく 「いや、ぜんぜん... けどもうそれしかないですよね」
スタッフ 「...じゃあそうしよう」

20分尺のところ、さすがにセットアップで5-6分も過ぎはじめていて危険な空気が漂い始めていた。 こんなときのためのコンティンジェンシープランとして用意していたスマホでスピーカーノートを見るという手段を、まさか本当にやることになるとは思わなかった。

さすがに準備の間の緊張がピークだったこともあり、良くも悪くも話している間はすごく平穏な気持ちでトークができた。 この経験から、次からは絶対にスピーカーノートを頼らずに、トークの内容を暗記していくことを心に誓ったのであった。

もう二度とこんな悪夢みたいな体験はしたくない。

DeveloperWeekについて改めて感じたこと

参加してから数日経って、どうやらDeveloperWeekというイベントは存在そのものが見本市的なノリのカンファレンスなんだということが分かってきた。

みんなのトーク内容の殆どが自分たちの製品を売り込むためのセールストーク的な内容で、事実スピーカーの大半はCTOやらセールス・エンジニアというポジションである。 自分のPRO SESSIONというやつも、おそらく製品の売り込み的な意味でPROだったのかなと分かってきた。

とはいえ、資料作成時のポリシーには「スポンサー枠のスピーカーじゃない場合には自社製品のアピールとかしちゃダメよ」みたいなことも書いてあったので、自分みたいに純粋に会社とか製品関係なくトークをしていた人もいたのかもしれないが、あまり目にはつかなかった。

2015年に自分が参加したときはハッカソンしか見ていなかったのでよくわからなかったが、スピーカーセッションのほうはこんな感じでコマーシャル的な要素がとても強い。 まあ、イベント自体やトークの内容自体、おもしろくはあったが、それでも去年参加したelm Europeのようなコミュニティ感というのはマジでゼロだった。

izumisy.work

感想

まさか、アメリカで人生最大のピンチとも言えるタイミングを迎えることになるとは思わなかったが、これはこれである意味ものすごいスリルだったので、終わってみるとジェットコースター的な興奮があってウケた。

アメリカで登壇失敗しても日本に返ってくればチャラみたいなものなので、失敗なんてなにも怖くないというのがリアルな感想である。とはいえ、もしこれがYoutubeとかに残ってしまうと思うと... 最悪で仕方がない。

カンファレンス自体は、規模はデカいわ、いろんなトークが聞けるわ、いろんな人と英語で話せるわで楽しかったが、むしろ改めてElmのコミュニティって暖かいんだな〜と思わされた。 やっぱコミュニティと繋がりの強さって大事ね。

ドメイン・イベントについて&Ruby製のよさげなPub/Subインターフェースgemまとめ

Rubyで特にRailsを使う際に「特定のドメインの変化によって別の処理の実行をトリガする」みたいなケースでは大抵の場合コールバックが使われる。 しかし、ぶっちゃけた話コールバックはかなり結合度の高いコードになってしまいがちで、実装的にスケールさせるためにはドメイン・イベントを使うほうが健全であると言える。 martinfowler.com

以下の記事はActiveRecordのコールバックがどのようなときによろしくない感じになるかを説明しており、非常に参考になる。一言で言うと、コールバックを使うことモデル自体に副作用が埋め込まれてしまう。一方でドメイン・イベントを使うことで、副作用がなにをするものなのか(メールを送る、外部のmBaaSを更新する、モニタリングサーバへメトリクスを送信する、等)を意識しないでよくなり、疎結合になる。 techracho.bpsinc.jp

さて、ドメイン・イベントの仕組みを実装する際、もちろんオンメモリなオブザーバ・パターンの仕組みを実装してもよいが、それは開発環境でしか使えない。プロダクションだと大抵APサーバは複数台構成のはずなので、Redisあたりにキューして複数の異なるAPサーバ上のサブスクライバがイベントを拾えるようにしたい。こうしておけばQueue-based Load Levellingもできるようになるので負荷分散もできてスケールする。

もっというならGCPのときはCloud Pub/Subで、AWSのときはSQSを使ったりできるようなプラガブルな感じになっていると最高だ。そんなものを求めていくつか探してみた。

dry-rb/dry-events

github.com

  • みんな大好きdry-rbシリーズのPub/Subインターフェイス
  • おそらく今回紹介するgemの中で一番シンプル。
  • Dry::Events::Publisherというモジュールをミクスインしたクラスを作ってサブスクライバを定義すると、そのクラスに対してイベントをパブリッシュできる。これだけ。
  • 開発が比較的活発なのもいい

kris.leech/wisper_next

gitlab.com

  • 元々はwisperというPub/Subインターフェイスgemを作っていた作者による新しいライブラリ
  • WisperNext.publisherWisper.subscriberというふたつのモジュールが用意されておりそれらをミクスインしたクラスを作るスタイル
  • パブリッシャクラスに対してsubscribeメソッドでサブスクライバクラスを追加していく。
  • 特定のプレフィクスを持つイベントのみをサブスクライブできる機能もある。
  • サブスクライブをする際に同期/非同期を選択できる。

kris.leech/ma

gitlab.com

  • 上と同じ作者のPub/Subインターフェイスgem。内部的にはwisper_nextが使われている。
  • ライブラリの説明に "events as first class citizens." と書かれている通り、wisper_nextとの違いはパブリッシュ/サブスクライブするイベントをクラスとして定義できること。Ma::Eventクラスを継承したクラスをイベントとして扱うことができる。
  • それ以外にはMa.publisherMa.subscriberというモジュールが用意されており、それぞれをミクスインしたクラスを作るスタイルで特に目新しいものはない。

edisonywh/rocketman

github.com

  • 今回紹介するgemの中で一番重厚かつ拡張性が高い。
  • Rocketman::ProdcuerRocketman::Consumerのふたつのモジュールがいて、それぞれをミクスインしたクラスでemitとかon_eventみたいなメソッドが使える。まあわかりやすい。
  • Redis上のイベントもサブスクライブできるアダプタが用意されている。現状はRedisだけだが、Registryという仕組みが用意されているので自前でカスタムの外部サービス用アダプタを作ることも可能。
  • 初期設定では発行されるイベントがキューとしてメモリ上に保存され、Rocketmanのスレッドワーカーが処理していく仕組みになっている。これだとアプリケーションが死んだときキューをロストするので、もしキューを永続化したければRedisをストレージとして使うことができる。
  • 作者によるとRedis PubSubやKafkaはそのあたりのミドルウェアをメンテしたりするコストがかかるが、Rocketmanを使えば実質アプリケーションレイヤで同じことができるので、すごく楽だよ、というのが売りらしい。

総括

アプリケーション層でPub/Subしたいだけならどれでもよさそう。もっとリッチに、イベントを永続化したり複数台構成で負荷分散とかに使いたい場合はrocketmanがよさそう。GCPとかAWSあたりのPub/Sub製品のアダプタも自前で作れそうだし。ただメンテされているのかどうかが若干気になるが...

Reactive Design Patterns

Reactive Design Patterns