ここ数年でインデントベースの記述は広くプログラマ界隈で受け入れられるようになってきました。プログラミング言語ではPythonの成功が大きく、ドキュメントではmarkdownとyamlが広く普及しています。そしてScala 3でもとうとうその波に乗ろうという動きが見えてきました・・・
(本記事は自分のブログからの転載記事です。)
(2019年9月28日追記・更新: 追記内容はここを見てください)
(2019年11月16日追記・更新: 追記内容はここを見てください)
(2021年3月7日追記・更新: 追記内容はここを見てください)
TL;DR
- Scala 3のリサーチコンパイラであるDotty 0.18.1-RC1にインデントベースの構文が実装されました
- Dotty 0.19.0-RC1の変更に合わせて修正しました
- インデントベースの構文はまだ提案段階でありScala3の正式な仕様に決定したわけではありません
- 今後機能が変化したり、機能が採用されなかったりする可能性も十分あります
- というか反対意見の方が多いです
- 従来の括弧ベースの構文も混ぜて使えます
- この記事はインデントベース構文の紹介記事です。この構文の良し悪しについては触れていません
まずはコードを御覧ください
ウソみたいだろ・・・Scalaなんだぜ、それ
object OptionalBraceExample:
enum Day:
case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
def isWeekend: Boolean = this match
case Saturday | Sunday => true
case _ => false
def fromString(str: String): Day =
try Day.valueOf(str)
catch
case _: IllegalArgumentException =>
throw new IllegalArgumentException(s"$str is not a valid day")
end try
end fromString
trait A:
def f: Int
class B:
def g: Int = 27
class C(x: Int) extends B with A:
def f = x
type T = A:
def f: Int
def use(dayString: String) =
println("\n--- start OptionalBraceExample ---")
val day = fromString(dayString)
// `then` is optional at line end
if day.isWeekend then
println("Today is a weekend")
println("I will rest")
else
println("Today is a workday")
println("I will work")
if (day == Day.Wednesday)
println("Today is a Wednesday")
println("Bad Day")
val optNum =
for
x <- Option(3)
y <- Option(2)
yield
x + y
optNum match
case Some(x) if x > 4 => println("bigger than 4")
case _ => println("Other")
println(s"B().g is ${B().g}.")
// required `-Yindent-colons`
val z = List(2, 3, 4) map:
x =>
val y = x - 1
y * y
z.foreach:
println
object BraceBasedExample {
enum Day {
case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
def isWeekend: Boolean = this match {
case Saturday | Sunday => true
case _ => false
}
}
def fromString(str: String): Day = {
try {
Day.valueOf(str)
} catch {
case _: IllegalArgumentException =>
throw new IllegalArgumentException(s"$str is not a valid day")
}
}
trait A {
def f: Int
}
class B {
def g: Int = 27
}
class C(x: Int) extends B with A {
def f = x
}
type T = A {
def f: Int
}
def use(dayString: String) = {
println("\n--- start BraceBasedExample ---")
val day = fromString(dayString)
if (day.isWeekend) {
println("Today is a weekend")
println("I will rest")
} else {
println("Today is a workday")
println("I will work")
}
if (day == Day.Wednesday) {
println("Today is a Wednesday")
println("Bad Day")
}
println(s"B().g is ${B().g}.")
val optNum =
for {
x <- Option(3)
y <- Option(2)
} yield {
x + y
}
optNum match {
case Some(x) if x > 4 => println("bigger than 4")
case _ => println("Other")
}
val z = List(2, 3, 4) map {
x => {
val y = x - 1
y * y
}
}
z.foreach(println)
}
}
いつもはこんなコードを書いていたはず・・・
従来のコード
object BraceBasedExample {
enum Day {
case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
def isWeekend: Boolean = this match {
case Saturday | Sunday => true
case _ => false
}
}
def fromString(str: String): Day = {
try {
Day.valueOf(str)
} catch {
case _: IllegalArgumentException =>
throw new IllegalArgumentException(s"$str is not a valid day")
}
}
trait A {
def f: Int
}
class B {
def g: Int = 27
}
class C(x: Int) extends B with A {
def f = x
}
type T = A {
def f: Int
}
def use(dayString: String) = {
val day = fromString(dayString)
if (day.isWeekend) {
println("Today is a weekend")
println("I will rest")
} else {
println("Today is a workday")
println("I will work")
}
if (day == Day.Wednesday) {
println("Today is a Wednesday")
println("Bad Day")
}
println(s"B().g is ${B().g}.")
val optNum =
for {
x <- Option(3)
y <- Option(2)
} yield {
x + y
}
optNum match {
case Some(x) if x > 4 => println("bigger than 4")
case _ => println("Other")
}
val z = List(2, 3, 4) map {
x => {
val y = x - 1
y * y
}
}
z.foreach(println)
}
}
@main def example: Unit = {
IndentBaseExample.use("Monday")
}
コードの解説
Pythonでインデントを用いてブロックを作る場合は改行の前に「:」が付きますが、Scalaの場合は少しルールが複雑です。
まず前提として、インデント構文と中括弧の構文は併用可能です。つまり、Scala2のコードを無理にインデント構文に書き換える必要はないです。
また、インデントにははタブとスペースの両方が使えますが混ぜると比較ができないケースがあるので、混ぜるな危険です。
テンプレート本体
Scalaの文法ではクラス、トレイト、オブジェクトの定義に「テンプレート本体(Template Body)」という用語を使用しています。そしてテンプレート本体
でインデント構文を使う場合はclass
やobject
などのキーワードが含まれる行末をを「:」にします。
object IndentBasedExample:
enum Day:
case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
def isWeekend: Boolean = this match
case Saturday | Sunday => true
case _ => false
ちなみにenum
はScala 3(Dotty)の機能でScala 2にはありません。気になる方は「Enumerations」を参照してください。
if
式
if
式の場合は、インデントブロック開始の合図が「then
」になります。最初then
を書き忘れたらコンパイラにthen
がないって怒られて、then
ってなんぞや???となりました。どうやら新しいキーワードみたいです。
if day.isWeekend then
println("Today is a weekend")
println("I will rest")
else
println("Today is a workday")
println("I will work")
if
式で括弧を用いる場合はthen
は必要ありません。
if (day == Day.Wednesday)
println("Today is a Wednesday")
println("Bad Day")
for
式
以下のコードは=
とfor
とyield
がインデント構文の開始の合図になっています。
val optNum =
for
x <- Option(3)
y <- Option(2)
yield
x + y
match
式
match
式は少し特殊でインデントを付けても付けなくてもいいことになっています。つまり以下の2つの書き方が許容されます。
optNum match
case Some(x) if x > 4 => println("bigger than 4")
case _ => println("Other")
optNum match
case Some(x) if x > 4 => println("bigger than 4")
case _ => println("Other")
match
以外にはcatch
も同様に次のcase
のインデントは自由です。
ラムダ式
ラムダ式の記号(=>
)もインデント構文の開始の合図になっています。
x =>
y = y - 1
y * y
エンドマーカー
インデントだとブロックの終わりがわかりにくい場合には、エンドマーカーを使ってブロックの終わりを明示することができます。以下のコードでは「end fromString
」が識別子のエンドマーカーです。エンドマーカーはオプションなのでなくても問題なく動作しますが、ドキュメントでは空白行が含まれていたり、ひと目でブロックを識別することが難しい長いブロック(15〜20行以上)で使うことを推奨しています。
def fromString(str: String): Day =
try Day.valueOf(str)
catch
case _: IllegalArgumentException =>
throw new IllegalArgumentException(s"$str is not a valid day")
end try
end fromString
エンドマーカーは以下の予約語と合わせて用いることもできます。
if
while
for
match
try
new
this
val
given
上記の例では「end try
」がエンドマーカーになっています。
インデントマーカー:
次はPythonっぽい、行末コロン(:
)の例です。開き中括弧({
)が有効な箇所で行末をコロンにするとインデント構文を開始することができます。
val z = List(2, 3, 4) map:
x =>
y = y - 1
y * y
ただし、このインデントマーカーはまだ他のインデントスキームよりも議論の余地が大きく、コンパイラオプションに-Yindent-colons
を指定した時だけ有効になります。
設定と書換え
インデント構文はデフォルトで有効ですが、コンパイラオプション-noindent
, -old-syntax
,-language:Scala2
のいずれかを指定すれば無効にできます。実際に試してみましたがインデント構文を利用している箇所はエラーになってコンパイルができなくなりました。
また、コンパイラオプションでインデント構文への書換えもできます。インデント構文への書換えは-rewrite
-new-syntax
オプションをつけてコンパイル後に、もう一度-rewrite
-indent
オプションをつけてコンパイルする必要があります。つまり面倒ですが2回コンパイラを起動する必要があります。この書換えは上手くいきました。
逆方向の書換えを行うには-rewrite
-noindent
オプションをつけてコンパイル後に、もう一度を-rewrite
-old-syntax
つけてコンパイルします。この書換えは0.19.0-RC1
時点では失敗しました。どうやらエンドマーカーの書換えで失敗しているようです。
@main
関数
インデント構文とは欠片も関係ありませんが、0.18.1-RC1で導入された@main
関数についても紹介しておきます。従来は以下のように書いていたmain関数ですが1、
object Example {
def main(args: Array[String]): Unit = IndentBaseExample.use("Monday")
}
@main
関数を使うと以下のように書くことができます。Scalaのスクリプティングが捗りそうです2。
@main def example: Unit = IndentBaseExample.use("Monday")
今すぐ試してみたい!
上記のサンプルコードをすぐに試せるようにGitHubに公開したのでご査収ください。インデントベースの構文と従来のブレースベースの構文はどちらも有効なので、実際に触ってみて感触を掴むのが一番だと思われます。念のため書いておきますが、サンプルコード自体に特に意味はありません。インデントベースの構文の雰囲気が分かるように適当に構文を並べただけです。
インデントベース構文の状況
インデントベースの構文は実は2017年にOdersky先生が#2491で提案されていて、このときは大激論の末に一旦クローズされています3。そしてようやく今回執念のプルリク(#7083)を投げて、捩じ込みました。
「捩じ込む」と表現したのは今回も当然のように大激論が起こったからです。見た目が違うから拡張子を変えた方がいいという意見や、読みにくさや曖昧さを指摘する意見や、初学者の混乱を指摘する意見まで様々です。
Odersky先生もこのプルリクはデータを集めるための実験としており、現在のところこのインデント構文はScala 3の新機能の中でも採用される可能性が最も低いものの一つだと考えられます。以下は#7083のリアクションですが、こんなに嫌われているプルリクには滅多にお目にかかれません(笑)。
経緯はともかくOdersky先生の壮大な実験は始まりました。Scalaのようにある程度普及した言語が途中からインデントベースの構文をサポートした例を自分は知りません。今後の成り行きが気になるところですが、Scala 3.0の機能凍結及びScala 3.0のM1リリースは今年(2019年)の秋で、Scala 3.0 finalのリリースは来年(2020年)秋と言われているので4、何か言いたいことがある方はなるべく早めに本家にフィードバックをかけた方が良いと思われます5。
ちなみに本記事はあくまでインデントベース構文の紹介が目的なのでこの構文の良し悪しについて突っ込んだ批評は控えておきます。ただ一応参考までに個人的な初見の感想を述べておくと、自分が趣味で使うのには良さそうな感じです。仕事で使うのにはコーディング規約をどうするか悩みどころが多いと感じましたが、結局はエディタ、IDE、フォーマッタ、リンタ等のツール群のサポートがきちんと得られれば使えるとは思うので、それらの対応次第かなと思っています。
最後に
本記事ではScala 3へ入る可能性のあるインデントベースの構文を紹介しました。本機能は実際にScala 3に入るかどうかはわかりませんが、まだまだ多くの議論がいるので、より多くのフィードバックが必要とされています。興味のある方は実際に触ってみてScala Contributors等でフィードバックしてみてはいかがでしょうか?
2021年3月の段階でScala 3.0.0-RC1にこのインデントベースの構文はしっかり組み込まれています。ドキュメントのサンプルコードでも様々な箇所でインデント構文が使われているので、Scala 3を学ぶ上で避けては通れない内容になっていると思われます。
参考文献
- Announcing Dotty 0.18.1-RC1 – switch to the 2.13 standard library, indentation-based syntax and other experiments
- Allow significant indentation syntax by odersky · Pull Request #7083 · lampepfl/dotty
- Change indentation rules to allow copy-paste by odersky · Pull Request #7114 · lampepfl/dotty
- Consider syntax with significant indentation · Issue #2491 · lampepfl/dotty
- New Control Syntax
- Optional Braces
更新内容
2019年9月28日の更新内容
先日発表されたDotty 0.19.0-RC1でインデント構文が若干変更されました。
- クラスやオブジェクトの定義でインデントをする場合に
:
が必要なくなった - インデントマーカー
:
の利用時には-Yindent-colons
のオプションの指定が必要になった
上記の内容は本文にも反映済みです。またインデント構文への書換えも試してみたので追記を行っています。
下記のサンプルリポジトリに関しても0.19.0-RC1にバージョンアップして対応済みです。
またDotty 0.19.0-RC1でContextual Abstractionsに関する修正もあったので以下の記事も合わせて修正しています。
2019年11月16日の更新内容
先日発表されたDotty 0.20.0-RC1でインデント構文が若干変更されました。
インデントに関係ある変更は以下のとおりです。関連する箇所に関して記事の追記を行っています。
- クラス、トレイトの後ろにオプションで
with
を置けるようになった -
if
式の行末のthen
がオプションになった
2021年3月7日の更新内容
Scala 3.0.0-RC1の内容に合わせて修正しました。細々とした変更があります。
-
App
トレイトをミックスインして書く方法もあります。 ↩ -
@main
関数は引数をとることもできてコマンドライン引数を受け取れるのですが、話が脱線するのでこの記事ではこれ以上は触れません。 ↩ -
#2491も今回と似たような提案ですが、大きな違いは
with
がインデントマーカーとして使われているところです。またエンドマーカーも現在とは異なっていました。 ↩ -
A Tour of Scala 3を参照。 ↩
-
フィードバックの方法としてはScala Contributorsでトピックを立てるのが一番簡単だと思われます。もちろん問題が明確ならイシューを開いたりプルリクを送る方法もあります。 ↩