M.Hiroi's Home Page

Scala Programming

お気楽 Scala プログラミング入門

[ PrevPage | Scala | NextPage ]

Scala の基礎知識 (2)

今回は「関数」と「配列」について取り上げます。Scala はオブジェクト指向言語なので、クラスを定義しないとプログラムを作ることができません。このクラスの中で定義された関数が「メソッド (method)」になります。とりあえずオブジェクト指向のことはおいといて、メソッドの関数的な使い方について説明します。

●関数の基礎知識

プログラミングは模型を組み立てる作業と似ています。簡単な処理は Scala の機能やライブラリを使って実現することができます。ところが、模型が大きくなると、一度に全体を組み立てるのは難しくなります。このような場合、全体をいくつかに分割して、まずその部分ごとに作ります。最後に、それを結合して全体を完成させます。

これはプログラミングにも当てはまります。実現しようとする処理が複雑になると、一度に全部作ることは難しくなります。そこで、全体を小さな処理に分割して、一つ一つの処理を作成し、それらを組み合わせて全体のプログラムを完成させます [*1]

分割した処理を作成する場合、それを一つの部品として扱えると便利です。つまり、小さな部品を作り、それを使って大きな部品を作り、最後にそれを組み合わせて全体を完成させます。このとき、もっとも基本となる部品が関数です。Scala はオブジェクト指向言語なので、関数ではなくメソッドになりますが、メソッドを関数のように使うことは簡単にできます。

-- note --------
[*1] このような方法を分割統治法といいます。

●関数の定義方法

Scala の関数 (メソッド) 定義はとても簡単です。本稿では、関数的な使い方をするメソッドのことを関数と呼ぶことにします。例題として、数を 2 乗する関数を作ってみましょう。次の例を見てください。

scala> def square(x: Int): Int = x * x
def square(x: Int): Int

scala> square(10)
val res0: Int = 100

関数定義の構文を示します。

def 名前(仮引数1: 型1, 仮引数2: 型2, ...): 型 = 式

関数の定義は下図のように数式と比較するとわかりやすいでしょう。

def の後ろの square が関数名、( ) の中の x が入力データを受け取る引数、その後ろに関数が出力する値の型、= のあとに実行する式を書きます。関数定義で使用する引数のことを「仮引数」、実際に与えられる引数を「実引数」といいます。上図の場合、square(x: Int) の x が仮引数で、square(10) の 10 が実引数になります。Scala の場合、関数の仮引数は immutable なので、値を書き換えることはできません。ご注意ください。

関数が出力する値を「返り値」といいます。返り値の型は仮引数の後ろに指定します。Java の場合、関数の値は return を使って返しますが、Scala は不要です。式を実行した結果が返り値となります。ブロック式の場合は最後に実行した式の値が返り値となります。また、Java と同様に return を使って明示的に値を返すこともできます。

引数が不要な関数を定義する場合、次に示す 2 通りの方法があります。

1. def 名前() : 返り値の型 = 本体
2. def 名前   : 返り値の型 = 本体

1 のようにカッコを付ける場合、引数の型は Unit になります。呼び出すときはカッコ () を付けます。以前のバージョンではカッコを省略することができたのですが、ver 2.13.5 では warning が表示されます。将来のバージョンでは、カッコを省略することはできなくなるようです。2 の方法も引数無しの関数を定義できますが、カッコを付けて呼び出すとエラーになります。

簡単な例を示します。

scala> def foo(): Int = 10
def foo(): Int

scala> foo
       ^
       warning: Auto-application to `()` is deprecated. Supply the empty argument list `()` 
       explicitly to invoke method foo,
       or remove the empty argument list from its definition (Java-defined methods are exempt).
       In Scala 3, an unapplied method like this will be eta-expanded into a function.
val res1: Int = 10

scala> foo()
val res2: Int = 10

scala> def bar : Int = 20
def bar: Int

scala> bar
val res3: Int = 20

scala> bar()
          ^
       error: Int does not take parameters

本稿では、Scalaスタイルガイド - 引数のないメソッド に従い、副作用のあるメソッドは 1 の方法で、副作用のないメソッドは 2 の方法で定義することにします。

関数の返り値が不要な場合、返り値の型には Unit を指定してください。以前のバージョンでは、返り値の型が Unit で関数本体がブロック式の場合、Unit と = を省略することができました。

def main(args: Array[String]): Unit = { ... } == def main(args: Array[String]) { ... }

ところが現在のバージョン (ver 2.13.5) では、この書き方は非推奨になっています。

scala> def baz() { println("oops!!") }
                 ^
       warning: procedure syntax is deprecated: instead, 
       add `: Unit =` to explicitly declare `baz`'s return type
def baz(): Unit

なお、main メソッドの返り値の型も Unit です。以前のバージョンでは、Unit を省略する書き方が一般的だったのですが、現在のバージョン (ver 2.13.5) を使う場合は Unit = を書くようにしてください。

●局所変数と大域変数

それでは、ここで変数 x に値が代入されている場合を考えてみましょう。次の例を見てください。

リスト : 局所変数と大域変数

object sample0201 {
  val x = 10

  def square(x: Int): Int = x * x

  def main(args: Array[String]): Unit = {
    println(x)
    println(square(5))
    println(x)
  }
}
$ scalac sample0201.scala
$ scala sample0201
10
25
10

object の先頭で変数 x を定義しています。object やクラスで定義された変数はインスタンスを生成するときインスタンスの中に割り当てられます。Java では「フィールド変数」、他のオブジェクト指向言語では「インスタンス変数」と呼びます。本稿では Java と同じくフィールド変数と呼ぶことにします。

sample0201 はシングルトンオブジェクトなので、そのインスタンスは一つしかありません。この中で定義されたフィールド変数は、同じオブジェクトにあるメソッドからアクセスすることができます。

main で最初に println(x) を実行します。この場合、フィールド変数の x を参照するので 10 が表示されます。それでは、square(5) の実行結果はどうなると思いますか。x には 10 がセットされているので 10 の 2 乗を計算して返り値は 100 になるのでしょうか。これは 5 の 2 乗を計算して結果は 25 になります。そして、square を実行したあとでもフィールド変数 x の値は変わりません。

square の仮引数 x は、その関数が実行されている間だけ有効です。このような変数を「ローカル変数 (local variable)」もしくは「局所変数」といいます。これに対し、プログラムのどこからでもアクセスできる変数を「グローバル変数 (golbal variable)」もしくは「大域変数」といいます。

Scala は変数の値を求めるとき、それが局所変数であればその値を参照します。局所変数でなければ、フィールド変数の値を参照します。Scala には厳密な意味での大域変数は存在しませんが、フィールド変数のアクセス権を設定することで、フィールド変数を大域変数のように使用することは可能です。ただし、この方法はあまりお勧めできません。

プログラムを作る場合、関数を部品のように使います。ある関数を呼び出す場合、いままで使っていた変数の値が勝手に書き換えられると、呼び出す方が困ってしまいます。部品であるならば、ほかの処理に影響を及ぼさないように、自分自身の中で処理を完結させることが望ましいのです。これを実現するための必須機能が局所変数なのです。

●局所変数の定義と有効範囲

Scala の場合、関数の仮引数は局所変数になりますが、それ以外にも関数の中で局所変数が必要になる場合があります。Scala は関数内で宣言された変数を局所変数として扱います。今まで main の中で変数を宣言しましたが、これらの変数はすべて局所変数になります。

簡単な例を示しましょう。

リスト : 局所変数の有効範囲

object sample0202 {
  def main(args: Array[String]): Unit = {
    val x = 1
    println("x = " + x);
    {
      val y = 2
      println("y = " + y);
      {
        val z = 3
        println("x = " + x)
        println("y = " + y)
        println("z = " + z)
      }
      println("x = " + x)
      println("y = " + y)
      // println("z = " + z)  error
    }
    println("x = " + x)
    // println("y = " + y)  error
    // println("z = " + z)  error
  }
}
$ scalac sample0202.scala
$ scala sample0202
x = 1
y = 2
x = 1
y = 2
z = 3
x = 1
y = 2
x = 1

局所変数が値を保持する期間のことを、変数の「有効範囲 (scope : スコープ)」といいます。局所変数の有効範囲は変数が定義されているブロックの中だけです。for 式の場合も同じで、定義された変数は局所変数として扱われ、そのあとの式やブロックが有効範囲になります。

sample0202 の変数 x は main の一番外側のブロックで定義されているので、main の処理が終了するまで有効です。変数の定義や式の直後にブロックを書く場合は、セミコロン ( ; ) で区切ってください。変数 y は 2 番目のブロックで、変数 z は 3 番目のブロックで定義されているので、各々のブロックの終わりまでが変数の有効範囲になります。ブロックの実行が終了すると、そのブロックで定義された局所変数は廃棄されます。

したがって、3 番目のブロックの中では変数 x, y, z が有効です。ブロックの処理が終了すると変数 z が廃棄されるので、2 番目のブロックの中では変数 x, y が有効です。そして、そのブロックの処理が終了すると、変数 y が廃棄されるので有効な変数は x と引数の args だけになります。

●整数の和

それでは簡単な例題として、整数 n から m までの和、二乗の和、三乗の和を求める関数を作ってみましょう。次のリストを見てください。

リスト : 整数の和、二乗の和、三乗の和

object SumOf {
  def square(x: BigInt): BigInt = x * x

  def cube(x: BigInt): BigInt = x * x * x

  def sumOfInt(n: Int, m: Int): BigInt = {
    var a: BigInt = 0
    for (i <- n to m) a += i
    a
  }

  def sumOfSquare(n: Int, m: Int): BigInt = {
    var a: BigInt = 0
    for (i <- n to m) a += square(i)
    a
  }

  def sumOfCube(n:Int, m:Int): BigInt = {
    var a: BigInt = 0
    for (i <- n to m) a += cube(i)
    a
  }
}

square は x の二乗を求める関数、cube は x の三乗を求める関数です。引数と返り値の型は BigInt にしました。BigInt は多倍長整数なので、大きな値でも求めることができます。SumOfInt は簡単です。整数 n から m までの値 i を変数 a に加算するだけです。SumOfSquare は square(i) の返り値を変数 a に、SumOfCube は cube(i) の返り値を変数 a に加算するだけです。なお、整数の和、二乗の和、三乗の和は数学の公式を使えば簡単に求めることができます。興味のある方はプログラムを改良してみてください。

それでは実行してみましょう。

scala> :load sumof.scala
Loading SumOf.scala...
defined object SumOf

scala> SumOf.square(10)
val res0: BigInt = 100

scala> SumOf.cube(10)
val res1: BigInt = 1000

scala> import SumOf._
import SumOf._

scala> square(11)
val res2: BigInt = 121

scala> cube(11)
val res3: BigInt = 1331

scala> sumOfInt(1, 100)
val res4: BigInt = 5050

scala> sumOfInt(1, 10000)
val res5: BigInt = 50005000

scala> sumOfSquare(1, 100)
val res6: BigInt = 338350

scala> sumOfSquare(1, 10000)
val res7: BigInt = 333383335000

scala> sumOfCube(1, 100)
val res8: BigInt = 25502500

scala> sumOfCube(1, 10000)
val res9: BigInt = 2500500025000000

コマンド :load でファイル SumOf.scala を読み込みます。object SumOf のインスタンスは object の名前 SumOf でアクセスすることができます。メソッドの呼び出しは インスタンス + ドット ( . ) + メソッド(引数, ...) で行います。これは Java と同じです。

次に、import で object SumOf のメソッドをすべて REPL にインポートします。これでオブジェクト名 SumOf を付けずにメソッドを呼び出すことができます。import の使い方は回を改めて詳しく説明する予定です。どの関数も正常に動作していますね。

●局所関数の定義

Scala は関数の中で別の関数を定義することができます。つまり、関数のネスト(入れ子)ができるわけです。入れ子の関数は局所的な関数として扱われるので、定義された関数の中でのみ有効です。他の関数から呼び出すことはできません。

たとえば、square と cube を関数の中で定義すると、次のようになります。

リスト : 局所関数の定義

object SumOf {
  def sumOfInt(n: Int, m: Int): BigInt = {
    var a: BigInt = 0
    for (i <- n to m) a += i
    a
  }

  def sumOfSquare(n: Int, m: Int): BigInt = {
    def square(x: BigInt): BigInt = x * x
    var a: BigInt = 0
    for (i <- n to m) a += square(i)
    a
  }

  def sumOfCube(n:Int, m:Int): BigInt = {
    def cube(x: BigInt): BigInt = x * x * x
    var a: BigInt = 0
    for (i <- n to m) a += cube(i)
    a
  }
}

局所関数の定義は今までと同じく def 文で行います。ブロックの中で def を使って局所関数を定義するだけです。

●累乗の計算

もう一つ、簡単な数値計算の例を紹介しましょう。累乗は下図のように x の n 乗という x を n 回掛ける計算です。

x ** 3 = x * x * x
x ** 4 = x * x * x * x
x ** 5 = x * x * x * x * x


  図 : 累乗の計算

Scala には累乗を計算するメソッド pow がありますが、私たちでも簡単にプログラムすることができます。累乗を単純な繰り返しで実装すると、次のようになります。

リスト : 累乗の計算

object Power {
  // 単純版
  def pow(x: BigInt, n: Int): BigInt = {
    var a: BigInt = 1
    for (_ <- 0 until n) a *= x
    a
  }

  // 改良版
  def pow1(x: BigInt, n: Int): BigInt = {
    var m = n
    var a: BigInt = 1
    var b = x
    while (m > 0) {
      while (m % 2 == 0) {
        b *= b
        m /= 2
      }
      a *= b
      m -= 1
    }
    a
  }
}

for の変数名がアンダーバー ( _ ) になっていますが、そのあとの処理で変数を使用しない場合は、変数名を定義せずにアンダーバー [*2] で済ますことができます。関数 pow の場合、n 回の乗算が必要になります。ところが、式を変形するともっと少ない回数で求めることができます。

x ** 4  = (x ** 2) ** 2 -> 2 回
x ** 8  = (x ** 4) ** 2 -> 3 回
x ** 16 = (x ** 8) ** 2 -> 4 回

x ** n = (x * x) ** (n / 2); (n は偶数)
x ** n = x * ((x * x) ** (n / 2)); (n は奇数)


  図 : 累乗を求める式の変形

累乗の場合、n が偶数であれば (x * x) ** (n / 2) で求めることができます。つまり、n が半分になるので、ひとつずつ n を減らすよりも減少の度合いは大きくなります。その分だけ計算回数が少なくなるわけです。

関数 pow1 を見てください。引数 x, n は immutable なので、mutable な変数 b, m に値を移します。2 番目の while ループで、m が偶数のあいだ m を半分にして、b を自乗します。ループが終了すると、m は奇数なので、a に b を乗算して m を -1 します。あとは、m が 0 になるまで処理を繰り返すだけです。

それでは実行してみましょう。

scala> :load Power.scala
Loading Power.scala...
defined object Power

scala> import Power._
import Power._

scala> pow(2, 16)
val res0: BigInt = 65536

scala> pow(2, 32)
val res1: BigInt = 4294967296

scala> pow(2, 64)
val res2: BigInt = 18446744073709551616

scala> pow(2, 128)
val res3: BigInt = 340282366920938463463374607431768211456

scala> pow1(2, 16)
val res4: BigInt = 65536

scala> pow1(2, 32)
val res5: BigInt = 4294967296

scala> pow1(2, 64)
val res6: BigInt = 18446744073709551616

scala> pow1(2, 128)
val res7: BigInt = 340282366920938463463374607431768211456

正常に動作していますね。

-- note --------
[*2] 正確にいうと、アンダーバーは「パターンマッチング」のワイルドカードのことで、for 式での変数定義はパターンマッチングを使用することができます。パターンマッチングについては別の回で詳しく説明する予定です。

●配列

Scala の配列 (Array) はデータを一列に並べたもので、Java の配列と同じデータ構造です。配列に格納されたデータを要素といいます。配列はホテルやマンションの部屋にたとえるとわかりやすいと思います。ホテル全体を配列とすると、各部屋がデータを格納する変数と考えることができます。ホテルでは、ルームナンバーによって部屋を指定しますね。配列の場合も、整数値によってデータを格納する変数を指定することができます。この整数値を「添字 (subscripts)」といいます。


                  図 : 配列の構造

たとえば、10 個のデータを格納する配列を考えてみます。これは、平屋建てのホテルで、部屋が 10 室あると考えてください。この場合は上図に示すように、データを格納する変数が並んでいて、それぞれ 0 から 9 までの添字で指定することができます。Scala の場合、Java と同じく添字は 0 から順番に数えます。

Scala の場合、配列は次のように宣言します。

val 変数名: Array[型] = new Array(大きさ)
val 変数名 = new Array[型](大きさ)

配列の型は Array[型] で表します。角カッコの中に要素の型を指定します。変数に型を指定した場合は、new Array(n) で大きさ n の配列を生成することができます。変数に型を指定しない場合は new Array[型](n) としてください。要素はデフォルトの値 (整数であれば 0) になります。

要素の初期値を指定して配列を生成することもできます。

val 変数名: Array[型] = Array(要素1, 要素2, ...)
val 変数名 = Array[型](要素1, 要素2, ...)

この場合、new を付けずに Array(n1, n2, n3, ...) のようにカッコの中で要素を指定します。この場合、カッコの中の要素数が配列の大きさになります。このとき、配列の型と要素の型が一致しないとコンパイルエラーになります。なお、配列の型指定は省略してもかまいません。要素の型より Scala が推論してくれます。

簡単な例を示しましょう。

scala> val a = Array(1,2,3,4,5,6,7,8)
val a: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8)

scala> a(0)
val res0: Int = 1

scala> a(7)
val res1: Int = 8

scala> a.size
val res2: Int = 8

scala> a(0) = 10

scala> a
val res4: Array[Int] = Array(10, 2, 3, 4, 5, 6, 7, 8)

Scala の場合、配列のアクセスは丸カッコを使います。Java のように角カッコではないので注意してください。配列の要素を取り出して変数に代入することも、配列の要素を書き換えることもできます。配列は自分自身の大きさを知っていて、変数名 + ドット ( . ) + size で値を取り出すことができます (size はメソッドです)。

配列は入れ子にすることができます。つまり、配列の要素に配列を入れてもかまいません。これで多次元配列を表すことができます。簡単な例を示しましょう。

scala> val a2 = Array(Array(1,2,3), Array(4,5,6), Array(7,8,9))
val a2: Array[Array[Int]] = Array(Array(1, 2, 3), Array(4, 5, 6), Array(7, 8, 9))

scala> a2(0)(0)
val res6: Int = 1

scala> a2(2)(2)
val res7: Int = 9

scala> a2(2)(2) = 20

scala> a2
val res9: Array[Array[Int]] = Array(Array(1, 2, 3), Array(4, 5, 6), Array(7, 8, 20))

scala> val b2 = Array.ofDim[Int](3,3)
val b2: Array[Array[Int]] = Array(Array(0, 0, 0), Array(0, 0, 0), Array(0, 0, 0))

多次元配列は要素の型を表す [ ] の中に配列の型を入れることで宣言します。2 次元配列の場合、型は Array[Array[Int]] になります。要素を指定して配列を生成する場合は、Array(...) の中に Array(...) を入れるだけです。

要素のアクセスは簡単で、最初の ( ) で配列に格納されている配列を取り出し、次の ( ) で取り出した配列の要素を指定します。たとえば、a2(2)(2) は a2[2] に格納されている配列 Array(7, 8, 9) を取り出し、次の (2) でその配列の 2 番目の要素を指定します。したがって、a2[2][2] の値は 9 になります。a2[2][2] = 20 とすると 9 が書き換えられて 20 になります。

大きさを指定して多次元配列を生成する場合は Array のメソッド ofDim を使います。

Array.ofDim[型](n1, n2, ...)

カッコの中で各次元の大きさを指定します。たとえば、(3, 3) ならば 3 * 3 の二次元配列を、(3, 4, 5) であれば 3 * 4 * 5 の三次元配列を生成します。ofDim で生成できる多次元配列は5次元までです。

●統計量の計算

それでは簡単な例題として、配列に格納されている実数 (Double) の合計値、平均値、最大値、最小値、標準偏差を求める関数を作ってみましょう。統計学ではこれらの値を「統計量」といいます。

データを \(x_1, x_2, \ldots, x_N\) とすると、総計量 (合計値) \(M\) と平均値 \(T\) は次式で求めることができます。

\(\begin{array}{l} T = x_1 + x_2 + \cdots + x_N = \displaystyle \sum_{i=1}^{N} x_i \\ M = \dfrac{x_1 + x_2 + \cdots + x_N}{N} = \dfrac{1}{N} \displaystyle \sum_{i=1}^{N} x_i \end{array}\)

平均値が同じ場合でも、データの特徴が異なる場合があります。たとえば、A = {4, 4, 5, 5, 5, 6, 6, 6, 7, 7} と B = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} の平均値は 5.5 になります。A のデータは平均値の近くに集まっていてますが、B のデータはバラバラになっていますね。統計学では、ばらつきの大きさを表すために「分散 (variance)」という値を使います。分散 \(S^2\)の定義を次に示します。

\( S^2 = \dfrac{(x_1 - M)^2 + (x_2 - M)^2 + \cdots + (x_N - M)^2}{N} = \dfrac{1}{N} \displaystyle \sum_{i=1}^{N} (x_i - M)^2 \)

標準偏差 \(S = \sqrt{S^2}\)

分散の定義からわかるように、平均値から離れたデータが多いほど、分散の値は大きくなります。逆に、平均値に近いデータが多くなると分散は小さな値になります。そして、分散の平方根が「標準偏差 (SD : standard deviation)」になります。標準偏差は、ばらつきの大きさを表すのによく使われています。

プログラムは次のようになります。

リスト : 統計量の計算

object sample0203 {
  // 身長のデータ (乱数で作成)
  val height:Array[Double] = Array(
    148.7, 149.5, 133.7, 157.9, 154.2, 147.8, 154.6, 159.1, 148.2, 153.1,
    138.2, 138.7, 143.5, 153.2, 150.2, 157.3, 145.1, 157.2, 152.3, 148.3,
    152.0, 146.0, 151.5, 139.4, 158.8, 147.6, 144.0, 145.8, 155.4, 155.5,
    153.6, 138.5, 147.1, 149.6, 160.9, 148.9, 157.5, 155.1, 138.9, 153.0,
    153.9, 150.9, 144.4, 160.3, 153.4, 163.0, 150.9, 153.3, 146.6, 153.3,
    152.3, 153.3, 142.8, 149.0, 149.4, 156.5, 141.7, 146.2, 151.0, 156.5,
    150.8, 141.0, 149.0, 163.2, 144.1, 147.1, 167.9, 155.3, 142.9, 148.7,
    164.8, 154.1, 150.4, 154.2, 161.4, 155.0, 146.8, 154.2, 152.7, 149.7,
    151.5, 154.5, 156.8, 150.3, 143.2, 149.5, 145.6, 140.4, 136.5, 146.9,
    158.9, 144.4, 148.1, 155.5, 152.4, 153.3, 142.3, 155.3, 153.1, 152.3
  )

  // 合計値
  def sum(buff: Array[Double]): Double = {
    var a = 0.0
    for (x <- buff) a += x
    a
  }

  // 平均値
  def avg(buff: Array[Double]): Double = {
    sum(buff) / buff.size
  }

  // 標準偏差
  def sd(buff: Array[Double]): Double = {
    val m = avg(buff)
    var s = 0.0
    for (x <- buff) s += (x - m) * (x - m)
    Math.sqrt(s / buff.size)
  }

  // 最大値
  def maximum(buff: Array[Double]): Double = {
    var m = Double.MinValue
    for (x <- buff){
      if (x > m) m = x
    }
    m
  }

  // 最小値
  def minimum(buff: Array[Double]): Double = {
    var m = Double.MaxValue
    for (x <- buff) {
      if (x < m) m = x
    }
    m
  }

  def main(args: Array[String]): Unit = {
    print("sum = ")
    println(sum(height))
    print("avg = ")
    println(avg(height))
    print("sd = ")
    println(sd(height))
    print("max = ")
    println(maximum(height))
    print("min = ")
    println(minimum(height))
  }
}

データ height は乱数で作成したある学年の生徒の身長を表しています。合計値を求める関数 sum は簡単ですね。配列から順番にデータを取り出して変数 a に加算するだけです。平均値を求める関数は、sum で合計値を求め、それを配列の大きさ buff.size で割り算するだけです。

標準偏差を求める関数 sd は、avg を呼び出して平均値を求め、for ループで分散 s を計算します。あとは、s / buff.size の平方根を求めるだけです。Math は Scala のパッケージ scala.math の別名です。Scala のライブラリはパッケージ (package) で管理されています。sqrt(n) は n の平方根を求める関数です。

最大値を求める関数 maximum も簡単です。変数 m を Double の最小値 Double.MinValue に初期化します。あとは、配列から要素を順番に取りして m と比較し、m よりも大きい場合は m の値を書き換えます。最後に m を返します。最小値を求める関数 minimum は、m を Double の最大値 MaxValue に初期化し、配列の要素が m よりも小さい場合はその値に書き換えます。

それでは実行してみましょう。

$ scalac sample0203.scala
$ scala sample0203
sum = 15062.699999999997
avg = 150.62699999999998
sd = 6.433472701426501
max = 167.9
min = 133.7

平均値は 150.6 で、標準偏差が 6.4 になりました。

●素数を求める

次は、100 以下の素数を求めるプログラムを作ってみましょう。いちばん簡単な方法は、奇数 3, 5, 7, 9, ... をそれまでに見つけた素数で割ってみることです。見つけた素数は配列に格納しておけばいいでしょう。プログラムは次のようになります。

リスト : 素数を求める

object prime {
  val N = 100
  val primeTable = new Array[Int](N)
  var primeSize = 0

  def isPrime(n: Int): Boolean = {
    for (i <- 0 until primeSize) {
      val p = primeTable(i)
      if (p * p > n) return true
      if (n % p == 0) return false
    }
    true
  }

  def printPrime(): Unit = {
    for (i <- 0 until primeSize) {
      print(primeTable(i))
      print(" ")
    }
  }

  def main(args: Array[String]): Unit = {
    primeTable(0) = 2
    primeSize += 1
    for (i <- 3 to N by 2) {
      if (isPrime(i)) {
        primeTable(primeSize) = i
        primeSize += 1
      }
    }
    printPrime()
  }
}
$ scalac prime.scala
$ scala prime
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

変数 primeTable は素数を格納する配列で、変数 primeSize は格納されている素数の個数を表します。関数 isPrime は引数 n が素数であれば true を、そうでなければ false を返します。

n が素数か判別する場合、実際には primeTable の素数をすべて調べる必要はなく、\(\sqrt n\) より小さい素数を調べるだけで十分です。素数を変数 p にセットします。\(p \gt \sqrt n\) のかわりに p * p > n をチェックし、真であれば return で true を返します。n % p == 0 であれば、素数で割り切れるので false を返します。

main では、最初に primeTable に 2 をセットして、primeSize の値を +1 します。あとは、for ループで 3 から始まる奇数列を生成し、変数 i が素数か isPrime でチェックします。そうであれば、primeTable に i を追加して、primeSize を +1 します。最後に関数 printPrime で素数を出力します。

●エラトステネスの篩

もうひとつ、素数を求める簡単な方法を紹介しましょう。最初に、2 から N までの整数列を生成します。先頭の 2 は素数なので、この整数列から 2 で割り切れる整数を取り除きます。2 で割り切れる整数が取り除かれたので、残った要素の先頭が素数になります。先頭要素は 3 になるので、今度は 3 で割り切れる整数を取り除きます。このように、素数を見つけたらそれで割り切れる整数を取り除いていくアルゴリズムを「エラトステネスの篩 (ふるい)」といいます。

プログラムは次のようになります。

リスト : エラトステネスの篩

object sieve {
  val N = 500
  val primes = new Array[Boolean](N / 2 + 1)

  def deleteMultiple(i: Int): Unit = {
    var j = i / 2 + i
    while (j < primes.size) {
      primes(j) = true
      j += i
    }
  }

  def printPrimes(): Unit = {
    print(2)
    print(" ")
    for (i <- 3 to N by 2){
      if (!primes(i / 2)) {
        print(i)
        print(" ")
      }
    }
  }

  def main(args: Array[String]): Unit = {
    var i = 3
    while (i * i <= N) {
      if (!primes(i / 2)) deleteMultiple(i)
      i += 2
    }
    printPrimes()
  }
}

Boolean 型の配列 primes で奇数列 (1, 3, 5, 7, ... ) を表します。false で素数を表し、素数でない場合は true に書き換えます。配列 primes は false で初期化されるので、最初はすべての数が素数ということになります。

奇数を変数 i とし、それに対応する配列 primes の添字を変数 j とすると、変数 i は 3, 5, 7, 9, ... に、それに対応する変数 j は 1, 2, 3, 4, ... になります。この場合、i の倍数に対応する j の値は j + i, j + i * 2, j + i * 3, ... になります。たとえば、3, 5, 7 の倍数は次のようになります。

i |  3  5  7  9 11 13 15 17 19 21 23 25
j |  1  2  3  4  5  6  7  8  9 10 11 12
--+-------------------------------------
3 |  O        0        O        0
5 |     0              0              0
7 |        0                    0

main で 3 から始まる奇数列を生成します。i が奇数を表します。奇数列の上限値は \(\sqrt N\) で十分です。この条件を i * i <= N で表しています。primes(i) が false の場合、i は素数です。i の倍数を関数 deleteMultiple で削除します。最後に printPrimes で、primes に残っている素数を表示します。

関数 deleteMultiple は簡単です。i に対応する primes の添字 j は i / 2 で求めることができます。あとは j + i, j + i * 2, j + i * 3, ... 番目の要素を true に書き換えます。実際には、j を i / 2 + i に初期化し、while ループで j に i を加算していくだけです。

関数 printPrimes も簡単です。最初に 2 を表示して、次の for ループで 3 から N までの奇数列を生成します。そして、primes(i / 2) が false ならば i は素数なので、print で i を表示するだけです。

それでは実行してみましょう。

$ scalac sieve.scala
$ scala sieve
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103
107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211
223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331
337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449
457 461 463 467 479 487 491 499

正常に動作していますね。

●値呼びと参照呼び

一般に、関数呼び出しには二つの方法があります。一つが「値呼び (call by value)」で、もう一つが「参照呼び (call by reference)」です。近代的なプログラミング言語では「値呼び」が主流です。

値呼びの概念はとても簡単です。

(1) 受け取るデータを格納する変数 (仮引数) を用意する。
(2) データを引数に代入する。
(3) 関数の実行終了後、引数を廃棄する。

値呼びのポイントは (2) です。データを引数に代入するとき、データのコピーが行われるのです。たとえば、変数 a の値が 10 の場合、関数 foo(a) を呼び出すと、実引数 a の値 10 が foo の仮引数にコピーされます。変数に格納されている値そのものを関数に渡すので、「値渡し」とか「値呼び」と呼ばれます。また、値呼びは任意の式の値を実引数として渡すことができます。たとえば foo(a + b) の場合、引数に渡された式 a + bを計算し、その結果が foo の仮引数に渡されます。

値呼びは単純でわかりやすいのですが、呼び出し先 (caller) から呼び出し元 (callee) の局所変数にアクセスできると便利な場合もあります。仮引数に対する更新が直ちに実引数にも及ぶような呼び出し方が「参照呼び」です。

Scala は「値呼び」です。数や Boolean などの基本的なデータは、仮引数にデータをセットするときデータのコピーが行われます。ただし、配列や文字列など一部のデータは、仮引数にデータをセットするとき、値のコピーは行われません。この場合、Scala の変数 (引数) はデータを格納しているのではなく、そのデータへの参照を格納しているのです。参照はC言語のポインタや Perl のリファレンスのことで、実態はデータ (オブジェクト) に割り当てられたメモリのアドレスです。次の図を見てください。

変数 a に文字列 "ab" を代入する場合、Scala は "ab" を a に書き込むのではありません。文字列 (オブジェクト) "ab" を生成して、図 (1) のようにオブジェクトへの参照を a に書き込みます。a の値を変数 b に代入する場合も、図 (2) のように a に格納されているオブジェクトへの参照を b に書き込むだけで、文字列はコピーされません。

したがって、配列や文字列の場合、代入はデータに名札を付ける操作と考えることができます。図 (2) のように、一つのデータに複数の名札を付けることもできるわけです。これは引数の場合も同じです。配列や文字列の場合、実引数に格納されている値はオブジェクトへの参照であり、それが仮引数にコピーされます。つまり、アドレスを値渡ししているわけです。

オブジェクトの同一性は演算子 eq で調べることができます。次の例を見てください。

scala> val a = new String("ab")
val a: String = ab

scala> val b = a
val b: String = ab

scala> val c = new String("ab")
val c: String = ab

scala> a eq b
val res0: Boolean = true

scala> a eq c
val res1: Boolean = false

scala> a == c
val res2: Boolean = true

new String("ab") は String 型のインスタンスを生成します。インスタンスはオブジェクトのことです。オブジェクト指向についていはあとで詳しく説明します。

変数 a と c には String のオブジェクトを代入し、変数 b に a の値を代入します。変数 a と b は同じオブジェクトを参照しているので、a eq b は true になります。a と c は異なるオブジェクトなので、値が同じ文字列 "ab" でも a eq c は false になります。値を比較したい場合は演算子 == を使います。

Java の場合、演算子 == は Scala の eq と同じ動作で、値を比較したい場合はメソッド equals を使います。Java と Scala では演算子 == の動作が異なるので注意してください。

ただし、配列のような更新可能なオブジェクトの場合、関数の引数に配列を渡してそれを破壊的に修正すると、呼び出し元の変数の値も書き換えられたかのようにみえます。次の例を見てください。

scala> def foo(ary: Array[Int]): Unit = ary(0) *= 10
def foo(ary: Array[Int]): Unit

scala> val a = Array(1,2,3,4,5)
val a: Array[Int] = Array(1, 2, 3, 4, 5)

scala> foo(a)

scala> a
val res4: Array[Int] = Array(10, 2, 3, 4, 5)

変数 a に配列 Array(1, 2, 3, 4, 5) をセットします。関数 foo() は配列 ary の先頭要素を 10 倍します。このとき、配列 a の内容を破壊的に修正していることに注意してください。foo(a) を呼び出したあと、配列 a の先頭要素が 10 に書き換えられていることがわかります。

この場合、変数 a の値が書き換えられたのではなく (変数 a は immutable なので値の書き換えはできません)、a が参照しているオブジェクトの内容を直接書き換えているだけなのです。元の値をそのままにしておきたい場合は、元の配列をコピーして新しい配列を生成してください。

●可変長引数

関数を定義するとき、仮引数の個数よりも多くの値を受け取りたい場合は、型の後ろに * を付けた仮引数を用意します。これを「可変長引数」といい、仮引数に入りきらない値は、配列 [*3] に格納されて可変長引数に渡されます。これで可変個の引数を受け取る関数を定義することができます。

簡単な例を示しましょう。

scala> def foo(a: Int, args: Int*): Unit = {
     | print(a)
     | print(" ")
     | print(args)
     | }
def foo(a: Int, args: Int*): Unit

scala> foo(1)
1 List()
scala> foo(1, 2)
1 ArraySeq(2)
scala> foo(1, 2, 3)
1 ArraySeq(2, 3)
scala> foo(1, 2, 3, 4, 5)
1 ArraySeq(2, 3, 4, 5)

scala> def foo0(args: Int*): Unit = println(args)
def foo0(args: Int*): Unit

scala> foo0()
List()

scala> foo0(1)
ArraySeq(1)

scala> foo0(1, 2)
ArraySeq(1, 2)

scala> foo0(1, 2, 3, 4, 5)
ArraySeq(1, 2, 3, 4, 5)

可変長引数は通常の仮引数よりも後ろに定義します。関数 foo は通常の引数が一つしかありません。foo(1) と呼び出すと、引数 a に 1 がセットされます。実引数はもうないので、仮引数 args には空のリスト List() が渡されます。リストは回を改めて詳しく説明します。次に foo(1, 2) と呼び出すと、実引数 2 が配列に格納されて仮引数 args に渡されます。同様に、foo(1, 2, 3) は 2 と 3 が配列に格納されて仮引数 args に渡されます。

関数 foo0 は、0 個以上の引数を受け取る関数、つまり、引数があってもなくてもどちらでも動作します。この場合、仮引数は args だけになります。実引数がない場合、引数 args には空のリスト List() が渡されます。もし、複数の引数があれば、それらを配列にまとめて仮引数 args に渡します。

可変長引数を持つ関数にデータを渡すとき、データが配列に格納されていると、配列から要素を取り出して渡さないといけません。これでは面倒なので、Scala には配列 [*4] を展開して要素を可変長引数に渡す機能が用意されています。次の例を見てください。

scala> val a = Array(1,2,3,4,5)
val a: Array[Int] = Array(1, 2, 3, 4, 5)

scala> foo0(a: _*)
            ^
       warning: Passing an explicit array value to a Scala varargs method is deprecated (since 2.13.0) 
       and will result in a defensive copy; Use the more efficient non-copying ArraySeq.unsafeWrapArray 
       or an explicit toIndexedSeq call
ArraySeq(1, 2, 3, 4, 5)

scala> foo0(a.toIndexedSeq: _*)
ArraySeq(1, 2, 3, 4, 5)

配列の要素を展開する場合、引数の後ろに : _* をつけます。これで配列の要素を取り出して可変長引数の関数に渡すことができます。ただし、ver 2.13 から mutable な配列を渡すと warning が表示されます。メソッド toIndexdSeq を使うと配列を immutable なデータ型に変換することができます。

-- note --------
[*3] 実際には ArraySeq というコレクションに格納されます。ArraySeq は Seq 型というデータ型を継承していて、配列を Seq 型のように使うことができます。ArraySeq は mutable と immutable の二種類がありますが、可変個引数に用いられる ArraySeq は immutable のほうです。
[*4] 実際は配列だけではなく Seq 型を継承している immutable なデータ型であれば、可変長引数の関数に渡すことができます。

●デフォルト引数

Scala の関数は引数にデフォルトの値を設定することができます。これを「デフォルト引数」といいます。値は = で指定します。

簡単な例を示しましょう。

scala> def foo(a: Int, b: Int = 10, c: Int = 100): Array[Int] = Array(a, b, c)
def foo(a: Int, b: Int, c: Int): Array[Int]

scala> foo(1)
val res0: Array[Int] = Array(1, 10, 100)

scala> foo(1, 2)
val res1: Array[Int] = Array(1, 2, 100)

scala> foo(1, 2, 3)
val res2: Array[Int] = Array(1, 2, 3)

関数 foo の引数 a は通常の引数で、引数 b と c がデフォルト値を指定した引数です。デフォルト引数は通常の引数の後ろに定義します。foo を呼び出すとき、引数 a には値を渡さないといけませんが、引数 b と c の値は省略することができます。このとき、使用される値がデフォルト値です。

たとえば、foo(1) と呼び出すと Array(1, 10, 100) が返され、引数 b と c の値はデフォルト値が使用されていることがわかります。foo(1, 2) と呼び出すと、引数 b の値はデフォルト値ではなく、実引数 2 が b の値になります。同様に、foo(1, 2, 3) と呼び出すと、仮引数 c の値は実引数 3 になるので Array(1, 2, 3) が返されます。

●名前付き引数

Scala は関数を呼び出すとき、仮引数名を指定して実引数を渡すことができます。これを「名前付き引数」といいます。引数の個数が多い関数は、その順番を覚えるのが大変です。名前付き引数は名前で引数を指定できるので、引数の順番を覚える必要はありません。つまり、引数の順番を変えることができるのです。次の例を見てください。

scala> def bar(a: Int, b: Int, c: Int): Array[Int] = Array(a, b, c)
def bar(a: Int, b: Int, c: Int): Array[Int]

scala> bar(1, c = 3, b = 2)
val res3: Array[Int] = Array(1, 2, 3)

scala> bar(c = 3, a = 1, b = 2)
val res4: Array[Int] = Array(1, 2, 3)

最初の例では、第 1 引数の 1 はそのまま引数 a に渡されます。第 2 引数 c = 3 と第 3 引数は名前付き引数なので、2 は引数 c に、3 は引数 b に渡されます。次の例のように、すべて名前付き引数で実引数を渡すこともできます。

名前付き引数はデフォルト引数と組み合わせると便利です。次の例を見てください。

scala>  def foo(a: Int, b: Int = 10, c: Int = 100): Array[Int] = Array(a, b, c)
def foo(a: Int, b: Int, c: Int): Array[Int]

scala> foo(1, c = 200)
val res5: Array[Int] = Array(1, 10, 200)

デフォルト引数 c の値を変更したい場合、名前付き引数を使わないと、foo(1, 10, 200) のように引数 b の値を指定しないといけませんが、名前付き引数で指定すると foo(1, c = 200) のように引数 b の指定を省略することができます。

今回はここまでです。次回は「再帰定義」と「高階関数」について説明します。


初版 2014 年 7 月 21 日
改訂 2021 年 3 月 7 日

Copyright (C) 2014-2021 Makoto Hiroi
All rights reserved.

[ PrevPage | Scala | NextPage ]