ジンジャー研究室

長めのつぶやき。難しいことは書きません。

CSS in Elm 方式を導入してから1年半以上たった感想

CSS in JS(Elm)したら想像以上に良かった という記事をずいぶん前に出して結構ブクマを貰えたんだけど、今は「なんだかなー」と思っているので整理する。冷静に考えると、そもそもこのエントリで書いていることのほとんどが「良かった」ではなく「悪くなかった」としか言ってない。

class が無いのでどのスタイルを直せば良いのか分からない

これが一番大きい。このビューのスタイルを直したい、と思った時に class が書いてないのでどこを直しにいけば良いのか分からない。

DevTools であれこれ試す時に同じスタイルが一気に変わらない

class でやれば同じところが変わってくれっる

DevTools からコピペできない

あれこれ試した後「これだ」と思ったらそこからコピペした後、 Elm のコードに直さないといけない。

スタイルを変えるのにコンパイルが必要

Sass とか Post CSS をすでに導入していたら同じ条件なので関係ないが、何かの都合で見た目ををサクッと変えたい時に CSS をいじって解決したくなる。

CSS が進化した

今は変数も使えるしグリッドも使えるし、ブラウザも追いついてきたので相対的に Elm でロジックを書く必要性が減った。スコープももう少ししたら Shadow DOM とか使ってどうにかなるのではないか。

関連アプリと共通化出来ない(仮説)

これは実際に困ったことではなく「そういうことが想定される」というケースなのでまだ仮説段階。例えば隣のアプリは React を使っているが見た目は統一したいとか、徐々に JS から Elm (あるいは逆)に置き換えたいといった場合に、CSS を共通基盤に使うことはあると思う。

引継ぎが厳しい(仮説)

今回は引き継いだ人は普通に出来る開発者だったのでそれほど問題なかったが、もしデザイナーとかだったら厳しかったかもしれない。

というわけで、今は「普通に CSS ファイルで良くね?」に傾いているけど、それでまた問題になったらその時はまた考える。

Promise でリトライや同時実行数の制御をするやつ作った

f:id:jinjor:20170917145732p:plain

JavaScript でバッチ実行を少しでも楽にしたいという思いで作ってみた。

www.npmjs.com github.com

まだバージョン 0.7.0 だけど、大体のことは出来るはず。

機能

デモ を触ると大体何ができるかわかると思います。

  • 実行間隔の指定
  • 同時実行数の制限
  • リトライ数の指定
  • リトライ間隔の調整
  • 失敗したリクエストの返却

動機

DynamoDB への書き込み時に流量を制御しようと色々していて、 Promise が不便だと思った。SDK でもなんか色々パラメータがあると後から知ったのだけど、まぁ Promise が便利になるのに越したことはないということで。もちろん既存のライブラリも沢山探したけど、しっくりくるのが無かった => 作ろう。

API に関しては、シリアライズ可能なデータとしてのリクエストの配列を受け取るようにして、失敗した時に情報を保存したり渡したりできるようにしている。リクエストの数はそんなに多くない想定なので、ストリームで読みつつ書き込むみたいな処理は今の所ない。

今後

やる気が持続すれば付くかもしれない機能。

  • バックオフのもう少し緻密な設定
  • タイムアウトの設定
  • ログ埋め込みの仕組み
  • 入力バリデーションエラー

それでは良い Promise ライフを!

Elm のパイプ |> の良さ

小ネタ。

JavaScript

[1,2,3].map(a => a + 1)

が、 Elm だと

[1,2,3]
  |> List.map (\a -> a + 1)

で、両方とも左から右に読めるからそんなに変わらないなーと思ってたんだけど、一つ違う点に気づいた。JavaScript で Promise を気持ちよく連鎖してて書いてて、いざ並列実行しようとなった時に

const promises =
  [1,2,3].map(a => a + 1).map(toPromise)
Promise.all(promises)

のように少し回りくどくなり、なぜ promises.all() と書けないのかと考えたら「配列にメソッドを追加するのが微妙だから」と気づいた(prototype 拡張で不可能ではない)。一般的に言うと既存の型に何か関数を追加できない。

Elm のパイプを使う場合、その制約はなくて

[1,2,3]
  |> List.map (\a -> a + 1)
  |> Debug.log "converted" -- List に対して Debug モジュールの関数を使う
  |> List.map toString
  |> MyListUtil.getByIndex 1 -- List に対して MyListUtil モジュールの関数を使う
  |> Debug.log "result" -- Maybe に対して Debug モジュールの関数を使う

こうしてどんどん連鎖できる。便利。

と言うのを、 JavaScript と Elm を行ったり来たりしてて気づいた。おわり。

Elm 用の CSV デコーダーを作った

Elm 界隈では構文解析するライブラリを「パーサー(Parser)」、それを Elm の型に落とし込むものを「デコーダー(Decoder)」と呼び分けることが多い。パーサーは既にあって(lovasoa/elm-csv)、デコーダーで良いものがなかったので作った。

CsvDecode - elm-csv-decode 1.0.0

使い方

elm-tools/parser インスパイアのパイプライン式。

-- CSV の各行をこの User 型にしたい
type alias User =
    { id : String
    , name : String
    , age : Int
    , mail : Maybe String
    }


-- デコーダー Decoder User を作る
userDecoder : Decoder User
userDecoder =
    succeed User
        |= field "id"
        |= field "name"
        |= int (field "age")
        |= optional (field "mail")


-- デコードしたい CSV を用意
source : String
source =
    """
id,name,age,mail
1,John Smith,20,john@example.com
2,Jane Smith,19,
"""


-- 実行
> CsvDecode.run userDecoder source
Ok
    [ { id = "1", name = "John Smith", age = 20, mail = Just "john@example.com" }
    , { id = "2", name = "Jane Smith", age = 19, mail = Nothing }
    ]

API 一覧

型を見るだけで使い方が本当に分かるのか実験。

-- Types
type Decoder a
type alias Options = { separator : String, noHeader : Bool }

-- Primitives
succeed : a -> Decoder a
fail : String -> Decoder a
field : String -> Decoder String
index : Int -> Decoder String
fieldsAfter : String -> Decoder (Dict String String)
fieldsBetween : String -> String -> Decoder (Dict String String)

-- Convertion
int : Decoder String -> Decoder Int
float : Decoder String -> Decoder Float
string : Decoder String -> Decoder String
optional : Decoder a -> Decoder (Maybe a)

-- Transform
(|=) : Decoder (a -> b) -> Decoder a -> Decoder b
map : (a -> b) -> Decoder a -> Decoder b
andThen : (a -> Decoder b) -> Decoder a -> Decoder b

-- Run
run : Decoder a -> String -> Result String (List a)
runWithOptions : Options -> Decoder a -> String -> Result String (List a)
runAll : Decoder a -> String -> (List a, List String)
runAllWithOptions : Options -> Decoder a -> String -> (List a, List String)
defaultOptions : Options

分からないと思うのでドキュメント読んでください。

苦労した点

空文字の扱い。要するに foo,,bar の2列目。特に optional を使った時に、数値だと Nothing なのに文字列だと Just "" になるのは微妙なので、デフォルトで空文字は null 扱いにして string と指定した場合のみ値が存在することにした。

ソース

割ときれいに書けたかもしれない。

elm-test は最近のアップデートでトップレベルに Test 型の関数を置くだけで勝手に実行してくれるようになった。便利。

Chrome 拡張機能「JSON-YAML Toggle」を作った

f:id:jinjor:20170722154842p:plain

chrome.google.com

これは何?

JSONYAML をページ上で切り替えるツール。最近 AWS を触っていて、設定ファイルを JSON でも YAML でも書けるんだけどサンプルがどっちか片方しかなく、コピペに不便だった。

使い方は、コンテテクストメニューで「Toggle JSON/YAML」を選ぶだけ。

f:id:jinjor:20170722154953g:plain

ソースはこちら。

github.com

似たものを作りたい方へ

「右クリックしたら画面を操作する」Chrome 拡張を作る場合、上のリポジトリを参考にすると楽。

  1. パッケージとして公開にするには ZIP 化が必要なので、package/ をそのフォルダにする。
  2. manifest.json に必要事項を書く。必要なファイルとか実行条件とか。
  3. context menu 登録用の background.js と、各ページで動作する content-script.js を用意。右クリックされたら前者から後者にメッセージが送られる。DOM は後者からのみ操作できる。
  4. アイコン用画像を3種類ほど用意する。
  5. package を zip 化して Chrome ストアにアップロードして、概要など必要事項を入力する。宣伝用の画像とスクリーンショットが追加で必要。確定すると数分後に公開される。

今回は webpack で js-yaml モジュールと一緒にバンドルしたが、リンクで読み込むのでもいけるような気がしないでもない。

過去に作ったもの

実は似たものを過去にも作っていて、こちらは「日英分割」という日本語と英語の間にスペースを挿入する Chrome 拡張。使い方は上のと同じ。

f:id:jinjor:20170722161901p:plain

chrome.google.com

github.com

JSON-YAML Toggle はここからコピーして作ったので、ほとんど調べずに2日で作れた。

Elm を既存の JavaScript と併せて使う N の方法

今月、社外向けに1回、社内向けに1回 Elm の紹介をしたところ、両方とも好評で「ぜひ使ってみたい」という声が多く、良かったと思う一方で、ここは補足すべきだったなーという点があったので書きます。

両者に共通して頻出だったのが、

  • 既存プロジェクトの一部に導入するにはどうすればいいか
  • JavaScript のライブラリは使えるのか

という質問。 Elm の紹介としてはメインの話題じゃないんだけど、まぁ実際に導入しようと思ったら確かにそこ一番気になると思う。

Elm の初期化方法

まず、Elm の仕組みから見ていくと、例えば Main モジュールをエントリポイントとした場合、JavaScript 側で最も簡単に初期化する方法は次の通り。

<script src="/assets/elm.js">
<script>
Elm.Main.fullscreen();
</script>

fullscreen() を使うと、Elm で書いた画面がウインドウいっぱいに表示される。さらに、Elm の main 関数はフラグ(引数)を受け取ることができるので、設定の類を最初に突っ込むと良い。

<script>
Elm.Main.fullscreen({
  apiHost: 'https://hogehogeapi.com'
});
</script>

次に、画面の一部に Elm の画面を埋め込むには、次の例のように embed() を使う。

<div id="target"></div>
<script>
var elemenet = document.getElementById('target');
Elm.Main.embed(elemenet);
</script>

ちなみに、ここまでの例では HTML に直接ロジックを書いているが、最近のフロントエンド開発では Node.js で開発することが多い。Elm が吐き出すコードはそのまま Node.js のモジュールとして読み込める

var Elm = require('./elm.js');

Elm.Main.fullscreen();

さらに、 React コンポーネントとして Elm を利用することもできる。公式サポートではないが数十行のコードでそういう仕組みが作れる。具体的な話は公式記事の How to Use Elm at Work に書かれている。

JavaScript のライブラリを使いたい

Elm から JavaScript を呼ぶ(あるいはその逆)をするために Port という仕組みがある。これを使うと、JavaScript の世界と Elm の世界はお互いにデータを通信し合うことができる。

ここで注意すべき点は、 JavaScript は Elm の下位レイヤーではなく外部サービスになる ということ。Elm の世界から見た JavaScript の世界はサーバーだと思えばいい。これを公式ドキュメントではJavaScript as a Service」と呼んでいる。

例えば、 JavaScript 側にスペルチェックのロジックが実装されていて、 Elm 側からそれを使いたいとする。その場合、Elm 側から「スペルチェックをしてくれ」というリクエストを投げる。すると JavaScript は処理結果を Elm 側に返す。コードにするとこう。

var app = Elm.Spelling.fullscreen();

app.ports.check.subscribe(function(word) {// Elm 側からデータを受け取る
   var suggestions = spellCheck(word);
   app.ports.suggestions.send(suggestions);// Elm 側に結果を返す
});

制約としては、Elm 側のコードは非同期処理の書き方になる。この辺りは無理してハックするよりも、そういうものだと受け入れてそれ前提で設計を組むのが良いと思う。クラウドの画像処理エンジンを利用するのと同じ。テキストエディタとか(まだ Elm のライブラリがない)も無理せずに JavaScript のライブラリを使って Port でやりとりする。

Elm の世界と JavaScript の世界は隔離されていて、 Port 以外の方法でやりとりすることはできない。言い換えると、Elm が JavaScript のライブラリを「ラップ」することは出来ない。もし、ラップが可能であるとすると Elm の特徴である「実行時例外が発生しない」という保証を守ることができない。ユーザーからしたら見た目 Elm の関数だから、実は中で JavaScript がモリモリ使われていてクラッシュするという話だと疑心暗鬼になってしまう。だからそれは出来ないようになっている。(Elm の外側で起きたらそれは知らない、500エラーのようなもの)

同様に、Elm のエコシステム(外部ライブラリ)も JavaScript ライブラリをラップしているものは一切なく、全て Elm で書かれている。JavaScript をラップしたライブラリを公開することはシステムに禁止されている。だからインストールして使ってみたらクラッシュするという心配はない。

CSS はどうするの?

Elm 固有の特別な仕組みはない。普通に class をつけて 外部の CSS ファイルでスタイリングすれば良いと思う。

他の方法として、このブログでは一度 CSS in Elm (CSS in JS の Elm 版、要はインラインスタイル)という方法を紹介していてモジュール化の観点からは良いのだが、開発ツールとか色々考えていくと総合的なバランスは悪いので、一般的用途にはあまりおすすめできない。elm-css という有名なライブラリもあるのだが個人的にはちょっとやり過ぎ感を感じていて保留中。あとは CSS Modules の Elm 版を作る動きもあったが、今どうなってるか知らない。

とまあ、高度なことをしようとすると色々事情はあるものの、普通の CSS を普通に書くぶんには何も困らない。

失敗しない Elm の導入方法

良いツールがあったとしても、上手くいくかどうかは最終的にチームワークとか組織論とかに行き着いてしまう。社内向けには「ハイリスク・ハイリターン」と表現した。JavaScript に比べたら全然情報がないので、スタート地点でリスク高いのは否定できない事実で、そこからどう工夫してリスクを下げていくかが戦略になる。

その辺の話も公式記事の How to Use Elm at Work に書かれている。この記事にかかれているのは、 Elm を導入して成功した事例の共通点をまとめたものだ。短い記事だから興味のある人は全部読んでほしいのだが、まとめるとこう。

  • 小さい部分から実験しながら徐々に導入する
  • チームに必ず一人めっちゃ詳しい人がいる状態にする
  • 最初のプロジェクトは一番メリットの出る場所を戦略的に選ぶ
  • 議論も良いがとにかく Elm のコードを実際に書いてみる

本文にも書いてある通り 「リスクを最小化する」 のがポイント(というか、これ全然 Elm に限った話じゃないと思う)。

まとめ

なんやかんや言って、コードを見るのが一番イメージ湧きませんか?なので連携部分をコードで紹介しました。参考になれば幸いです。

追伸: Elm 本は 0.19 が出たタイミング(時期未定)で校正入れるので、もう少しお待ち下さい。

Idris で簡単なゲームを作ってみた

3日ほど前に、↓の記事を読んで「最近 Idris 熱いのかー」と思ったので入門してみた。

takezoe.hatenablog.com

実は以前から Elm コミュニティのエッジな人が Idris すごいと言ってて気になっていて、ちょうどバージョンも最近 1.0 になったばかりというのもあってタイミングとして良いと思った。

出来たのがこれ。

github.com

■ ■ □ ■ ■
■ ■ □ □ ■
□ □ ■ □ ■
■ □ □ □ □
■ ■ ■ □ ■

33
■ ■ □ ■ ■
■ ■ □ □ ■
□ □ ■ ■ ■
■ □ ■ ■ ■
■ ■ ■ ■ ■

12
■ ■ ■ ■ ■
■ □ ■ ■ ■
□ □ □ ■ ■
■ □ ■ ■ ■
■ ■ ■ ■ ■

21
Cleared! Press any key to continue.

ライツアウトを知らない人はググってください。

依存型の旨味を感じたかった

依存型を使うと、値から型を作ったりということができる。例えば「長さ n のベクトル」だったら Vect n のようになる。そうすると、 IndexOutOfBounds 例外を未然に防げる。これが Elm とかだと範囲を超えたら Maybe が返るから例外は出ないんだけど、確かに Maybe を取得した後に即座に分岐して Nothing の方に Debug.crash "ここに来ることはあり得ない" とか書いたりするので、それを防げるのは嬉しいのかもしれない。そういう期待があったのと、それ以外の用途が知りたいという動機があった。

お題にライツアウトを選ぶのは、以前からオセロとか 15 パズルみたいな盤面を使うゲームを使う時に、上で触れた「いちいち Maybe で面倒」問題が発生しやすいのを感じていたから。

実装例(一部)

今回は Lights n という型で長さ n の正方形を表したいので、2次元のベクトルの別名とする。型の別名は次のように書く。

Lights : Nat -> Type
Lights n =
  Vect n (Vect n Bool)

Lights n自然数(Nat 型)の引数 n を取って型を作る関数とみなせるので、 Nat -> Type

続いて2次元のインデックスも型にしておく。長さ n までのインデックスが Fin n なので、それをタプルにする。(Fin は Finite Set の意味でインデックスというよりは有限な集合的な意味らしいのだが、詳しいことはよくわからない。)

Position : Nat -> Type
Position n =
  (Fin n, Fin n)

ライツアウトの場合、押したボタンとその上下左右のボタンが反転するので、全てのインデックスが盤面をはみ出ないようにしたい。次の関数は、指定した分だけ移動した時に成功したら Just (Position n)、失敗したら Nothing を返す関数。

move : Integer -> Integer -> Position n -> Maybe (Position n)
move {n} dx dy (x, y) =
  case (integerToFin (finToInteger x + dx) n, integerToFin (finToInteger y + dy) n) of
    (Just x, Just y) => Just (x, y)
    _ => Nothing

一応出来た…がこれで良いのだろうか。 finToInteger で整数にして integerToFin でインデックスに戻しているところが、すごく負けた感がする。結局そこで整数にするのかよ。ちなみに最初の {n} は、型引数にある n の値をそのまま持ってこれる。なにやらすごく不思議な感じがする。

メインループは Elm アーキテクチャで実装

CUI だけど GUI とみなせば、見慣れた Elm アーキテクチャが使える。

モデルは、タイトル画面とプレイ中の状態のいずれか。

data Model =
  Start | Playing (Lights Main.size)

メッセージは、タイトルからゲーム画面への遷移と盤面を押すアクション。

data Msg
  = NoOp
  | StartGame
  | Toggle (Position Main.size)

アップデート。本当は最初の盤面をランダムにしたかったが、その辺は未学習なので決め打ち。 NoOp とかあり得ないモデルとメッセージの組み合わせがやはり出てきてしまったのが無念。その辺も型でなんとかできることを期待しているのだが。

update : Msg -> Model -> Model
update msg model =
  case (msg, model) of
    (NoOp, _) =>
      model

    (StartGame, _) =>
      Playing $ toggle (position 2 2) (empty size)

    (Toggle position, Playing lights) =>
      Playing $ toggle position lights

    _ =>
      model

ビューは、モデルが与えられた時に画面に表示する情報と入力をどうメッセージに変換するかをタプルで返す。Html msg のように Output msg のような別名を与えてもいいかもしれない。

view : Model -> (String, String -> Msg)
view model =
  case model of
    Start =>
      ("Press any key to start.", \_ => StartGame)

    Playing lights =>
      if isEmpty lights then
        ("Cleared! Press any key to continue.", \_ => StartGame)
      else
        (format lights, decodePosition)

感想

正直、ここまでで依存型の恩恵を肌で感じ取るには至らなかった。「盤面の長さが5なんだけど8とかが来るのを未然に防げてよかったー」みたいな気持ちにならない。null が来ないのを未然に防ぐのに比べて、ある範囲の整数値が来ないことはあまり嬉しくないということなのかなんなのか。あるいは使い方が間違っている?その辺り、 Idris 脳にならないと普通の Haskell を書いてしまうので難しいと思った。

当初の予定としては JavaScript に吐き出して HTML のデモを作ろうと思ったのだが、 FFI に関するドキュメントが古いのか、うまくインストールが出来なかったので諦めた。

コンパイル時のエラーメッセージに関しては、親切にしようという努力は見られるものの結構おかしな事を言ってくるので注意が必要。本当は「変数が見つからない」なのに「型が曖昧」と怒ってきたり、パターンマッチでコンストラクタ名を間違えるとワイルドカードになってコンパイルが通ってしまったり、色々と問題がある。

あと、型システムに関しても微妙に Haskell と違う点があって、Wiki にまとめられている。さらっと書かれてるけど「deriving does not work yet」めっちゃ辛い。

現時点での感想は以上です。