今回のエッセンスは、式は評価すると結果値を生成する
です。とても当たり前のように思えますが、Scalaではあらゆるところで、この考えが浸透しています。
式とブロック
式の例を見ていきましょう。
$ scala
// 真っ先に思い浮かべる式
scala> 1 + 1
res1: Int = 2
// 単一の値も式
scala> 1
res2: Int = 1
// よくある四則演算
scala> val x = (1 + 2) * 3 - 4 / 2
x: Int = 7
// 変数も式
scala> x
res3: Int = 7
// メソッド呼び出しも式
scala> x.toLong
res4: Long = 7
少し特殊な例を紹介しましょう。printlnなどの一見、値を返さなそうなメソッドがあります。しかしScalaではメソッド呼び出しは、式として判別されるため、意味のある結果を返さないUnit型として判断されます。
// これは、値を返しているのではなく、標準出力。
> println("hello")
hello
// :t を式の前に付けると結果型を調べる事ができる。
> :t println("hello")
Unit
中括弧で囲まれた1つ以上の式を ブロック(式) と呼びます。ブロックを評価すると、その中の全ての式と宣言が順に処理され、最後の式の結果値がブロック自身の結果値として返されます。
> :paste
// Entering paste mode (ctrl-D to finish)
{
val x = 1 + 1
val y = 2 + 2
x + y
}
// Exiting paste mode, now interpreting.
res4: Int = 6
式は評価され結果値を生成する。とても単純な考えですが、Scalaでは多くのところでこの考えが使われています。
制御構造と式
制御構造も式です。構文自体は新しいですが、式もしくは、ブロックを持った構造と考えれば単純な応用となります。制御構造は式を受け取り、自身も式として結果値を生成します。if式を見てみましょう。
> val x = 1
res: Int = 1
// 条件を判断して、"even"もしくは、"odd"という式を評価して結果値を生成する。
> if(x % 2 == 0) "even" else "odd"
res: String = "odd"
> :paste
// 式がブロック式に変わっただけ。最後に評価された式が結果値となる。
if(x % 2 == 0) {
"even"
} else {
"odd"
}
res: String = odd
続いてfor式
を見ていきましょう。少し変わった形ですが、ifと考え方は同じです。Listから取り出された値(x) と式の組み合わせに過ぎません。この例は、printlnを使ったので値を返さなかったのでしょうか?
> for(x <- List(1, 2, 3)) println(x)
1
2
3
このような形で記述しても、値を返すことはありません。やっていることは、JavaのforEachと同様になります。そのため、Unit型が返ることになります。
> :t for(x <- List(1, 2, 3)){ x * 2 }
Unit
forが値を返すのは、以下のようなケースです。yieldキーワードを利用してみましょう。このとき何故このような結果になるかは言及はしません。forは式を受け取り、自身も式として結果値を生成するということに意識を集中しましょう。
> for(x <- List(1, 2, 3)) yield x * 2
res: List[Int] = List(2, 4, 6)
case式を見てみましょう。現時点では、値を生成するswitch-caseという認識で構いません。
> val x = 3
x: Int = 3
> :paste
x match {
case 1 => "1"
case 2 => "2"
case 3 => "3"
case _ => "Other numbers"
}
res: String = 3
最後に制御構造ではありませんが、scalaのメソッド定義を見てみましょう。メソッドは、0個以上の引数と式の組という見方ができます。式は、単体の式かブロックの形で=
記号によって紐付けられています。そのため、メソッドも呼び出され評価されると結果値を生成します。
> def foo(x: Int): Int = x * 2
foo: (x: Int)Int
> foo(3)
res: Int = 6
> def bar(x: Int): String = {
if(x == 1) "1"
else "Other numbers"
}
> bar(3)
res: String = Other numbers
いろんな制御構造が式(ブロック)を受け取り、自身も結果値を返す式ということがわかりました。それでは、これがプログラミングにどのような影響を及ぼすかを見ていきましょう。
単体テスト
REPLで複数行記述するのも限界に近づいてきたので、sbtとエディタ・IDEを利用してScalaファイルを書いていきましょう。ついでに今まで書いてきた式は結果値を生成するので、標準出力をしながらではなくテストコードを書きながら学習を進めていきましょう(これを機にテストコードを書く習慣が身につくと一石二鳥ですね)。
$ sbt new scala/scala-seed.g8
Minimum Scala build.
name [My Something Project]: expression-and-value
Template applied in ./expression-and-value
$ cd expression-and-value
テンプレートには、あらかじめScalaTestというテストフレームワークが用意されています。テストコードを書くため、以下の2つのファイルを用意してみましょう。(初めに用意されているexampleパッケージは消してしまっても構いません。)
package expression
object Expression {
val one = 1
}
package expression
import org.scalatest.FunSuite
class ExpressionSpec extends FunSuite {
test("Expression::one should return 1") {
assert(Expression.one == 1)
}
}
2つのファイルが用意できたら、sbtでtestコマンドを実行してみましょう。緑色の文字で以下のように表示されれば成功です。
$ sbt
> test
[info] Compiling 1 Scala source to /Users/abab/Desktop/expression-and-value/target/scala-2.12/classes...
[info] Compiling 1 Scala source to /Users/abab/Desktop/expression-and-value/target/scala-2.12/test-classes...
[info] ExpressionSpec:
[info] - Function one should return 1
[info] Run completed in 253 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 6 s, completed 2017/02/22 9:36:07
今まで書いてきた式も同様にテストに掛けてみましょう。
package expression
object Expression {
val one = 1
def evenOdd(x: Int): String = {
if(x % 2 == 0) "even"
else "odd"
}
def multiList(mul: Int): List[Int] = {
for(x <- List(1, 2, 3, 4, 5)) yield x * mul
}
def oneTwoThree(x: Int): String = {
x match {
case 1 => "1"
case 2 => "2"
case 3 => "3"
case _ => "Other numbers"
}
}
}
テストコードは以下のようになります。
package expression
import org.scalatest.FunSuite
class ExpressionSpec extends FunSuite {
test("Function one should return 1") {
assert(Expression.one == 1)
}
test("""Expression::evenOdd should return "even" given 2""") {
assert(Expression.evenOdd(2) == "even")
}
test("""Expression::evenOdd should return "odd" given 3""") {
assert(Expression.evenOdd(3) == "odd")
}
test("Expression::multiList should return list of multiples of 2 given 2") {
assert(Expression.multiList(2) == List(2, 4, 6, 8, 10))
}
test("""Expression::oneTwoThree should return "1" given 1""") {
assert(Expression.oneTwoThree(1) == "1")
}
}
さらに追加情報ですがsbtの対話環境では、consoleコマンドでREPLを開き自身の定義したメソッドをimportして試すことができます。デバッグの際にお使いください。
$ sbt
> console
scala> import expression.Expression
scala> Expression.evenOdd(2)
result: String = even
今回は単独の式(関数)をテストしているだけですが、単純な式を組み合わせて複雑な式を作ってテストしていき、安全なプログラムを書くことができるのは関数型言語の強みです。単純なものからテストをしていく習慣を身に着けていきましょう。
文と式
今まで多くのScalaの機能に式の考えが使われていることを見てきました。ここでは、Javaについて見ていきましょう。Scalaでは式主体であったのに対して、Javaでは文主体です。
あるif文を持つJavaのメソッドを考えてみましょう。Scalaと比べてreturnキーワードがある分、冗長に見えます。
public int foo(int x){
if(x % 2 == 0){
return 0;
} else {
return 1;
}
}
この様な単純な場合、三項演算子を利用すると短く記述することができます。
public int foo(int x){
return x % 2 == 0 ? 0 : 1;
}
それでは、条件が3つ以上に増えた場合はどうでしょうか? やはりreturnキーワードが冗長です。
public String bar(int x){
if(x % 2 == 0){
return "2";
} else if(x % 3 == 0) {
return "3";
} else {
return "other";
}
}
それでは三項演算子を使ってみては、どうでしょうか?これは、少し書き方を工夫しても見づらい上にネストしたケースや単純な値ではなく、式(複数行に渡る文)を書きたい場合も難しくなってしまいます。
public String foo(int x){
return x % 2 == 0 ?
"2" :
x % 3 == 0 ?
"3" :
"other";
}
Javaでは、以下のように書くケースが多いと思われます。しかし、以下のコードには間違いが含まれています。コンパイラはresに初期値が代入されていなければエラーを起こします。代わりに何を入れるでしょうか? そうです、nullです。この手法で書いた場合、else節を書き忘れてしまえば、そのままnullが返却されてしまいNullPointerException
が発生してしまいます。
コメントでご指摘頂きました。以下のようなケースの場合、else節までしっかり書かれている場合は、null初期化の必要はありません。else節を書き忘れた場合は、初期化されていない可能性をコンパイラが教えてくれるため、そのときに無理にコンパイルを通そうとnullを代入するとNullPointerException
を起こしうるコードができてしまうので気をつけましょう。
public String bar(int x){
String res; // = null;
if(x % 2 == 0){
res = "2";
} else if(x % 3 == 0) {
res = "3";
} else { // else節を書き忘れたら・・・?
res = "other";
}
return res;
}
Scalaでの例を見てみましょう。構文としては、if-elseで書くことができ、さらにreturnキーワードが無いため、とても簡潔で可読性が高いです。
def bar(x: Int): String = {
if(x % 2 == 0){
"2"
} else if(x % 3 == 0){
"3"
}
}
さらにコンパイラはメソッドがStringの値を返すことをチェックします。そのため、コンパイル時に以下のようなエラーになり、とても安全に書くことができます。
$ scalac Bar.scala
Bar.scala:5: error: type mismatch;
found : Unit
required: String
} else if(x % 3 == 0){
^
one error found
もう一つ例を見てみましょう。Java8から導入されたStream APIです。言わずもがな関数型言語、おそらくScalaの影響も受けていると思います。
文字列のリストの要素をmapメソッドで角括弧[]で囲んだ文字に変換し、StringJoinerを使い、コロン: 区切りの文字列に変換している例です。ラムダ式は値を返す式であることがわかります。
Stream.of("John", "Alice", "Mike", "Mary").stream().map(name ->
"[" + name + "]").
collect(Collectors.joining(":"));
// [John]:[Alice]:[Mike]:[Mary]
しかし、Javaでは、波括弧{}で囲まれたブロックは、文を表すためreturnキーワードが必要になります。そのため式と文の頭の切り替えが必要になります。
(name -> {
String enclosedName = "[" + name + "]";
return enclosedName;
}
)
Scalaでは以下のように書きます。Scalaのラムダ式は、その名の通り式を書くためブロックで書いたとしても今まで通り値を返します。その他にも、List型
がmapメソッドを備えていたり、collectメソッドを使用する必要が無いため非常に簡潔に書くことができます。
List("John", "Alice", "Mike", "Mary").
map(name => s"[$name]").
mkString(":")
文主体でプログラムを書いた場合、冗長性・安全性・可読性のバランスを取るのが非常に難しいです。式主体の考え方は、これらの問題を綺麗に解決することができます。
演習
コードを書かなければ感覚を掴んだり覚えたりすることは難しい思います。演習問題を用意したので、問いてみましょう。別途資料を参照しなくても、本稿の知識のみで解けるはずです。
- case式でパターン漏れがあるとコンパイル時にどうなるか検証してみましょう。
- src/test/scala/expression/ExpressionSpec.scala で足らないテストケースを埋めてみましょう。
- 1 - N までのFizzBuzz文字列のリストを返す関数(式) fizzBuzzを2パターン定義して、テストを書いてみましょう。
- fizzBuzz(5) == List("1", "2", "fizz", "4", "buzz")
- ヒント: def, for, if, case
まとめ
多くの事柄について学んで来ましたが、一貫しているのは、式は評価すると結果値を生成する
ということだけです。このエッセンスだけで、
- 記述が簡潔になり可読性が上がる
- テストコードが書きやすくなる
- コンパイラが安全なコードを導いてくれる
こんなに多くの利点を得ることができました。Scalaを書く際には、式を強く意識してみましょう。