Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

elm-upgradeに従ってElmのバージョンを0.18から0.19へ上げる

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に渡すレコードの中に、onUrlRequestonUrlChangeがあります。名前からしてこれらを使えば良さそうです。それぞれ説明を読んでみます。

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.UrlRequestUrlChange 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.regexRegex.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のフレームワークとして改善されていると感じました。

参考記事