※これはElm中級者向けの小ネタ記事です
setViewportOf
が動かない(嗚咽)
先日、Elmで書かれたアプリケーションで、押すとスクロールするはずのボタンが動かないというバグに遭遇しました。コンソールには何のエラーメッセージも出てません。周辺のコードも特に変更されていません。他の人から引き継いだコードなので、いつから混入したのかもわかりません。ブラウザの仕様変更も疑ったのですが、とくに思い当たることはありません。関連しそうなGitHubのIssueなんかも見つかりません。
なかなか原因特定に戸惑って、Elmのコアライブラリの中身まで調べてようやく原因がわかりました。ElmのsetViewportOf
とCSSのscroll-behavior: smooth;
を併用したときに起こるバグでした。デザイナーの人があとからCSSだけを更新したときに入ったようです。
setViewportOf
は次のような実装になっています。
var _Browser_setViewportOf = F3(function(id, x, y)
{
return _Browser_withNode(id, function(node)
{
node.scrollLeft = x;
node.scrollTop = y;
return __Utils_Tuple0;
});
});
常にnode.scrollLeft
とnode.scrollTop
の両方を同時に設定していることがわかります。これは、scroll-behavior: smooth;
が設定されていなければ問題なく動くのですが、scroll-behavior: smooth;
が設定されると、
-
node.scrollLeft = x;
が実行され、滑らかスクロールアニメーションがスケジュールされる - でも、アニメーションなのでスクロールまだ動かない
- その直後、
node.scrollTop = y;
が実行され、次の新たなアニメーションがスケジュールされる。このとき、直前に1でスケジュールされたアニメーションが、まだまったく進まないままキャンセルされて上書きされてしまう
というわけです1。
そして、ElmのコアライブラリにはscrollLeft
のみを設定する関数は存在しません。Elmではこういう場合は基本的にElm単体では解決できず、JavaScriptを書いてそれをポートから呼び出すしかありません。今回私はsetScrollLeft
というポートを作ってそれを呼び出すことで対応しました2。
謎のslideViewportOf関数
ところで、この問題自体は認識されているようで、実はこの問題の解決策の案?がすでに検討されていることに気付きました。Elmのコアライブラリのソースコードにはこのスムーススクロールの問題を解決するための関数 slideViewportOf
が存在しているのですが、コメントアウトされています(中身はDebug.todo
ですが)。
たしかに、このスムーススクロールの問題を解決するためだけに関数を増やすというのは、あまりに姑息な対応に思えます。Elmはシンプルさが売りなのに、そんなその場しのぎの関数ばかり追加していたらシンプルさが台無しになってしまいます。
もしブラウザの細かい挙動まで細かく制御できるAPIを用意するなら、それは須らくブラウザ本来のAPIの混乱を受け継ぐことになります。しかしElmは、歴史的経緯でグチャグチャになりつづけているブラウザの膨大でAPIを追いかけるのはやめ、よりシンプルで洗練された、Elmにとって使いやすく安全で安定したAPIを用意する道を選びました。slideViewportOf関数を入れないのも、そうした姿勢の現れだと思われます3。
Elmとどう向き合うべきか
Elmでブラウザの細かい動作まで制御するには、結局のところポートを使うしかありません。逆にいえば、ポートを避けるにはポートが必要ないようなアーキテクチャを選択したり、ただのHTMLの描画を超えるような特殊な機能を意図的に省く必要が出てきます。筆者の経験上は、現実のウェブサービス開発ではどうしても様々な思惑や都合からElmのみでは対応しきれない要求が数多く生まれ、少なくない量のJavaScriptを書くとともに多数のポートを追加するはめになることが多いです。これには、「ポートを書けば解決できるんだからそれでいいじゃないか、何か問題が?」と捉える人と、「ポートを使いまくったらElmの良さが半減するじゃないか……」と捉える人の二通りがいるんじゃないかと思います。
Elmは以前は破壊的変更を頻繁に繰り返していましたが、現在は極めて安定した状態にあります。開発が中止したわけではないとのことですが、言語やコアライブラリの仕様はほぼ凍結されたと言えるかも知れません。言語のバージョンアップに追われるだとか、先行きが不透明で不安だということはない一方で、このあたりのポートで解決しないといけない面倒な部分はまだ解決の目処が立たないというか、もはや諦めるしかないという感じです。個人的には、Elmを採用するかどうかの分かれ目は、ポートをどれだけ使うか、ポートの多用をどれだけ受け入れるかにあると思っています。
-
ちなみに、ここでChromeとFirefoxで振る舞いが微妙に異なっていました。Chromeではまったく動かないのに、Firefoxだと
node.scrollLeft = y;
にブレイクポイントを仕掛けてステップ実行すると、node.scrollLeft = x;
だけが適用されてブラウザに制御が戻るらしくスクロールが動きます。 ↩ -
まあ、スクロールがスムースかどうかという演出だけなので、プロジェクトによっては
scroll-behavior: smooth;
のほうを切るという手もありましたが、ぬるっと動くのがデザイナーの希望ならそれを叶えるしかありません。 ↩ -
TypeScriptはブラウザAPIの混沌を受け入れ、それを手懐けるための超複雑な型システムを導入し、とてつもない労力をかけて信じられないほど膨大な型注釈ライブラリを揃えることで成功した言語ですね。DartやPureScriptなどの言語は、ブラウザのAPIを簡単にラップできるFFIによって安全性と自由、使いやすさを両立させています。 ↩