Chrome
Elm
Stream
ElmDay 19

ElmのStreamライブラリで画面を作るなら

ElmのStreamライブラリを試した

Elm Advent Calender 19日目です。

Elmのアドカレは活発ですね!

去年のelm Advent Calenderではlm-native-uiでiosアプリ作ってました -> 記事

stream

  • github
  • Elm package
  • elmにはlazy listを扱う標準ライブラリがないから作ったぜ
  • 他にも似たような機能を実現するライブラリはあるけどstack over flowに対して脆弱だ
  • stack over flowしない lazy listライブラリだぜ
  • ほかにもStreamライブラリはたくさんありますが、これのみ試します

どんな時に使えるだろうか?

  • ドキュメントにもある通り無限に流れるデータに対してどうこうする時につかえそうだ
  • elmがクライアントアプリケーションを作るのに使われるものだったら無限スクロールに表示されるようなデータだろうか? タイムライン表示のようなリアルタイムでバシバシデータが流れて来るもの?

簡単な使い方

  • 一つの要素なStream
singleHoge : Stream String
singleHoge = Stream.singleton "hoge"
  • リストから生成されるStream
streamFromList : Stream String
streamFromList = Stream.fromList ["hogo", "huga", "hugi"]
  • Stream同士を結合
concatStream : Stream String
concatStream = Stream.concat singleHoge streamFromList
  • Streamの先頭から要素を1つと要素を抜いたStreamを取る
elementAndTail : (String, Stream String)
elementAndTail = Stream.next concatStream
-- -> ("hoge", ["hogo", "huga", "hugi"])
  • StreamをListに変換
listFromStream : List String
listFromStream = Stream.toList concatStream
-- -> ["hoge", "hogo", "huga", "hugi"]
  • その他
    • map, filter, reduce, zip... Listにある関数と同じようなものたち

タイムライン的なものを作ってみて検証

  • サーバサイドから無限にデータが流れて来る(websocketで流しまくるよ)
  • 表示するのは最新の10件

Listで作って見る

type alias Feed = {
 id: Int,
 subject: String,
 text: String
}

type alias Model = {
 feeds: List Feed
}

type Msg =
 ReceiveFeed Feed

update msg model =
    case msg of
        ReceiveFeed feed ->
            ( { feeds = feed :: model.feeds }, Cmd.none )


feedView feed = div [] [ text feed.subject ]

feedList = List.map feedView >> List.take 10

view model = div [] (model.feeds |> feedList)

port receiveFeedPort : (Feed -> msg) -> Sub msg

init = ({ feeds = [] }, Cmd.none)

main = 
    Html.program
        { view = view
        , init = init
        , update = update
        , subscriptions = always <| receiveFeedPort ReceiveFeed
        }

Streamで作って見る(変更箇所のみ記載)

type alias Model = {
 feeds: Stream Feed
}

update msg model =
    case msg of
        ReceiveFeed feed ->
            ( { feeds = Stream.concat (Stream.singleton feed) model.feeds }, Cmd.none )

init = ({ feeds = Stream.fromList [] }, Cmd.none)


feedList = Stream.map feedView >> Stream.nextN 10 >> Tuple.second

動かしてみた

↓こんな感じ

pic.twitter.com/f5pC44UHfb

— ス (@4245Ryomt) 2017年12月19日

List version

  • 300000レコード程度流し込むと画面の更新がガクガクしだす
  • Perfomanceツールから状況を見るとheapをものすごい勢いで消費している(消費600MBまでアガる)
  • その影響かminor gcが頻発し時間が取られて画面がガクガクになっているようだ(?)
  • スクリーンショット 2017-12-19 13.06.32.png

Stream version

  • 300000レコード流しこんでも描画がガクガクになっていない
  • heapの消費を見ても100MB程度で収まっている
  • minor gc に時間がとられていないようだ
  • スクリーンショット 2017-12-19 13.05.45.png

グラフ見てて動き全然ちげえな。。。

感想

  • 適当なサンプルでの比較で、これでいいのか‥?感あるがStreamを使ってメリットを感じることができた。
  • なぜStreamだとheapを消費しないのか?
    • mapとかfilterとか読んでもすぐ新しいインスタンスにならないから?Array無駄に生成されないから?
  • どうしてこのStreamだとイイ感じなのか、説明のつくように精進いたします。