Elm でマテリアルデザインなウェブアプリを作った

アプリ概要

アプリとしてはニッチな用途で、写真用のモノクロネガフィルムを自家現像するときに便利なタイマーアプリです。下記は埋め込まれた実際に動くアプリです。

Photo Film Dev で公開しています。

時間配分をレシピと呼んでいますが、レシピはサーバーに保存されるためログインすれば複数の端末でアプリを使用できます。

今はレシピは自分のアカウントでしか見れないですが別のアカウントに共有する機能が作れたらなと考えています。

技術構成

ソースコードGitHub に GPL3 で提供しています。

github.com

アプリの技術構成としては次の通りです。

Elm

Elm は言語としては Haskell 様の文法を採用した alternative HTML/CSS/JS です。The Elm Architecture がいい感じです。チュートリアルを読めば書けるようになります。

モジュール分割

最初は1ファイルに全部書いて始めたのですがわりとすぐにどの辺りに何を書いたか分からなくなったのと同じ名前を付けたいものが出てきたのでモジュール分割をしました。詳細は後述します。

モジュール 役割
Model Msg 以外のデータ型
データ同士の変換関数など
Model.Foo enum として定義する Foo
getset など関連する関数
Msg Msg のデータ型
Msg.Foo 入れ子になる Msg のデータ型
update 関数と対応する
Port port 関数とその引数になる型
その型と内部で使用する型との変換関数
Cmd Port で定義した関数を組み合わせた Cmd を生成する関数
Sub Port で定義した関数を組み合わせた Sub を生成する関数
Update 直和型として定義した Msg 型のそれぞれの値構築子に対応する update 関数
View 画面要素ごとに分割した view 関数
Text 多言語テキスト

Enum

Elm にはアドホック多相が組み込み関数にしかありません。例えば Step という段階を表す型を作って Haskell であれば Enum 型クラスにするところですができないのでモジュールを分けて toIntfromIntcompare などを定義することになります。標準ライブラリーの型でも Maybe など Monad になるものはモジュールごとに andThen を定義しているので正当な方法だと思います。

module Model.Step exposing (..)

type Step
    = Soak
    | Dev
    | Stop
    | …

toInt : Step -> Int
toInt step =
    case step of
        Soak -> 0
        Dev -> 1
        Stop -> 2
        …

fromInt : Int -> Maybe Step
fromInt value =
    case value of
        0 -> Just Soak
        1 -> Just Dev
        2 -> Just Stop
        …
        _ -> Nothing

compare : Step -> Step -> Order
compare step0 step1 = Basics.compare (toInt step0) (toInt step1)

Dict 様レコードでの DRY

フィールドの型が全部同じで辞書(マップ)として使用するような下記のようなレコードの場合フィールドごとに同じような処理を書こうとしてもそのままでは実装を複製するしかありません。Dict にする手もありますが存在することが分かっているのに不必要な Maybe だらけになってしまいます。

type alias TimeSpans =
    { soak : TimeSpan
    , dev : TimeSpan
    , stop : TimeSpan
    , fix : TimeSpan
    , rinse : TimeSpan
    , wet : TimeSpan
    }

そこで先の enum として作った型をキーとして getset 関数を作ります。

module Model.Step exposing (..)

…

get : Step -> { soak : a, dev : a, stop : a, fix : a, rinse : a, wet : a } -> a
get step =
    case step of
        Soak -> .soak
        Dev -> .dev
        Stop -> .stop
        …

set : Step -> a -> { soak : a, dev : a, stop : a, fix : a, rinse : a, wet : a } -> { soak : a, dev : a, stop : a, fix : a, rinse : a, wet : a }
set step value record =
    case step of
        Soak -> { record | soak = value }
        Dev -> { record | dev = value }
        Stop -> { record | stop = value }
        …

これで下記のようにアクセスできます。

timeSpans |> get step
timeSpans |> set step value

これで don't repeat yourself にのっとって処理をまとめることができます。

Msgupdate の分割

Msgupdate の分割のしかたは対応させています。

-- Msg.elm
type Msg
    = Drawer Visible
    | SelectRecipe Recipe
    | GoRun
    …

-- Main.elm
update : Msg -> Model Msg -> ( Model Msg, Cmd Msg )
update msg model =
    case msg of
        Msg.Drawer visible -> Update.drawer visible model
        Msg.SelectRecipe recipe -> Update.selectRecipe recipe model
        Msg.GoRun -> Update.goRun model
        …

Msg.Drawer には Update.drawer というように対応させています。入れ子になったメッセージである VisibleMsg.Visible モジュールに定義してあります。Model の状態による条件分岐は各々の update の中で行っています。

PortCmdSub

Model モジュールに Recipe は下記のような型で定義しているのですがこれは port 関数で JavaScript に渡したり JavaScript から渡されたりすることができません。なので Port モジュールに同名の Recipe を作り変換関数も Port に定義しました。

-- Model.elm
type alias Recipe =
    { id : UUID
    , name : String
    , timeInputs : TimeInputs
    }

-- Port.elm
type alias Recipe =
    { id : String
    , name : String
    , timeInputs : Model.TimeInputs
    }

Port.Recipe から Model.Recipe への変換には elm-form-decoder を使いました。フォームからの変換以外でも外界からデータを取り込む場合に便利です。

Port.setRecipeCmdPort.Recipe に依存していますが Cmd.setRecipeModel.Recipe に依存するように Port の変換関数を使って変換しています。Sub.changeRecipe も同じく Port.changeRecipeSub から変換していますが、msg の構築子が2つあるのは変換の成功時・失敗時を表現するためです。

-- Port.elm
port setRecipeCmd : Recipe -> Cmd msg -- ここの Recipe は Port.Recipe

-- Cmd.elm
setRecipe : Recipe -> Cmd msg -- ここの Recipe は Model.Recipe
-- Port.elm
port changeRecipesSub : (Array Recipe -> msg) -> Sub msg -- ここの Recipe は Port.Recipe

-- Sub.elm
changeRecipes : (List RecipeDecoderError -> msg) -> (Dict String Recipe -> msg) -> Sub msg -- ここの Recipe は Model.Recipe

Material Design

Material Design を採用しライブラリーとしては elm-mdc を使用しました。UI センスがなくても Material Designガイドラインにしたがえばそれっぽくなるので便利です。

github.com

Elm のライブラリーは Elm しか内包できません。このライブラリーは Elm の他に JavaScript なども含むため Git リポジトリーとして公開され、サブモジュールとして依存します。

elm.jsonpackage.json は下記のようになります。

// elm.json
{
    "type": "application",
    "source-directories": [
        "src",
        "elm-mdc/src"
    ],
    "elm-version": "0.19.1",
    "dependencies": {},
    …
}
// package.json
{
  "dependencies": {
    "elm-mdc": "file:elm-mdc",
    …
  },
  …
}

ビルドタスクは GNU Make で管理しているのですが Parcel を使用しているにもかかわらず上記事情のため elm-mdc の面倒も見ているので少々複雑になっています。

多言語テキスト

Text モジュールでは下記の型の値を列挙しています。といっても今のところ日本語のみですが。

type alias Text = Language -> String

モジュール依存関係

モジュールの依存関係を下図に示します。Text は図から省略しています。

f:id:kakkun61:20200720040856p:plain
モジュール依存関係

Text は他のいずれにも依存しません。Model グループは Text にのみ依存します。黄色で ModelMsg を囲っているものは単に矢印の数を削減するためのもので、黄色枠への矢印もしくは黄色枠からの矢印はその中の1個以上への矢印もしくはその中の1個以上からの矢印と読んでください。MsgModelText にのみ依存します。他に特記すべきは PortCmdSub のみから依存され必ずラップされたものが他から参照されます。

Firebase

サーバーは自前で書くつもりだったのですが Firebase をさわってみたら認証とストレージだけで十分だったのでサーバーは自前では立てないことになりました。local storage を使ってスタンドアローンで動くところまで作っていましたが Firestore がオフラインの面倒も見てくれるので local storage の実装は消しました。

先に書いたように Elm のライブラリーは Elm のみに限定されるので、必然的に JavaScript に依存する Firebase のラッパーライブラリーはありません。とはいえ書くのはグルーコードなので大した問題ではありません。

クレジットページ

頒布をともなう場合、多くのオープンソースライセンスはコピーライト表記とライセンス表記を求めます。なのでクレジットページを作成しました。これは自動生成されるようにしています。NPM License CheckerElm License Checker を使用して生成しています。Elm License Checker はなかったので作りました。(Elm License Checker は PureScript 製です。作成時 Elm で CLI を作れることを知らなかったので。)

www.npmjs.com

謝辞

Discord の Elm-jp サーバーのみなさまにはお世話になりました。

Windows Terminal に MSYS2/MinGW64 を追加する

Windows 10 2004 にしたので Windows Terminal をさわってみた。自分の用途だと Cmder はもう使わなくていいなと思う。

さて、Windows Terminal に MSYS2/MinGW64 を追加したのでその設定を示す。

{
    …,

    "profiles":
    {
        …,

        "list": [
            …,

            {
                "guid": "{43257a9a-8d31-4208-8ed4-3d4365d44bd1}",
                "name": "MSYS2/MinGW64",
                "commandline": "%ChocolateyToolsLocation%\\msys64\\msys2_shell.cmd -defterm -no-start -mingw64 --login -i",
                "icon": "%ChocolateyToolsLocation%\\msys64\\mingw64.png"
            }
        ]
    }
}

profiles.list にレコードを追加する。

GUID は適当に生成した GUID を設定する。

commandlinemsys2_shell.cmd を経由してシェルを起動するようにする。msys2_shell.cmd を使用することで初期設定や環境変数の設定などをコマンドライン引数で行える。

アイコンは mingw64.exe から抽出したものを使用している。

Windows のホームフォルダーにドットファイルを作らないでほしい

Windows ではファイル名の先頭にドットを付けるだけでは隠しファイルにならないのでユーザーのホームフォルダーを汚染することになる。

だいたいの場合では LOCALAPPDATA 環境変数の示す場所にソフトウェア名のフォルダーを作ってその下を自由に使えばいい。

LOCALAPPDATA の示す場所は普通 C:\Users\kazuki\AppData\Local といったぐあいだが、ユーザーが変えている場合もあるので固定パスにするのはよろしくない。

また、隠しファイルを作りたい場合はファイルシステムの隠し属性を有効にしてほしい。そうするとデフォルト状態ではそのファイルは見えなくなる。

LOCALAPPDATA 以外の似たようなフォルダーはWindowsのディレクトリ構成ガイドライン - torutkのブログの記事がくわしい。

torutk.hatenablog.jp

Haskell 環境構築ツールフローチャートを作りました

Haskell 環境構築ツールフローチャート

前に環境構築についての記事を書いたのですが、初学者向けにパッと見て分かるようにフローチャートにしました。

kakkun61.hatenablog.com

Google ドライブ 図形描画のファイルはこちらです。コメントを付けることができます。

docs.google.com

新しいディスプレー

f:id:kakkun61:20200307011401j:plain

会社が 4K ディスプレーなのもあって、家でプログラミングしてたら画面が窮屈に感じてきてうちも 4K ディスプレーを導入しました。

特に Elm の標準のフォーマットが密度低いのでフル HD だと不便だったのが最後の引き金でした。

4K かつ HDR にしました。

KEIAN 28インチ4K対応LEDモニター KWIN28 ゲーミングモニター

KEIAN 28インチ4K対応LEDモニター KWIN28 ゲーミングモニター

  • 発売日: 2019/04/20
  • メディア: Personal Computers

4K で High DPI 設定による高精細は言わずもがな HDR も綺麗ですね。

綺麗なんだけど WindowsHDR 出力をオンにしたときの SDR(今までのダイナミックレンジを表すレトロニム)コンテンツが白くかすんでしまって WindowsHDR オンにするのは時期尚早な感じがします。

自分は Windows では HDR はオフにしました。

PS4 をせっかく Pro 買ったのに今まで full HD の SDR でしか出力してなくもったいなかったのがやっともったいなくなくなり、Windows がいまいちでもとりあえず満足です。

HDMI にバージョンがあることを知らず PS4 Pro から 4K 60Hz が出ず後悔しそうでしたが、ディスプレーの HDMI 入力端子によってバージョン 1.4 と 2.0 が違うらしく挿し直したらちゃんと出力してくれました よかった。

ディスプレー買って初めて輝点があったのでちょっと凹んだんですが、グレー一色表示とかしなければ意外とめだたなくてよかったです。 でもやっぱり今度から輝点黒点保証付けて買おうかな。

……

…………

と、これだけでは終わらず右下のディスプレーも新しく買ってて、こっちはフル HD だけど Adobe RGB カバー率 99% のカラーキャリブレーションディスプレーです。

10bit カラーでもあります。

デジカメで Adobe RGB で撮ってましたがこれまで sRGB でしか見れてなく、見れないものを画像編集しててモヤモヤしてたのが、やっと本当の色を見ることができます。

特に緑の鮮やかさに差が大きいらしいのでわざとそういうの撮って比較したいですね。

カラーキャリブレーションの機能はこれ単体ではできなくカラーセンサーがまた高いのだけどそれは先の話ということで……

4K かつ Adobe RGB にすればよかったじゃんという意見はごもっともなんだけど14万円とかして用途別に2台買う方がやすいんですよね……

postgresql-pure を開発しました

この記事は Haskell Advent Calendar 2019 の6日目の記事です。


hackage.haskell.org

postgresql-pure は HaskellPostgreSQL ドライバー(クライアントライブラリー)で次のような目標で開発しました。

  • マルチコア環境でのパフォーマンス向上
    • 暗黙のロックを回避する
  • マルチプラットフォーム対応
    • C ライブラリーの libpq への依存をなくして特に Windows でのビルドを容易にする
    • 既存ライブラリーとしては postgres-wire が高速だがそれは Windows をサポートしていない
    • pure Haskell 実装のため Eta などの環境へも移植しやすい可能性がある

使用方法

簡単に使用方法を説明します。

下記のようなテーブルがあるとします。

CREATE TABLE person (
  id serial PRIMARY KEY,
  name varchar(255) NOT NULL
);
INSERT INTO person (name) VALUES ('Ada');

このとき ghci で下記のように実行できます。

> :set -XOverloadedStrings
> :set -XFlexibleContexts
> :set -XDataKinds
> :set -XTypeFamilies
> :set -XTypeApplications
> 
> import Database.PostgreSQL.Pure
> import Data.Default.Class (def)
> import Data.Int (Int32)
> import Data.ByteString (ByteString)
> import Data.Tuple.Only (Only (Only))
> import Data.Tuple.List.Only ()
> import Data.Tuple.Homotuple.Only ()
> 
> conn <- connect def
> preparedStatementProcedure = parse "" "SELECT id, name FROM person WHERE id = $1" Nothing
> portalProcedure <- bind @_ @2 @_ @_ "" BinaryFormat BinaryFormat (parameters conn) (const $ fail "") (Only (1 :: Int32)) preparedStatementProcedure
> executedProcedure = execute @_ @_ @(Int32, ByteString) 0 (const $ fail "") portalProcedure
> ((_, _, e, _), _) <- sync conn executedProcedure
> records e
[(1,"Ada")]

重要な部分を抽出すると parsebindexecute の順に呼びだし、最後に sync でサーバーに送信します。parsebindexecute は入出力のない関数であり、リクエストのビルダーと対応するレスポンスのパーサーを構築しています。そしてそのビルダーとパーサーを sync が使用して送受信を行ないます。bindモナド値を返すようになっているのは失敗する可能性があるためで、型は MonadFail m => m a となっており IO a ではありません。

型適用が所々に明記されていますが、これは ghci で実行しているので結果の使用部分からの推論ができないためで、実際のコードではほとんどの場合で型適用の明記は必要なくなります。

postgresql-pure では、タプルによる要素数の不一致を型検査で検出するインターフェース Database.PostgreSQL.Pure(上記の例)と、しないインターフェース Database.PostgreSQL.Pure.List と、HDBC 互換インターフェース Database.HDBC.PostgreSQL.Pure の3つを提供しています。

高速化

高速化に寄与した技術を説明します。

Haskell

まずは Haskell 一般に関連するものについての技術です。

大きな byte string を何度も確保しない

送信と受信のたびに byte string を確保することを避けました。約 3 kB 以上のメモリーを確保すると暗黙のグローバルロックがかかります1。送受信のたびに確保するのをやめ、代わりにバッファとして確保した領域を何度も再利用するようにしました。手動によるメモリー管理のために bytestring パッケージの Data.ByteString.Internal.mallocByteString と network パッケージの Network.Socket.sendBufrecvBuf を使用しました。

mallocByteString :: Int -> IO (ForeignPtr a)
sendBuf :: Socket -> Ptr Word8 -> Int -> IO Int
recvBuf :: Socket -> Ptr Word8 -> Int -> IO Int

接続時に mallocByteString で2つのバッファを確保します。送信するメッセージは Data.ByteString.Builder.Extra.BufferWriter によって構築し、受信するメッセージは Data.Attoparsec.parseWith でパースします。

シンボルには ShortByteString を使用する

LISP のシンボルのような短い文字列には ShortByteString を使用しましょう。ShortByteStringByteString よりもオーバーヘッドが少なく、またヒープフラグメンテーションを引き起こしません。ShortByteString には lengthindex のような簡単な操作だけが提供され複雑な操作は提供されていません。このライブラリーではサーバーのパラメーターを保存するために使用しました。

PostgreSQL

次に PostgreSQL 固有の効率化について説明します。

PostgreSQL プロトコルには2つの問い合わせ方法があります。ひとつは簡易問い合わせで、もうひとつは拡張問い合わせです。簡易問い合わせには最小限の機能しかなくプリペアドステートメントやバイナリーフォーマット、結果を1レコードずつフェッチすることなどはサポートされていないので、このライブラリーでは拡張問い合わせを採用しました。

PostgreSQL プロトコルTCP の上で動作し、複数のメッセージは結合してひとつの TCP ペイロードに格納することができます。

下記の図はメッセージをひとつずつ送信した場合を表しています。

f:id:kakkun61:20191203182127p:plain
メッセージをひとつずつ送信した場合

メッセージを結合した場合は下記のようになります。

f:id:kakkun61:20191203182234p:plain
メッセージを結合した場合

メッセージを結合することで送受信するデータ量を減らし、またシステムコールの回数も減らすことができます。

既存のドライバー

ベンチマーク

下記のような単純な定数値のみの問い合わせを秒間何回問い合わせられるかを計測しました。定数値を使用したのはサーバーがボトルネックにならないためです。

SELECT 2147483647 :: int4, 9223372036854775807 :: int8, 1234567890.0123456789 :: numeric, 0.015625 :: float4, 0.00024414062 :: float8, 'hello' :: varchar, 'hello' :: text, '\xDEADBEEF' :: bytea, '1000-01-01 00:00:00.000001' :: timestamp, '2000-01-01 00:00:00.000001+14:30' :: timestamptz, '0001-01-01' :: date, '23:00:00' :: time, true :: bool;

環境は下記の通りです。

計測結果を下に示します。縦軸は秒間リクエスト数で横軸はスレッド数です。

このライブラリーも postgres-wire も約4スレッドまではほぼ同じパフォーマスです。それ以上になると postgres-wire が線形比例を下回っていくのに対し、このライブラリーはより線形に近くなっています。

(比較対象は開発途中でのベンチマークにおいておそかったものを除去しています。)

f:id:kakkun61:20191203182308p:plain
ベンチマーク結果


postgresql-pure は IIJ イノベーションインスティテュートの業務として作成されました。