Help us understand the problem. What is going on with this article?

Scala 3、Pythonのようにインデントベースの構文で書けるようになるってよ!

ここ数年でインデントベースの記述は広くプログラマ界隈で受け入れられるようになってきました。プログラミング言語ではPythonの成功が大きく、ドキュメントではmarkdownとyamlが広く普及しています。そしてScala 3でもとうとうその波に乗ろうという動きが見えてきました・・・

(本記事は自分のブログからの転載記事です。)

(2019年9月28日追記・更新: 追記内容はここを見てください)
(2019年11月16日追記・更新: 追記内容はここを見てください)

TL;DR

  • Scala 3のリサーチコンパイラであるDotty 0.18.1-RC1にインデントベースの構文が実装されました
  • インデントベースの構文はまだ提案段階でありScala3の正式な仕様に決定したわけではありません
    • 今後機能が変化したり、機能が採用されなかったりする可能性も十分あります
    • というか反対意見の方が多いです
  • 従来の括弧ベースの構文も混ぜて使えます
  • この記事はインデントベース構文の紹介記事です。この構文の良し悪しについては触れていません

まずはコードを御覧ください

ウソみたいだろ・・・Scalaなんだぜ、それ

object IndentBasedExample
  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 with
    def f: Int

  class B with
    def g: Int = 27

  class C(x: Int) extends B with A with
    def f = x

  type T = A with
    def f: Int

  def use(dayString: String) =
    val day = fromString(dayString)

    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")

    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 =>
        y = y - 1
        y * y

    z.foreach:
      println

@main def example: Unit =
  IndentBaseExample.use("Monday")

いつもはこんなコードを書いていたはず・・・

従来のコード
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の場合はもっと多くのキーワードがインデント構文の開始の合図になり得ます。

= => <- if then else while do try catch finally for yield match

また、class, object, given, や enum定義でもインデント構文を利用できます。

インデントはタブとスペースの両方が使えますが混ぜると比較ができないケースがあるので、混ぜるな危険です。

定義

以下はobjectの定義とenumの定義でインデント構文を開始しています。

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」を参照してください。

また、定義の行末を以下のようにwithで終わることもできます。

trait A with
  def f: Int

class B with
  def g: Int = 27

class C(x: Int) extends B with A with
  def f = x

type T = A with
  def f: Int

withはオプションですが、インデント構文を使う場合は、基本的につけたほうが良さそうです。例えば、以下のようにクラスBの定義で次の行にインデントをつけ忘れた場合、withがあるとコンパイラが構文エラーにしてくれます。
もし、withをつけていなかった場合はエラーにはならずに、defから始まる関数定義はクラスBに所属するのではなくクラスBと同じ名前空間のメソッドとして定義されてしまいます。

class B with // `with`をつけているのでインデントを次行でつけ忘れても構文エラーになる
def g: Int = 27

if

if式の場合は、インデントブロック開始の合図が「then」になります。最初thenを書き忘れたらコンパイラにthenがないって怒られて、thenってなんぞや???となりました。どうやら新しいキーワードみたいです。
行末のthenは0.20.0-RC-1でオプションになりました。つまり以下の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

以下のコードは=foryieldがインデント構文の開始の合図になっています。

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」が識別子のエンドマーカーです。エンドマーカーはオプションなのでなくても問題なく動作しますが、ドキュメントではひと目でブロックを識別することが難しい長いブロック(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

上記の例では「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に公開したのでご査収ください。インデントベースの構文と従来のブレースベースの構文はどちらも有効なので、実際に触ってみて感触を掴むのが一番だと思われます。念のため書いておきますが、サンプルコード自体に特に意味はありません。インデントベースの構文の雰囲気が分かるように適当に構文を並べただけです。

hinastory/dotty_examples - 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等でフィードバックしてみてはいかがでしょうか?

参考文献

更新内容

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でインデント構文が若干変更されました。

インデントに関係ある変更は以下のとおりです。関連する箇所に関して記事の追記を行っています。


  1. Appトレイトをミックスインして書く方法もあります。 

  2. @main関数は引数をとることもできてコマンドライン引数を受け取れるのですが、話が脱線するのでこの記事ではこれ以上は触れません。 

  3. #2491も今回と似たような提案ですが、大きな違いはwithがインデントマーカーとして使われているところです。またエンドマーカーも現在とは異なっていました。 

  4. A Tour of Scala 3を参照。 

  5. フィードバックの方法としてはScala Contributorsでトピックを立てるのが一番簡単だと思われます。もちろん問題が明確ならイシューを開いたりプルリクを送る方法もあります。 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away