読者です 読者をやめる 読者になる 読者になる

空飛ぶ羊

勉強したり登壇したり勉強会参加したり

学習メモ:Scalaの制御構文 #Scala #dwango

この記事について

ドワンゴScala研修テキストを学習した時のメモです。

okoysm.hatenablog.jp

DOING

Scalaの制御構文

言葉の定義

ScalaはCやJavaなどの手続き型の言語に比べて、文よりも式になる構文が多いため、変数などの状態をできるだけ排除したわかりやすいコードが書きやすい。

構文 (Syntax)

  • そのプログラミング言語内でプログラムが構造を持つためのルール
    • プログラミング言語内で特別扱いされるキーワード(class, val, ifなど)
    • 正しいプログラムを国姓するためのルール(クラスの中身は{}で括られる)

式 (Expression)

  • プログラムを構成する部分のうち、評価することで値になるもの
    • 11+2は数値になり、"hoge"は文字列の値になる

文 (Statement)

  • プログラムを構成する部分のうち、評価しても値にならないもの(⇔式(Expression))
    • val i = 1は、定義全体としては値を持たない。(変数iは定義され,iの値は1になるけれど)

{}式

  • {}構文の一般形は {exp1; exp2; ... expN; }
  • ;は改行で区切られていれば省略できる。
  • exp1からexpNを順番に評価し、expNを評価した値を返す。

Scalaにおける一般的なメソッド定義

scala > def foo(): String = {
  "foo" + "foo"
}

if式

Javaとほぼ同じだったのでスキップ

練習問題

var age: Int = 5という年齢を定義する変数とvar isSchoolStarted: Boolean = falseという就学を開始しているかどうかという変数を利用して、 1歳から6歳までの就学以前の子どもの場合に“幼児です”と出力し、それ以外の場合は“幼児ではありません”と出力するコードを書いてみましょう。

自分の回答

var age: Int = 5
var isSchoolStarted: Boolean = false

if(1 <= age && 6 <= age && !isSchoolStarted){println("幼児です")}else{println("未熟児です")}

while式

Javaとほぼ同じだったのでスキップ

練習問題

do whileを利用して、0から数え上げて9まで出力して10になったらループを終了するメソッドloopFrom0To9を書いてみましょう。loopFrom0To9は次のような形になります。???の部分を埋めてください。

def loopFrom0To9(): Unit = {
  var i = ???
  do {
    ???
  } while(???)
}

自分の回答

i++みたいなのってScala使えないんですね・・・

def loopFrom0To9(): Unit = {
  var i = 0
  do {
    println(i)
    i += 1
  } while(i < 10)
}

for式

Javaの制御構文と似た使い方ができるものの、全く異なる構文です。for式の本当の力を理解するには、flatMap, map, withFilter, foreachといったメソッドについて知る必要がありますが、ここでは基本的なfor式の使い方のみを説明します。

とあるので、心してかかりたいと思います。

基本的な構文

for(ジェネレータ1;  ジェネレータ2; ... ジェネレータn) A
# ジェネレータ1 = a1 <- exp1; ジェネレータ2 = a2 <- exp2; ... ジェネレータn = an <- expn
  • ジェネレータって言ってるのが条件っぽい(a1 <- exp1など)
  • exp1〜expnには、ある数の範囲を表す式を使える
    • 1 to 10→1から10まで (10を含む)
    • 1 until 10 →1から10まで (10を含まない)
scala> for(x <- 1 to 5; y <- 1 until 5){
         println("x = " + x + " y = " + y)
       }
x = 1 y = 1
x = 1 y = 2
x = 1 y = 3
x = 1 y = 4
x = 2 y = 1
x = 2 y = 2
x = 2 y = 3
x = 2 y = 4
x = 3 y = 1
x = 3 y = 2
x = 3 y = 3
x = 3 y = 4
x = 4 y = 1
x = 4 y = 2
x = 4 y = 3
x = 4 y = 4
x = 5 y = 1
x = 5 y = 2
x = 5 y = 3
x = 5 y = 4

ループ変数の中から条件にあったものだけを絞りこめる

以下の例だと x!=y が追加されたことによってx, y同じ値のものが表示されなくなっています。

scala> for(x <- 1 to 5; y <- 1 until 5 if x != y){
         println("x = " + x + " y = " + y)
       }
x = 1 y = 2
x = 1 y = 3
x = 1 y = 4
x = 2 y = 1
x = 2 y = 3
x = 2 y = 4
x = 3 y = 1
x = 3 y = 2
x = 3 y = 4
x = 4 y = 1
x = 4 y = 2
x = 4 y = 3
x = 5 y = 1
x = 5 y = 2
x = 5 y = 3
x = 5 y = 4

コレクションの要素を1つ1つ辿って処理を行うために使うことも可能

例えばListの中身の文字列をすべて出力する処理

scala> for(e <- List("A", "B", "C", "D", "E")) println(e)
A
B
C
D
E

コレクションの要素を辿って、加工して新しいコレクションを作成できる

この場合ジェネレータで指定したリストの要素の前にPreが付いた要素を持つリストが新しく作成されています。

scala> for(e <- List("A", "B", "C", "D", "E")) yield {
     |   "Pre" + e
     | }
res9: List[String] = List(PreA, PreB, PreC, PreD, PreE)

yieldキーワードを使ったfor式はfor式ではない

  • for-comprehensionと呼ぶ事がある
  • yieldキーワードを使うことで、コレクションの要素を加工して返すという異なる用途に使える

練習問題

1から1000までの3つの整数a, b, cについて、三辺からなる三角形が直角三角形になるような a, b, cの組み合わせを全て出力してください。直角三角形の条件にはピタゴラスの定理を利用してください。 ピタゴラスの定理とは三平方の定理とも呼ばれ、a ^ 2 == b ^ 2 + c ^ 2を満たす、a, b, c の長さの三辺を持つ三角形は、直角三角形になるというものです。

自分の回答

^が使えなくて微妙にハマった・・・

for (a <- 1 to 1000; b <- 1 to 1000; c <- 1 to 1000) if (a * a == b * b + c * c) {
  println("a=" + a + ", b=" + b + ", c=" + c)
}

模範解答と違ったところ

for文の中にif文を入れ込むかfor文とif文を分けるか。 模範解答はfor (a <- 1 to 1000; b <- 1 to 1000; c <- 1 to 1000 if a * a == b * b + c * c)
自分の回答はfor (a <- 1 to 1000; b <- 1 to 1000; c <- 1 to 1000) if (a * a == b * b + c * c)

match式

match式は使い方によって非常に幅のある制御構造です

基本構文

  • Javaのswitch-caseみたいなやつ
  • Scalaはbreakしなくていい(フォールスルー動作がない)
  • _はdefaultのようなもの(ワイルドカードパターン)
    • match式を使う際は漏れがないように、ワイルドカードパターンを利用することが多い。
マッチ対象の式 match {
  case パターン1 [if ガード1] => 式1
  case パターン2 [if ガード2] => 式2
  case パターン3 [if ガード3] => 式3
  case ...
  case パターンN => 式N
  case _       => "nothing"
}

パターンをまとめる

Javaのswitch-caseのときはフォールスルーさせていたけど、Scalaの場合はOR(|)でつなげられる。

"abc" match {
  case "abc" | "def" =>
    println("first")
    println("second")
}

パターンマッチによる値の取り出し

switch-case以外の使い方として、コレクションの要素の一部にマッチさせる使い方もある。
下の場合、「リストの先頭要素が"A"で5要素」のパターンにマッチすると残りが表示され、しなければ"nothing"が表示される。

scala> val lst = List("A", "B", "C", "D", "E")
lst: List[String] = List(A, B, C, D, E)

scala> lst match {
     |   case List("A", b, c, d, e) =>
     |     println("b = " + b)
     |     println("c = " + c)
     |     println("d = " + d)
     |     println("e = " + e)
     |   case _ =>
     |     println("nothing")
     | }
b = B
c = C
d = D
e = E

+ガード式なパターンマッチもできる

さっきのプログラムにガード式(今回の場合if b != "B")を追加すると"nothing"が表示されるようになる。

scala> val lst = List("A", "B", "C", "D", "E")
lst: List[String] = List(A, B, C, D, E)

scala> lst match {
     |   case List("A", b, c, d, e) if b != "B" =>
     |     println("b = " + b)
     |     println("c = " + c)
     |     println("d = " + d)
     |     println("e = " + e)
     |   case _ =>
     |     println("nothing")
     | }
nothing

パターンマッチのパターンのネストが可能

  • Listを要素として持つListをパターンマッチすることもできる
  • パターンの前に@がついているのはasパターンと呼ぶ
    • @の後に続くパターンにマッチする式を@の前の変数(今回の場合a)に束縛する。
  • このプログラムの場合パラメータがListで先頭要素がList("A")で2要素もつ場合matchし、List(A)ともう一つの要素が表示されている
scala> val lst = List(List("A"), List("B", "C", "D", "E"))
lst: List[List[String]] = List(List(A), List(B, C, D, E))

scala> lst match {
     |   case List(a@List("A"), x) =>
     |   println(a)
     |   println(x)
     |   case _ => println("nothing")
     | }
List(A)
List(B, C, D, E)

パターンマッチの場合、値をとりだすことはできない

  • パターンマッチで使った変数を使った場合、コンパイルエラーになる。
  • 下のerror patternの場合変数aを指定しているためコンパイルエラーになる
scala> (List("a"): Any) match {
     |   case List(a) | Some(a) =>
     |     println(a)
     | }
<console>:14: error: illegal variable in pattern alternative
         case List(a) | Some(a) =>
                   ^
<console>:14: error: illegal variable in pattern alternative
         case List(a) | Some(a) =>
                             ^

値を取り出さないパターンマッチは可能。ただこれいつ使うんだろう。型を見たい時とかだろうか・・・

(List("a"): Any) match {
  case List(_) | Some(_) =>
    println("ok")
}

型によるパターンマッチ

  • Scalaではcaseに値ではなく型を指定することも可能
  • 特定の型に所属する場合にのみマッチするパターンは名前:マッチする型と書く
  • AnyRef=JavaでいうObject型(なんでも格納できる)
  • 例外処理やequalsの定義などで使うことがある
  • しばしばキャストの代わりにパターンマッチが用いられるので覚えておくこと

このプログラムの場合AnyRefがStringにマッチしているので、すべてを大文字にした結果が表示されています。

scala> import java.util.Locale
import java.util.Locale

scala> val obj: AnyRef = "String Literal"
obj: AnyRef = String Literal

scala> obj match {
     |   case v:java.lang.Integer =>
     |     println("Integer!")
     |   case v:String =>
     |     println(v.toUpperCase(Locale.ENGLISH))
     | }
STRING LITERAL

JVMの制約による型のパターンマッチの落とし穴

  • 型変数を使った場合、正しくパターンマッチが行われない。
  • このプログラムだとcaseにList[Int]とList[String]を書いているが、パターンマッチでは区別できない
    • このプログラムの場合Scalaコンパイラの「型消去」という動作によりIntの部分が消され、ListであればすべてList[Int]に到達してしまう
scala> val obj: Any = List("a")
obj: Any = List(a)

scala> obj match {
     |   case v: List[Int]    => println("List[Int]")
     |   case v: List[String] => println("List[String]")
     | }
<console>:16: warning: non-variable type argument Int in type pattern List[Int] (the underlying of List[Int]) is unchecked since it is eliminated by erasure
         case v: List[Int]    => println("List[Int]")
                 ^
<console>:17: warning: non-variable type argument String in type pattern List[String] (the underlying of List[String]) is unchecked since it is eliminated by erasure
         case v: List[String] => println("List[String]")
                 ^
<console>:17: warning: unreachable code
         case v: List[String] => println("List[String]")
                                        ^
List[Int]

型変数を含む方のパターンマッチはワイルドカードパターンを使うと良い

obj match {
  case v: List[_] => println("List[_]")
}

練習問題

new scala.util.Random(new java.security.SecureRandom()).alphanumeric.take(5).toList

以上のコードを利用して、 最初と最後の文字が同じ英数字であるランダムな5文字の文字列を1000回出力してください。 new scala.util.Random(new java.security.SecureRandom()).alphanumeric.take(5).toListという値は、呼びだす度にランダムな5個の文字(Char型)のリストを与えます。なお、以上のコードで生成されたリストの一部分を利用するだけでよく、最初と最後の文字が同じ英数字であるリストになるまで試行を続ける必要はありません。これは、List(a, b, d, e, f)が得られた場合に、List(a, b, d, e, a)のようにしても良いということです。

自分の回答

うまくいかずギブアップ・・・

var i = 0

while(i <= 1000){
  val lst: List[Char] = new scala.util.Random(new java.security.SecureRandom()).alphanumeric.take(5).toList

  lst match {
    case List(a, b, c, d, e) if a == e =>
      i += 1
      println(lst)
  }
}
  

模範解答

問題文の

なお、以上のコードで生成されたリストの一部分を利用するだけでよく、最初と最後の文字が同じ英数字であるリストになるまで試行を続ける必要はありません。これは、List(a, b, d, e, f)が得られた場合に、List(a, b, d, e, a)のようにしても良いということです。

これを実装すると実にすっきりします。あとforかwhileどっちで回すといいとかあるのかな。

for(i <- 1 to 1000) {
  val s = new scala.util.Random(new java.security.SecureRandom()).alphanumeric.take(5).toList match {
    case List(a,b,c,d,_) => List(a,b,c,d,a).mkString
  }
  println(s)
}

この章での疑問点

if文の順序

age => 1 && age <=6 && !isSchoolStartedだとNGで
1 <= age && 6 <= age && !isSchoolStartedだとコンパイルが通るのはなぜだろうか・・・

値を取り出さないパターンマッチの使い所

  • 値を取り出さないパターンマッチは可能なのはわかったけど、これいつ使うんだろう。

進捗

DONE

TODO

  • クラス
  • オブジェクト
  • トレイト
  • 型パラメータと変位指定
  • 関数
  • Scalaのコレクションライブラリ
  • ケースクラスとパターンマッチング
  • エラー処理
  • Implicit
  • 型クラスの紹介
  • FutureとPromise
  • テスト
  • Javaとの相互運用
  • S99の案内
  • トレイトの応用編:依存性の注入によるリファクタリング

次回はクラスをやります。次回はいつだろう・・・年内にドワンゴテキスト終わらせたい。