Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

EmacsでScala3のインデントが正しく動かないので調査した

自分はEmacsでScalaを開発している。最近はScala 3がアツいのでEmacsでScala3を書いているのだが、Scala 3固有のシンタックスをうまく認識しないというトラブルが起こり困っている。

具体的には、Scala 3から利用できるOptional Bracesを使っているとき、TABを押下するとインデントが正しく認識されずに、コードが壊れるというものだ。

TL;DR

  • 現行のscala-modeは正しくOptional Braces Syntaxを処理できない(開発中)
  • メリットもそんなにないので普通にブレースを書いたほうがいい
  • どうしても使いたかったら別のエディタを選ぶしかない

Optional Braces

Optional Bracesとは、Scala 3から導入された新たな構文で、特定の場合には中カッコ(ブレース)を書かなくても良いというものだ:

class Foo:
    def bar = for
    x <- 1 to 9  
    y <- 1 to 9
    yield x * y

new Foo().bar

これにより、比較的簡潔にクラスやメソッドを書くことができるようになるし、書き味が若干PythonやRubyに近付く(従来通りにブレースを使った記法もできるので、完全に好みで選ぶことができる)。

インデントがうまくいかない

さて、自分はEmacs + lsp-modeを使ってScalaを書いている。EmacsはLSPを経由してMetals(ScalaのLSPサーバ)を呼び出し、これをもとにシンタックスハイライトや補完を行っている。

デフォルトではタブキーを押下したとき、以下のような挙動になる:

  • その行のインデントを適切な位置に調整する
  • 必要に応じて補完も行う

しかし、このOptional Bracesを使っていると、TABキーを押下したとき、勝手にその行のインデントレベルが0、つまり先頭のスペースが全て削除されてしまい、当然コンパイルも通らなくなる。

これでは困るので、今回はこの問題の原因調査と解決に向けた調査を行うことにした。

TABを押下したときに何が起こるか

ある操作をしているのにおかしなことが発生するときの調査でまず行うのは、describe-key関数を実行してキーに紐付いている関数を調べることだ。M-x describe-keyしてからTABを押下する。

すると自分の環境ではcompany-indent-or-complete-commonを呼び出していることがわかった。(Companyとは補完用の候補を表示するためのUIエンジン。) Companyのインデント機能がうまくLSPと連携できずにインデントがおかしくなっているのではないかと思い、Companyのインデントがどのように行なわれているかを調べることにした。

Companyのインデント実装

前述したcompany-indent-or-complete-commonの内部では、インデントのためにindent-for-tab-commandが呼ばれていた。これはTABキーを押下した際に標準的にEmacsが呼び出す標準的な関数だ。

まず、LSPはこのあたりに手を入れてはいなそうなので、既に入っているscala-modeがobsoleteになっているのではないかと仮説を立て、いったんscala-modeを入れ直すことにした。しかし、現在のバージョンは0.23だったが入れ直しても変化はなかった。つまりscala-modeが仮に原因だったとしてもこれでは直らないことがわかった。また、scala-mode.elには"A programming mode for the Scala language 2 and 3"とあるので、Scala 3の文法に対応していないはずはなさそうに思える。

次に、既にコンパイルされているscala-mode.elcファイルが古いままになっているのではないかと仮説を立てた。これを削除してうまく動作するならこれが原因だったことになる。がしかし.elcファイルを削除しても動作は同じだった。

次に、indent-for-tab-commandscala-modeをうまく呼び出せていないのではないかと仮説を立てた。scala-modeが提供するインデント用のコマンドを直接実行して正常にインデントができれば、この仮説が正しいことになるはずだ。scala-modeが提供しているインデント用関数はscala-indent:indent-lineなので、これをevalして直接実行してみる。しかしこれでも動作は同じだった。

Scala-modeのインデント実装

そうこうしているうちに https://github.com/hvesalai/emacs-scala-mode/issues/160 を発見した。去年あたりからScala 3対応は議論されているものの、 https://github.com/hvesalai/emacs-scala-mode/pull/170/files で今開発が進んでいるという状況だった。

これでは手出しができないので、諦めてオフサイドルールを使わずに従来通りブレースを書くことにした。

オフサイドルールどうなの

という調査結果をScalaを書いている同僚に共有したところ、あまりオフサイドルールに良い印象は持っていないようだった。昔からScalaを書いているユーザで、なおかつOptional Braces Syntaxによるオフサイドルールが大好き、という人はあまりいないのではないか。

オフサイドルールは一見記述が簡潔に見えるというメリットがあるが、どこが開始で終了なのかパースしにくいという問題を抱え込んでしまう。コピペしたときなどに適切なインデント位置を自動的に推論できない(実際、scala-modeではそれが難しくてインデントの実装に手間取っている)。YAMLやPythonのコードをペーストしたときにインデント位置が合わずに何度もTABキーを押下した経験は誰しも持ち合わせているはずだ。ちょっと見掛けの複雑さが増えるが、ブレースがある構文のほうがずっと扱いやすいのではないかと思う(なので自分はどちらかといえばYAMLよりJSONが好き)。

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?