ForSchool事業部の石上です。ウェブのフロントエンドを中心にStudyplus for Schoolの開発に携わっています。
あるアプリケーションのElmのバージョンを0.18から0.19に上げる対応をしました。今回はこのことについて書きます。
背景
Studyplus for School で新たにちょっとしたサブシステムが必要になり、その小さなSPAのためのウェブフロントエンドの言語として今回、Elmを採用しました。
弊社のプロダクトのほとんどは、サーバーサイドはRuby on Railsで作られており、ウェブフロントエンドはJavaScriptかTypeScriptです。Elmでウェブのフロントエンドを書くことはけっこう挑戦的でしたが、会社としても他の言語に手を伸ばしていきたいという話もあり、CTOとチームリーダーから許しをいただきElmでの実装に至りました。
ある程度実装が終わった段階でElm 0.19のリリースが発表され、これまでのElmの仕様から大きく変更がありました。主要パッケージの移動、関数の仕様変更の数々...。しかしこれはある程度覚悟していたことです。今回実装したものがまだリリース前かつ小規模なアプリケーションだったので、1日くらいガッとやれば対応できるだろうということで、対応しました。
対応手順
- Elmのバージョンを上げる
- elm-upgradeを実行
- elm-upgradeで自動修正されない部分を手で書き換え
Elmのバージョンを上げる
yarn upgrade elm@0.19-bugfix2
elm-upgradeを実行
elm-upgradeというツールがあるので、それを使います。
npx elm-upgrade
elm-upgradeを実行すると、修正の方針について何点か質問されるのでそれに答えながら修正を実行していきます。
INFO: Found elm at node_modules/.bin/elm INFO: Found elm 0.19.0 INFO: Found elm-format at node_modules/.bin/elm-format INFO: Found elm-format 0.8.1 INFO: Cleaning ./elm-stuff before upgrading INFO: Converting elm-package.json -> elm.json INFO: Detected an application project (this project has no exposed modules) INFO: Installing latest version of elm-community/list-extra Here is my plan: Add: elm-community/list-extra 8.1.0 Would you like me to update your elm.json accordingly? [Y/n]:
- elm-stuffを消す
- elm-package.jsonからelm.jsonへ移行する
- 最新のelm-community/list-extraをインストールする
と言われています。
Elm 0.19からはパッケージの依存関係を記録するファイルが elm-package.json
から elm.json
になっています。
インストールしたモジュールのコードはelm-stuff
から ~/.elm
になりました。
elm-upgrade
がこの移行をやってくれます。パッケージを入れ直そうとしているので、そのための質問が続きます。全部Yesと答えました。
完了すると、以下のメッセージが表示されます。
SUCCESS! Your project's dependencies and code have been upgraded. However, your project may not yet compile due to API changes in your dependencies. See <https://github.com/elm/compiler/blob/master/upgrade-docs/0.19.md> and the documentation for your dependencies for more information. Here are some common upgrade steps that you will need to do manually: - elm/core - [ ] Replace uses of toString with String.fromInt, String.fromFloat, or Debug.toString as appropriate - undefined - [ ] Read the new documentation here: https://package.elm-lang.org/packages/elm/time/latest/ - [ ] Replace uses of Date and Time with Time.Posix - elm/html - [ ] If you used Html.program*, install elm/browser and switch to Browser.element or Browser.document - [ ] If you used Html.beginnerProgram, install elm/browser and switch Browser.sandbox - elm/browser - [ ] Change code using Navigation.program* to use Browser.application - [ ] Use the Browser.Key passed to your init function in any calls to Browser.Navigation.pushUrl/replaceUrl/back/forward - elm/url - [ ] Changes uses of Navigation.Location to Url.Url - [ ] Change code using UrlParser.* to use Url.Parser.*
elm-upgradeで自動修正されない部分を手で書き換え
上記のメッセージの通り、以降はソースを手で直していきます。親切にリストになっているので、これを上から潰していけばいいでしょう。
- elm/core - [ ] Replace uses of toString with String.fromInt, String.fromFloat, or Debug.toString as appropriate - undefined - [ ] Read the new documentation here: https://package.elm-lang.org/packages/elm/time/latest/ - [ ] Replace uses of Date and Time with Time.Posix - elm/html - [ ] If you used Html.program*, install elm/browser and switch to Browser.element or Browser.document - [ ] If you used Html.beginnerProgram, install elm/browser and switch Browser.sandbox - elm/browser - [ ] Change code using Navigation.program* to use Browser.application - [ ] Use the Browser.Key passed to your init function in any calls to Browser.Navigation.pushUrl/replaceUrl/back/forward - elm/url - [ ] Changes uses of Navigation.Location to Url.Url - [ ] Change code using UrlParser.* to use Url.Parser.*
Replace uses of toString with String.fromInt, String.fromFloat, or Debug.toString as appropriate
2件ありました。以下のように修正しました。
- toString 1000 + String.fromInt 1000
Read the new documentation here: https://package.elm-lang.org/packages/elm/time/latest/
https://package.elm-lang.org/packages/elm/time/latest/ を読むと、Elm 0.19においての時間の取扱について書かれています。Daylight Saving Timeの話を出したりしつつ、人間用の時刻をモデルやデータベースに持たせるんじゃない! ということが書かれています。データとしてはPOSIXタイムとタイムゾーンとして扱い、人間用の表現はモデルではなくビューの関数の中でやるんだぞというようなことが書かれています。なるほど。
Replace uses of Date and Time with Time.Posix
0.19ではそのような考えがモジュールに反映されています。これまでの0.18ソースで使っていたDateとTimeをTime.Posixに置き換える必要があります。
Time.every
しか使っていなかったので特に変更は必要ありませんでした。
If you used Html.program*, install elm/browser and switch to Browser.element or Browser.document
使っていないので関係ありませんでした。
If you used Html.beginnerProgram, install elm/browser and switch Browser.sandbox
使っていないので関係ありませんでした。
Change code using Navigation.program* to use Browser.application
今回このアプリケーションはSPAとして実装、つまりページを読み込み直さずに状態とURLを書き換えてページ遷移を行いたいと考えていました。このフロントエンド側でのルーティングの機能は、Elm以外の言語・ライブラリでもたいてい何かしらの形で提供されています(ReactならReact Routerなど)。
Elm 0.18では「クリック時のデフォルト挙動を無効化しつつ、引数でメッセージを渡して副作用を起こす」みたいなヘルパーを書いてこれを実現していました。 こういう感じです。
Elm 0.19ではこんなことをしないでもこの機能を実装することができるようになったようです。
Browser.applicationを見ると、関数シグネチャはこうなっています。
application : { init : flags -> Url -> Key -> ( model, Cmd msg ) , view : model -> Document msg , update : msg -> model -> ( model, Cmd msg ) , subscriptions : model -> Sub msg , onUrlRequest : UrlRequest -> msg , onUrlChange : Url -> msg } -> Program flags model msg
application
に渡すレコードの中に、onUrlRequest
とonUrlChange
があります。名前からしてこれらを使えば良さそうです。それぞれ説明を読んでみます。
onUrlRequest
When someone clicks a link, like
<a href="/home">Home</a>
, it always goes through onUrlRequest. The resulting message goes to your update function, giving you a chance to save scroll position or persist data before changing the URL yourself with pushUrl or load. More info on this in the UrlRequest docs!
リンクのクリック時にデフォルトでonUrlRequest
メッセージが発行されるとのこと。ありがたい!
onUrlChange
When the URL changes, the new Url goes through onUrlChange. The resulting message goes to update where you can decide what to show next.
↑URLが変更されるとonUrlChange
になるとのこと。
UrlRequest Browser.UrlRequest
、UrlChange Url.Url
というメッセージを用意してあるとすると、こういう感じになります。
Browser.application { view = view , init = init , update = update , subscriptions = subscriptions , onUrlRequest = UrlRequest , onUrlChange = UrlChange }
update msg model = UrlRequest urlRequest -> case urlRequest of Browser.Internal url -> ( model, Navigation.pushUrl model.nav.key (Url.toString url) ) Browser.External url -> ( model, Navigation.load url ) UrlChange url -> ({ model | url = url }, Cmd.none)
Use the Browser.Key passed to your init function in any calls to Browser.Navigation.pushUrl/replaceUrl/back/forward
Elm 0.18でページ遷移したいときは、Navigation.newUrl "/hoge"
みたいな形でできました。0.19のBrowser.Navigation
にはnewUrl
は無く、見てみるとpushUrl
を使えば良いようです。
しかし遷移先の文字列をただ渡すのではなく、Browser.Keyなるものを渡す必要があるみたいです。
pushUrl : Key -> String -> Cmd msg
このKey
は外から書き換えたり出来ない値で、initで入ってくるものをModelに持っておいて、pushUrl などURL変更の関数に渡して使います。
You only get access to a Key when you create your program with Browser.application, guaranteeing that your program is equipped to detect these URL changes. If Key values were available in other kinds of programs, unsuspecting programmers would be sure to run into some annoying bugs and learn a bunch of techniques the hard way!
Changes uses of Navigation.Location to Url.Url
言われている通り修正しました。
Change code using UrlParser. to use Url.Parser.
言われている通り修正しました。
その他
elm installできない?
Elmのモジュールの依存関係はnpmなどと同様、1つのJSONファイルに記録されます。しかし npm install
で全部ダウンロードというようなことはできません。elm 0.19で elm install
を実行すると以下のメッセージが表示されます。
~/project (feature/update-elm-to-0.19 *)$ elm install -- INSTALL WHAT? --------------------------------------------------------------- I am expecting commands like: elm install elm/http elm install elm/json elm install elm/random Hint: In JavaScript folks run `npm install` to start projects. "Gotta download everything!" But why download packages again and again? Instead, Elm caches packages in /Users/ishigami/.elm so each one is downloaded and built ONCE on your machine. Elm projects check that cache before trying the internet. This reduces build times, reduces server costs, and makes it easier to work offline. As a result elm install is only for adding dependencies to elm.json, whereas elm make is in charge of gathering dependencies and building everything. So maybe try elm make instead?
パッケージは./elm-stuff
ではなく、 ~/.elm
にキャッシュされます。
Variable Shadowing
こんなエラーも出てきました。
./src/Main.elm [========================= ] - 1 / 2-- SHADOWING --------------------------------------------- src/Views/Hoge.elm The name `hoge` is first defined here: 23| viewHogeName hoge = ^^^^^^^ But then it is defined AGAIN over here: 25| Just hoge -> ^^^^^^^ Think of a more helpful name for one of them and you should be all set! Note: Linters advise against shadowing, so Elm makes “best practices” the default. Read <https://elm-lang.org/0.19.0/shadowing> for more details on this choice. Detected errors in 1 module.
怒られている通り、こう直します。
viewHogeName maybeHoge = case maybeHoge of Just hoge -> hoge.name Nothing -> ""
エラーメッセージの通り、なんでこう直さないと怒られるんだというのは以下に書かれています。
Regex
I cannot find a `Regex.regex` variable: 330| Regex.regex "hogehoge" ^^^^^^^^^^^ The `Regex` module does not expose a `regex` variable. These names seem close though: Regex.never Regex.find Regex.replace Regex.split Hint: Read <https://elm-lang.org/0.19.0/imports> to see how `import` declarations work in Elm.
Regexも変わっていました。今回エラーが出た Regex.regex
は Regex.fromString
になったようです。以下のような変更が必要でした。
removeHogehoge hogehoge = let regex = - Regex.regex "hogehoge" + Regex.fromString "hogehoge" in - Regex.replace Regex.All regex (\{ match } -> "") hogehoge + Regex.replace regex (\{ match } -> "") hogehoge
さらにその他
非標準ライブラリにもElm 0.19で変わったものがあり、その対応も必要でした。
所感
変更範囲はかなり大きいので、大きめのアプリケーションを0.19対応するのは相当大変そうです。
しかし、elm-upgradeが最初にやることリストを出してくれたりコンパイルエラーにここ読めリンクが適切に貼られていたりと、とても親切に感じました。
変更点についても、Browser.application
で簡単にSPAを作れるようになっていたりして、SPAのフレームワークとして改善されていると感じました。