47
23

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-16

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

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

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

TL;DR

  • Scala 3のリサーチコンパイラであるDotty 0.18.1-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)」という用語を使用しています。そしてテンプレート本体でインデント構文を使う場合はclassobjectなどのキーワードが含まれる行末をを「:」にします。

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

以下のコードは=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」が識別子のエンドマーカーです。エンドマーカーはオプションなのでなくても問題なく動作しますが、ドキュメントでは空白行が含まれていたり、ひと目でブロックを識別することが難しい長いブロック(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に公開したのでご査収ください。インデントベースの構文と従来のブレースベースの構文はどちらも有効なので、実際に触ってみて感触を掴むのが一番だと思われます。念のため書いておきますが、サンプルコード自体に特に意味はありません。インデントベースの構文の雰囲気が分かるように適当に構文を並べただけです。

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

2021年3月の段階でScala 3.0.0-RC1にこのインデントベースの構文はしっかり組み込まれています。ドキュメントのサンプルコードでも様々な箇所でインデント構文が使われているので、Scala 3を学ぶ上で避けては通れない内容になっていると思われます。

参考文献

更新内容

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

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

2021年3月7日の更新内容

Scala 3.0.0-RC1の内容に合わせて修正しました。細々とした変更があります。


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

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

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

  4. A Tour of Scala 3を参照。 

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

47
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
23