Scala言語の概要

Table of Contents

1 概要

Scala言語の基本について学ぶ.

以下は,作成中である.

1.1 注意

本Webページの作成には Emacs org-mode を用い, 数式等の表示は MathJax を用いています. IEでは正しく表示されないことがあるため, Firefox, Safari等のWebブラウザでJavaScriptを有効にしてお使いください. また org-info.js を利用しており, 「m」キーをタイプするとinfoモードでの表示になります. 利用できるショートカットは「?」で表示されます.

2 字句要素

2.1 識別子

識別子 (identifiers)は,変数名,関数名,クラス名などに利用される. 他のプログラミング言語と同様に,通常は英文字から始まる英数字の列を用いるが, それ以外にも様々な文字列が利用できる.

  • 英文字から始まる英数字の列 (例: abc, x_1)
  • Unicode文字の列 (例: あいう, αβγ)
  • 英数字の列の後に,下線と記号の列を付けた文字列 (例: empty_?)
  • 記号の列 (例: +, +=, +:)

記号の列の識別子は,演算子を実現するために関数名に用いられることがある.

記号が連続している場合,最長の文字列が識別子として取り出される. したがって,必要に応じて空白を挿入しておく必要がある.

scala> 1+-2
<console>:1: error: value +- is not a member of Int
       1+-2
        ^

scala> 1 + -2
res: Int = -1

2.2 整数リテラル

整数リテラル (integer literals)は IntLong 型の定数を表す. L の修飾子を付けない限りは Int と解釈される.

  • Int リテラル (例: 123456789, 0x75BCD15)
  • Long リテラル (例: 12345678901L, 0x2DFDC1C35L)
scala> 123456789
res: Int = 123456789

scala> 0x75BCD15
res: Int = 123456789

scala> 12345678901L
res: Long = 12345678901

scala> 0x2DFDC1C35L
res: Long = 12345678901

2.3 浮動小数点数リテラル

浮動小数点数リテラル (floating point literals)は FloatDouble 型の定数を表す. f の修飾子を付けない限りは Double と解釈される.

scala> 3.141592f
res: Float = 3.141592

scala> 3.141592653589793d
res: Double = 3.141592653589793

scala> 3.141592653589793
res: Double = 3.141592653589793

2.4 ブール・リテラル

ブール・リテラル (Boolean literals)は Boolean 型の定数を表す.

scala> true
res: Boolean = true

scala> false
res: Boolean = false

2.5 文字リテラル

文字リテラル (character literals)は Char 型の定数を表す. シングル・クォートでくくって表す.

scala> 'a'
res: Char = a

scala> 'あ'
res: Char = あ

scala> '\''
res: Char = '

以下のエスケープ文字 (Scala spec: Escape Sequences)が利用できる.

エスケープ文字Unicode名前
\b\u0008backspace
\t\u0009horizontal tab
\n\u000alinefeed
\f\u000cform feed
\r\u000dcarriage return
\"\u0022double quote
\'\u0027single quote
\\\u005cbackslash
\uXXXX\uXXXXunicode character

2.6 文字列リテラル

文字列リテラル (string literals)は String 型の定数を表す. ダブル・クォートでくくって表す. また,文字リテラルと同様にエスケープ文字 (Scala spec: Escape Sequences)が利用できる.

scala> "Hello,\n神戸!\n"
res: String =
"Hello,
神戸!
"

scala> "\\d+"
res: String = \d+

また,三重のダブル・クォート (""")でくくると,エスケープ文字が無効になり, 改行も含めることができる.

scala> """Hello,
神戸!
"""
res: String =
"Hello,
神戸!
"

scala> """\d+"""
res: String = \d+

文字列の前に s を付けることで,文字列補間 (String Interpolation)を利用できる. 文字列中に $変数名 あるいは ${式} と記述することで, 値を toString で文字列に変換した結果を挿入できる.

scala> val x = 123
x: Int = 123

scala> s"Answer is $x."
res: String = Answer is 123.

scala> s"Answer is ${2*x}."
res: String = Answer is 246.

2.7 コメント

コメント (comments)は,Javaと同様に // から行末までと, /* から */ までの二種類が利用できる. なお, /* から */ までのコメントは,入れ子にすることができる.

scala> 123 // コメント
res: Int = 123

scala> 123 /* コメント /* 注釈 */ コメント */ + 456
res: Int = 579

2.8 改行

ScalaではJavaと同様の文末にセミコロン (;)を付けるが, 行末の場合に省略することができる.

scala> 123;
res: Int = 123

scala> 123
res: Int = 123

3

3.1 メソッド呼び出し

Javaと同様に x.m(args) で,オブジェクト x に定義されているメソッド m を呼び出すことができる.

scala> "Kobe".length
res: Int = 4

scala> 123.toBinaryString
res: String = 1111011

scala> 123.toBinaryString.length
res: Int = 7

scala> "Kobe".charAt(1)
res: Char = o

scala> 123.+(1)
res: Int = 124

scala> 1.to(5)
res: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)

scala> 1.to(9).take(5)
res: scala.collection.immutable.Range = Range(1, 2, 3, 4, 5)

Scalaでは 123Int 型のオブジェクトであり,加算 (+), toBinaryString, to などのメソッドが定義されている.

3.2 演算子と優先度

Scalaではメソッド呼び出しに前置記法,挿入記法,後置記法を利用でき, 柔軟な構文が実現できている.

まず x.m(y) は,ピリオドやカッコを省略して x m y と挿入記法で記述できる.

scala> 123 + 1
res: Int = 124

scala> 1 to 5
res: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5)

scala> 1 to 9 take 5
res: scala.collection.immutable.Range = Range(1, 2, 3, 4, 5)

1 to 9 take 5 のように,複数の挿入記法がある場合,基本的には左結合的に (1 to 9) take 5 として解釈される. ただし,以下のように演算子名 (メソッド名)の 最初の文字 によって優先度が定義されており,下のものほど強い結合になる.

(all letters)
|
^
&
= !
< >
:
+ -
* / %
(all other special characters)

たとえば 1 + 2 * 3 は, * のほうが優先度が高いから 1.+(2.*(3)) と解釈される.

ただし,上記の規則には以下の例外がある.

  • = で終わる演算子の一部 (+=, -= など)は,代入演算子と呼ばれ = と同じ優先度と解釈される. なお = で終わる演算子であっても,以下は代入演算子とは見なされない.
    • さらに = で始まっている (== など).
    • >=, <=, != のいずれか.
  • : で終わる演算子 (::, +: など)は右結合的に解釈される. さらに x m yy.m(x) と解釈される.

特に,2番目の例外がリストの表記で有効に利用されている. たとえば 1 :: 2 :: Nil は,右結合的に解釈されて 1 :: (2 :: Nil) と同様であり, さらにこれは (Nil.::(2)).::(1) と同一である.

scala> 1 :: 2 :: Nil
res: List[Int] = List(1, 2)

scala> (Nil.::(2)).::(1)
res: List[Int] = List(1, 2)

つまり x.::(y) は,リスト x の先頭に要素 y を付け加えるメソッドであり, これにより 1 :: 2 :: NilList(1, 2) が生成されている.

演算子 +, -, ! , ~ は,前置演算子として利用できる. これらを前置演算子として利用した場合,以下の対応でメソッドが呼び出される.

前置記法メソッド呼び出し
+ xx.unary_+
- xx.unary_-
! xx.unary_!
~ xx.unary_~

3.3 ブロック式

Scalaではブロック (blocks)も式として値を持つ. { 文1; 文2; ...; 式 } というブロックの場合,最後の式が値となる. また { 文1; 文2; ...; 式 } は,以下のように記述できる.

{
  文1
  文2
  ...
  式
}
scala> { val x = 123; x + 456 }
res: Int = 579

scala> {
  val x = 123
  x + 456
}
res: Int = 579

scala> val y = { val x = 123; x + 456 }
y: Int = 579

scala> val y = {
  val x = 123
  x + 456
}
y: Int = 579

3.4 条件式

Scalaで条件式 (conditional expressions)は以下のように記述する.

if (条件式) 式1 else 式2

「条件式」が成り立てば「式1」,成り立たなければ「式2」が値となる.

scala> if (true) "OK" else "NG"
res: String = OK

scala> val score = 60
score: Int = 60

scala> val mark = if (score >= 90) "S" else if (score >= 80) "A" else if (score >= 70) "B" else if (score >= 60) "C" else "D"
mark: String = C

3.5 for内包表記

for内包表記 (for comprehensions)は,リストなどを生成するために利用できる.

scala> for (i <- 1 to 5) yield i + 1
res: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 3, 4, 5, 6)

1から5の各 i に対して i + 1 が計算され,それらがリスト (Vector)の要素としてまとめられている.

複数の変数でループすることもできる.

scala> for (i <- 0 to 2; j <- 0 to 2) yield 3*i+j
res: scala.collection.immutable.IndexedSeq[Int] = Vector(0, 1, 2, 3, 4, 5, 6, 7, 8)

ループ定義部分に if で条件を記述することもできる.

scala> for (i <- 0 to 2; j <- 0 to 2; if i != j) yield 3*i+j
res: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3, 5, 6, 7)

ループ定義部分が複雑な場合, {} でくくって複数行で記述する.

scala> for {
  i <- 0 to 2
  j <- 0 to 2
  if i != j
} yield 3*i+j
res: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3, 5, 6, 7)

また,変数を定義し,その値を利用することもできる.

scala> for {
  i <- 0 to 2
  j <- 0 to 2
  k = 3*i + j
  if k % 2 == 0
} yield k
res: scala.collection.immutable.IndexedSeq[Int] = Vector(0, 2, 4, 6, 8)

当然,yieldの式にブロックを用いることもできる.

scala> for (i <- 0 to 2; j <- 0 to 2) yield {
  val k = 3*i + j
  if (k % 2 == 0) k else -k
}
res: scala.collection.immutable.IndexedSeq[Int] = Vector(0, -1, 2, -3, 4, -5, 6, -7, 8)

4 パターンマッチ

Scalaのmatch式を用いると,Javaのswitch文と同様の処理を行える.

match {
  case パターン1 => 式1
  case パターン2 => 式2
  ...
}

ただし,match式は式の一種であり値を持つ. 「式」の値が「パターンi」にマッチする場合,「式i」の値が返される. 複数のパターンにマッチする場合は,一番上のパターンが選ばれる.

以下は,変数 c に代入された値の種類によって異なる文字列を返すmatch式の例である.

scala> val c = 4
c: Int = 4

scala> c match {
  case 0 => "なし"
  case 1 => "ひとつ"
  case 2 => "ふたつ"
  case 3 => "みっつ"
  case k if k > 3 => "たくさん"
  case _ => "わかりません"
}
res: String = たくさん

Javaのswitch文とは異なりcaseの後ろに "if" で条件を記述できる. また,すべてにマッチするパターンとして "_" を利用できる.

さらに ListSeq などのデータ構造にマッチするパターンも記述できる (素晴らしい!). 以下は,変数 xs に代入された列の長さによって異なる文字列を返すmatch式の例である.

scala> val xs = Seq(1)
xs: Seq[Int] = List(1)

scala> xs match {
  case Seq() => "no elements"
  case Seq(x) => s"single element $x"
  case _ => "two or more elements"
}
res: String = single element 1

5 変数の宣言と定義

変数の宣言には var (variable)または val (value)を用いる. var で宣言した変数は,Javaの通常の変数と同様に何度でも値を代入できる. val で宣言した変数は,Javaのfinal変数と同様で,初期値を一度だけ代入でき,その後は値を変更できない.

var 変数名: データ型 = 初期値
val 変数名: データ型 = 初期値

データ型を指定する部分は,Scalaの型推論により省略できる場合がある.

6 関数の宣言と定義

6.1 defによる関数定義

Scalaでは以下のように関数を定義する.

def 関数名(引数1: データ型1, ..., 引数n: データ型n): データ型 = 関数本体

例えば,以下は整数の引数 x に1を加えた値を返す関数 inc の定義である. return文は必要ない.

def inc(x: Int): Int = x + 1

この場合,Scalaの型推論が結果のデータ型を推論できるから,その部分は省略できる.

def inc(x: Int) = x + 1

返り値を持たない関数は以下のように定義する.

def 関数名(引数1: データ型1, ..., 引数n: データ型n) { 関数本体 }

例えば,以下は整数の引数 x の値を表示する関数の定義である.

def p(x: Int) { println(x) }

Scalaでリスト処理 で学ぶように,Scalaには関数を引数とする関数やメソッドが用意されている. これらは高階関数と呼ばれる.

例えば,リスト (List)に対して用意されている map(f) メソッドは, リストの各要素に対して関数 f を適用した新しいリストを返す.

scala> val list = List(3, 1, 4, 2)
list: List[Int] = List(3, 1, 4, 2)

scala> list.map(inc)
res: List[Int] = List(4, 2, 5, 3)

この例では,リスト List(3, 1, 4, 2) の各要素に関数 inc が適用され, その結果として新しいリスト List(4, 2, 5, 3) が求められている.

6.2 匿名関数

匿名関数 (anonymous function)を用いると, def で関数を定義せずに, 高階関数に関数を渡すことができる. 匿名関数は以下のように記述する.

(引数名1: データ型1, ..., 引数名n: データ型n) => 関数本体

特に引数が1つの場合は,カッコを省略して良い.

引数名: データ型 => 関数本体

また,型推論できる場合はデータ型の指定は省略できる.

以下は list.map(inc) と同様の処理を行う例である.

scala> list.map(x => x + 1)
res: List[Int] = List(4, 2, 5, 3)

また,引数名を省略して "_" で表すことも可能である.

scala> list.map(_ + 1)
res: List[Int] = List(4, 2, 5, 3)

すなわち _ + 1x => x+1 と同一である.

6.3 caseを用いた匿名関数

Scalaでは,match式で用いられているcaseの部分を記述することでも匿名関数を実現できる.

{
  case パターン1 => 式1
  ...
  case パターンn => 式n
}

以下は list.map(x => x+1) と同様の処理を行う例である.

scala> list.map({ case x => x + 1 })
res: List[Int] = List(4, 2, 5, 3)

scala> list.map { case x => x + 1 }
res: List[Int] = List(4, 2, 5, 3)

以下は,別の例である.

scala> list.map {
  case 0 => "なし"
  case 1 => "ひとつ"
  case 2 => "ふたつ"
  case 3 => "みっつ"
  case k if k > 3 => "たくさん"
  case _ => "わかりません"
}
res: Seq[String] = List(みっつ, ひとつ, たくさん, ふたつ)

7 クラス定義

Scalaのクラス,オブジェクト,トレイトについて説明する.

オブジェクト指向プログラミングの考え方については, Javaを用いて説明した以下のWebページを参照のこと.

7.1 class

クラスの定義はJavaと同様である.

以下の例は STACK クラスを定義している

class STACK {
  var elems: List[Int] = List()
  def push(x: Int) { elems = x :: elems }
  def pop: Int = elems match {
    case Nil => 0
    case x :: xs => { elems = xs; x }
  }
}

elems がインスタンス変数, pushpop がメソッドである.

利用例は以下の通り.

scala> val s = new STACK
s: STACK = STACK@10993713

scala> s.push(123)

scala> s.elems
res: List[Int] = List(123)

scala> s.pop
res: Int = 123

scala> s.elems
res: List[Int] = List()

以下のように記述すれば,コンストラクタに引数を指定できる. なお List() は,引数が指定されなかった場合のデフォールト値である.

class STACK(var elems: List[Int] = List()) {
  def push(x: Int) { elems = x :: elems }
  def pop: Int = elems match {
    case Nil => 0
    case x :: xs => { elems = xs; x }
  }
}

利用例は以下の通り.

scala> val s = new STACK(123 :: 456 :: Nil)
s: STACK = STACK@45592af7

scala> s.elems
res: List[Int] = List(123, 456)

scala> s.pop
res: Int = 123

7.2 object

object により,単一のオブジェクトを生成し名前を与えることができる. また,その中で変数やメソッドの定義ができるから, Javaでのstatic変数やstaticメソッドと同様の事を行える. 特に, main メソッドを定義すれば,コマンドラインから実行できる.

class STACK(var elems: List[Int] = List()) {
  def push(x: Int) { elems = x :: elems }
  def pop: Int = elems match {
    case Nil => 0
    case x :: xs => { elems = xs; x }
  }
}

object Test {
  def main(args: Array[String]) {
    val elems = args.map(_.toInt).toList
    val s = new STACK(elems)
    println(s.elems)
  }
}

以下は,上の内容のファイル Class.scala をコンパイルし,コマンドラインから実行する例である.

$ scalac Class.scala
$ scala Test 12 34 56
List(12, 34, 56)

また,クラスと同じ名前のオブジェクトを定義し,そこにファクトリーメソッドなどを定義することがある. このようなオブジェクトはコンパニオンオブジェクト (companion object)と呼ばれる.

7.3 case classとcase object

Scalaには case classcase object と呼ばれる 特別なクラス定義およびオブジェクト定義方法が用意されている.

通常のクラス定義およびオブジェクト定義とは異なり,以下の特徴を持つ.

  • インスタンス生成に new が必要ない.
  • toString メソッドが自動的に定義される.
  • hashCode, equals メソッドが自動的に定義され, オブジェクトの構造同値性 (structural equivalence)を実現できる.
  • apply, unapply メソッドが自動的に定義され, match式のパターンマッチに利用できる.

以下は,命題論理式の構造を定義したプログラム例である (CaseClass.scala).

abstract class Formula
case object False extends Formula
case object True extends Formula
case class Var(name: String) extends Formula
case class Not(x1: Formula) extends Formula
case class And(x1: Formula, x2: Formula) extends Formula
case class Or(x1: Formula, x2: Formula) extends Formula
case class Imp(x1: Formula, x2: Formula) extends Formula

以下は,利用例である.

scala> Imp(And(Var("x"), Not(Var("x"))), False)
res: Imp = Imp(And(Var(x),Not(Var(x))),False)

さらに進んだ利用については Scalaで複素数計算 および Scalaと命題論理 を参照のこと.

7.4 TODO trait

trait はJavaの interface と同様であり,クラス階層による継承とは異なり, クラスに機能を追加するために用いられる. =trait=には以下の特徴がある.

  • インスタンスを作成することはできないし,インスンタンス生成時のパラメータも存在しない.
  • with キーワードを用いて,1つのクラスに複数の trait の機能を追加 (ミックスイン)できる.

8 Javaとの連携

8.1 ScalaからJavaを呼び出す

ScalaからJavaのプログラムを利用するのは簡単だ. 例えば,REPLで以下を実行すると100x100のサイズのフレームが画面に表示される.

scala> val frame = new java.awt.Frame
scala> frame.setSize(400, 200) // フレームのサイズを指定
scala> frame.add(new java.awt.Button("Hello Kobe!")) // フレームにボタンを追加
scala> frame.setVisible(true) // フレームを表示
scala> frame.dispose // フレームを消す

次に,自分で作成したJavaプログラムを呼ぶ出す方法を説明する. 以下のようなJavaプログラムがあり,それをコンパイルして作成された classファイルが XXX ディレクトリに保存されているとする.

public class A {
    public void a(int x) {
        System.out.println("Method a: " + x);
    }
}

この場合,以下のようにclassファイルの置かれているディレクトリ名を classpathオプションに指定してscalaを起動する. ただし XXX がカレント・ディレクトリの場合は, -classpath XXX の部分は省略できる.

$ scala -classpath XXX

この後,REPLから以下のようにJavaのクラスAのメソッドaを呼び出せる.

scala> val z = new A
z: A = A@29a33620

scala> z.a(10)
Method a: 10

8.2 JavaからScalaを呼び出す

Java側のプログラムを B.java とする.

public class B {
    public static void main(String[] args) {
        System.out.println(C.c(10));
    }
}

Scala側のプログラムは C.scala とする.

object C {
  def c(x: Int) = s"Method c: $x"
}

Javaのプログラムをコンパイルし実行するには,scalaのインストール時に作成された ディレクトリ scala-2.12.5/lib の場所の指定が必要になる. 以下では,そのディレクトリを LIB として説明する.

  1. Scalaプログラム C.scala をコンパイルする.
    scalac C.scala
    
  2. Javaプログラム B.java をコンパイルする.
    • Windowsの場合
      javac -classpath .;LIB\* B.java
      
    • Mac, Linuxの場合
      javac -classpath .:LIB/"*" B.java
      
  3. Javaプログラムを実行する.
    • Windowsの場合
      java -classpath .;LIB\* B
      
    • Mac, Linuxの場合
      java -classpath .:LIB/"*" B
      

Date: 2020-12-15 00:30:42 JST

Author: 田村直之

Validate XHTML 1.0