Skip to content

Instantly share code, notes, and snippets.

@bati11 bati11/0_learn_scala.md
Last active Dec 31, 2016

Embed
What would you like to do?
こちらの勉強会で使った資料です -> http://sscala.connpass.com/

Scala入門ハンズオンの資料

こちらの勉強会で使った資料です -> http://sscala.connpass.com/

Scala入門ハンズオン

Scalaのバージョンは 2.11.1 でやりました。

Day1

  • val
  • 演算子
  • メソッド
  • if式
  • リスト
  • レンジ
  • for式
  • タプル
  • 自分でメソッドつくる

Day2

  • パターンマッチ
  • String interpolation
  • 再帰
  • クラス
  • 継承
  • ケースクラス

Day3

  • 型パラメータ入門
  • 関数
  • リストの高階メソッド
  • カリー化
  • 畳み込み
  • Option
  • Map
  • 暗黙の型変換

Day4

  • Scalaの階層構造
  • パッケージとアクセス修飾子
  • シングルトンオブジェクトとコンパニオンオブジェクト
  • トレイト
  • 型パラメータの境界
  • コレクション
  • 暗黙の引数
  • ScalaのAPIドキュメントを読んでみる
  • 例外

Day1

REPL

REPLを使ってScalaを使ってみましょう!

REPLとは、対話型評価環境のことで、Read Eval Print Loop の頭文字をとってREPLと呼びます。

REPLは、ターミナルでscalaコマンドで起動させます。そうするとプロンプトがこうなります。

scala>

止めるときは scala> :q です。

val

変数に数値を代入してみましょう。変数宣言にはvalを使います。

scala > val x = 10
x: Int = 10

x: Int = 10と出力がありますが、これは「xという変数はInt型で、値が10です」という意味になります。

Int型という言葉が出てきました。Scalaでは値に型があります。型が合わない計算はエラーになります。例えば、Int型の値には文字列との+は定義されているので動作しますが、文字列との-は定義されていないためエラーになります。

scala> val y = x + "aaa"
y: String = 10aaa

scala> val z = x - "aaa"
<console>:8: error: overloaded method value - with alternatives:
  (x: Double)Double <and>
  (x: Float)Float <and>
  (x: Long)Long <and>
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int
 cannot be applied to (String)
       val z = x - "aaa"
                 ^

型についてはいずれもうちょっと詳しく。。

valで宣言した変数に値を再度代入してみましょう。エラーが起きますか?

scala> x = 20

そうです、エラーになります。valで宣言した変数に値を代入した後、再度値を代入することはできません。

ただし、REPL上ではvalを使って同じ変数名で宣言し直すことができます。あくまでもREPL上だけです。REPLで同じ変数名が使えないのは不便ですからね。

次は適当に数値だけ入力してみましょう。

scala> 11
res1: Int = 11

res1: Int = 11というのは、先ほどのx: Int = 10と同様に解釈できます。REPLでは入力した式を評価した結果が自動的にresXという変数に代入されます。

scala> res1 + 2
res2: Int = 13

明示的に変数の型を書くこともできます。逆に言うと書かなくてもできる限り適切な型を自動でつけてくれます。この機能のことを型推論と呼びます。そのため開発者は常に型を書くわずらわしさから解放されます。

scala> val y: String = 10 + "aaa"
res3: String = 10aaa

文字列の操作

"で囲むと文字列リテラルになります。

scala> "abc"
res4: String = "abc"

Scalaの文字列は、Javaのjava.lang.Stringを使います。ScalaではJavaのクラスを使うことができます。このことは、Javaの資産が使えるためScalaの良いところである反面、Javaをやったことない人にはわかりづらくなる要因な気もします。。。

java.lang.Stringのメソッドをいくつか呼び出してみましょう。参考: String (Java Platform SE 8)

scala> "abc".startsWith("a")
scala> "abc".startsWith("b")
scala> "abcdefg".substring(2,5)
scala> "abc".length()
scala> "abc".length

最後の例では、lengthメソッドを()なしで呼び出しています。引数をとらないメソッドを呼び出すときは()を省略して呼び出すことができます。

数値同士の演算

演算子、+, -, *, /, % を使って数値計算してみましょう。%は剰余を求めます。括弧を使って計算の優先順位も変えれます。

scala> 1 + 2
scala> 5 - 9
scala> 2 * 3 + 4
scala> 2 * (3 + 4)
scala> 12 / 2 % 4

これらの演算子は、実はInt型であるオブジェクトのメソッドです。そのため、1 + 2というのは1.+(2)と同じ意味になります。

まず、Scalaでは全ての値はオブジェクトです。そのため、1に対してメソッド呼び出しができます。1.+(2)というように+メソッドを呼び出しています。

そして、Scalaではメソッド呼び出しの.を省略できます。1 + (2)になります。

さらに、単一の引数をとるメソッドの()が省略できます。1 + 2になります。これでメソッド呼び出しを演算子のように扱うことができました。

オブジェクトとは、JavaやRubyで言うところのオブジェクトと同じです。クラスがあってインスタンス化する、っていうアレです。なのでScalaにもクラスがあって、それをインスタンス化できます。クラスについては後の方でやります。

数値同士の比較

比較演算子もメソッドとして定義されています。>, <, >=, <=, ==, !=を適当に使ってみましょう。Booleanには論理演算子&&, ||が定義されています。

scala> 21 < 50
scala> 21 >= 50
scala> 101 == 100 + 1
scala> 101 != 100 + 1
scala> (21 < 50) && (101 != 100 + 1)
scala> 21 < 50 && 101 != 100 + 1
scala> (21 < 50) || (101 != 100 + 1)

if式

他の言語のifと同じように使えます。Scalaのifは式なので値を返します。

scala> val s = if (21 < 50) "hoge" else "fuga"
s: String = "hoge"

scala> val s =
     |   if (21 < 50) {
     |     "hoge"
     |   } else {
     |     "fuga"
     |   }
s: String = "hoge"

Unit型

先ほどのif式のelseを省略することもできます。しかし、さっきとはちょっと違うことが起きます。

scala> val s = if (21 < 50) "hoge"
s: Any = hoge

変数sの型がStringではなくAnyになってしまいました。さっきのif式の条件の比較演算子を逆にしてみましょう。

scala> val s = if (21 > 50) "hoge"
s: Any = ()

変数sの値が()になってます!これはなんでしょうか?

()はユニットと呼ばれる値でJavaでいうところのvoidです。Scalaでは副作用のある処理の返り値に使われます。副作用とは何かの状態を変える処理です。オブジェクトの状態を変える、DBに対して操作をする、コンソールに文字列を出力するなどです。

コンソールに文字列を出力するときはprintlnが使えます。

scala> println("Hello")
Hello

scala> val x = println("Hello")
Hello
x: Unit = ()

printlnの返りをxに代入しました。xの値、つまりprintlnの返り値が()になっています。そして()の型はUnitであることが分かります。

話をval s = if (21 < 50) "hoge"に戻します。このif式は条件がtrueなのかfalseなのかによって、"hoge"というString型の値、もしくは、()というUnit型の値のどちらかを返します。このときStringとUnitは別の型であるため、どちらの型の値でも大丈夫なように、変数sの型はAnyという型になってしまいます。こうなるともはやsをStringとして扱うことはできません。

scala> val s = if (21 < 50) "hoge"
s: Any = ()

scala> s.length
<console>:9: error: value length is not a member of Any
              s.length
                ^

このような挙動は望んでいるものではないでしょう。そのためif式の返り値を使いたい場合は基本的にelseも書くようにします。

valで変数宣言をするときに型を明示しておくと、変数が予想外にAnyになってしまうことを防ぐことができます。こうしておくと、sを使うときではなくsに値を代入するときにエラーを発生させることができるので、よりエラーの原因を特定しやすくなります。デバッグ時などに型を明示的に指定することで予想と異なる型になってしまっている箇所を探したりできます。

scala> val s: String = if (21 < 50) "hoge"
<console>:7: error: type mismatch;
 found   : Unit
 required: String
       val s: String = if (21 < 50) "hoge"
                       ^

リスト

リストには同じ型の値を複数個格納できます。リストはListという型で表されます。同じ型じゃない、例えば数値と文字列、両方を格納することもできちゃいますが、やらない方がいいでしょう。

scala> List(1, 2, 3, 4, 5)
res5: List[Int] = List(1, 2, 3, 4, 5)

scala> List("a", "b", "c")
res6: List[String] = List(a, b, c)

scala> List(List(1,2,3), List(4,5,6), List(7,8,9))
res7: List[List[Int]] = List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))

xs: List[Int]というのは、変数xsはList[Int]型であるという意味です。要素としてInt型の値を持ってるリストであるということが分かります。

List同士は、++で連結できます。連結元のリストの要素が増えるわけではなく、連結した新しいリストをつくって返します。valで宣言した変数と同様に、一度つくったリストの中身を書き換えることはできません。このように中身を書き換えることができないオブジェクトのことをイミュータブルであると言います。逆に状態を変えれることをミュータブルなオブジェクトと言います。Scalaでは、イミュータブルなオブジェクトとvalによる変数を使うことで副作用のある操作を制限できるようにしています。

scala> val xs = List(1,2,3)
scala> val ys = List(4,5,6)
scala> val zs = xs ++ ys
scala> xs
scala> ys

updatedメソッドを使うと指定した箇所の要素の値を更新できますが、この場合も元のリストを上書きするわけではなく、値を更新した新しいリストを返します。

scala> val xs = List(1,2,3)
scala> val ys = xs.updated(2, 1)
scala> xs

リストの要素を取得するにはapplyメソッドを使います。applyメソッドはちょっと特殊で、()のみでapplyメソッドを呼び出すことができます。

scala> xs.apply(2)
res8: Int = 3
scala> xs(2)
res9: Int = 3

cons

リストをつくるには別の方法もあります。その1つが::メソッドを使う方法です。

scala> Nil.::(3)
res10: List[Int] = List(3)

Nilというのは空のリスト表します。空のリストの::メソッドを使って要素を増やしました。もう1つ増やしてみます。

scala> Nil.::(3).::(2)
res11: List[Int] = List(2,3)

また要素が増えました。よく見るとリストの最後ではなく最初に追加されてます。::メソッドは最後ではなく先頭に要素を追加します。ちょっと直感的ではないように感じますが通常はこういう風には書かず、もう少し分かりやすく書き直して使います。

Scalaではメソッド名の末尾が:であるメソッドをピリオドを使わずに演算子として使う場合、右結合になります。つまりこう書けます。

scala> 2 :: 3 :: Nil
res12: List[Int] = List(2,3)

見やすくなりましたね。::メソッドは:で終わってるので右結合になり、さっきのNil.::(3).::(2)と全く同じ意味になります。ちなみに::をcons演算子と呼びます。

リストの最後に要素を追加したい場合は:+メソッドを使います。ただし注意が必要です。理由は後の方で説明しますが、:+++は、要素の追加に要する時間がリストのサイズに比例して増えて行きます。リストに要素を1つずつ追加する処理が何回も行われる場合は、逆順のリストに対して::で追加し、最後にreverseで順番を逆にする方がいいです。

リストの比較

==, != で比較できます。

scala> List(1,2,3) == List(1,2,3)
scala> List(1,2,3) == List(2,3,4)
scala> List(1,2,3) == List(1,2,3,4)

リストのメソッド

Listの要素を先頭の要素を取得するときapplyメソッドを使ってxs(0)と書かなくてもheadメソッドが使えます。ただし注意が必要です。

scala> xs.head()
<console>:9: error: Int does not take parameters
              xs.head()
                     ^

scala> xs.head

なんと()をつけて呼び出すとエラーになってしまいました。引数をとらないメソッドを呼び出すときは()を省略できると先ほど言いましたが、今回は省略しないといけないです。

引数なしのメソッドにおける省略について、以下の2パターンがあることになります。

  • hogeと書けるしhoge()とも書ける
  • hogeとは書けるがhoge()とは書けない

なぜでしょうか?

なぜその様な特殊なルールになっているか、というと、理想としては

  • 副作用を伴わないメソッド(getterみたいなやつ)は括弧を付けない
  • 副作用を伴うメソッドは括弧をつける

という方向にしたいからです。

単純に定義時に () つけたメソッドは省略不可、定義時に () つけなかったメソッドは () の記述不可 としてしまうと、Java のライブラリを呼び出すときに整合性がつかなくなるので、 Javaとの互換を意識して一見すると妙なルールになっています。

[引用元 : @gakuzzzz さんのScalaの省略ルール早覚え]

ということで、Scalaでは副作用のない引数なしのメソッドを呼び出すときは()をつけずに呼び出します。逆に副作用のあるメソッドを呼び出すときは()をつけます。

headだけでなく他のメソッドも使ってみましょう。

scala> val xs = List(1,2,3,4,5)
scala> xs.head
scala> xs.tail
scala> xs.isEmpty
scala> xs.length
scala> xs.last
scala> xs.init
scala> xs.take(2)
scala> xs.drop(3)
scala> xs.max
scala> xs.sum
scala> xs.product
scala> xs.contains(3)
scala> xs.contains(10)
scala> xs.reverse
scala> List(List(1,2,3), List(4,5,6), List(7,8,9)).flatten

Range

1から20までのリストをつくります。全部書いてもいいですが、Inttoメソッドを使うともっとスマートにできます。

scala> List(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)
res13: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

scala> (1 to 20).toList
res14: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

(1 to 20)は何を返しているのでしょうか?

scala> 1 to 20
res15: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

Inclusiveという型でした。これはRange、つまり値の範囲を表していて、リストのように複数の値を持っています。InclusiveListで使ったheadtailtakeなどが使えます。

ListInclusiveの他にもScalaには複数の値を持つためのデータ構造が色々用意されています。このようなデータ構造をコレクションと呼びます。ほとんど同じように使えますが、内部の実装が異なっておりデータ構造に対して行いたい操作が一番高速になるようなデータ構造を選ぶことが大切です。

こちらのドキュメントが参考になります。Collections - 性能特性 - Scala Documentation

Listの内部構造やその他のコレクションについてはそのうち詳しくやります。

レンジをもうちょっと使ってみましょう。1文字を表すCharにもtoメソッドがあります。

scala> 2 to 20 by 2
res16: scala.collection.immutable.Range = Range(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

scala> 10 to 0 by -1
res17: scala.collection.immutable.Range = Range(10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)

scala> (13 to 13 * 24 by 13).toList
res18: List[Int] = List(13, 26, 39, 52, 65, 78, 91, 104, 117, 130, 143, 156, 169, 182, 195, 208, 221, 234, 247, 260, 273, 286, 299, 312)

scala> 'a' to 'z'
res19: scala.collection.immutable.NumericRange.Inclusive[Char] = NumericRange(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z)

for式

for式を使うとコレクションに対する繰り返し処理を行うことができます。

コレクションの変換、あるコレクションから別のコレクションをつくることはよくありますよね。1から10までのコレクションから、各要素を2倍したコレクションをつくってみましょう。

scala> val xs = 1 to 10
xs: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

scala> val ys = for(x <- xs) yield x * 2
ys: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

型がIndexedSeq[Int]となっていますが、これもコレクションの一種です。今は深く追わないことにします。

x <- xsは、「xsから取り出した各要素の値をxが受け取る」という意味です。x <- xsの部分をジェネレータと呼びます。

yield x * 2の部分はfor式の出力になります。取り出した各要素をどうやって変換するかを指定します。上の例では、各要素を2倍する、と指定しています。

for式にフィルタを追加することもできます。フィルタを使うことでジェネレータで生成する要素の数を減らすことができます。フィルタの条件を満たす値だけを元のコレクションから取り出すことができるのです。

フィルタはジェネレータのところにifを使って書きます。このifは先ほどやったif式とは異なるものです。ジェネレータのところに書いたifはあくまでフィルタの役目で、条件分岐ではありません。

さっきの例に、2倍した結果が12より大きくなるものだけを取り出すようにしてみます。

scala> for (x <- 1 to 10 if x * 2 > 12) yield x * 2
res20: scala.collection.immutable.IndexedSeq[Int] = Vector(14, 16, 18, 20)

複数のコレクションから値を取り出すこともできます。2重ループに相当する処理ですね。

scala> for (x <- List(1,2,3); y <- List(10, 100, 1000)) yield x + y
res21: List[Int] = List(11, 101, 1001, 12, 102, 1002, 13, 103, 1003)

横に長くなるのが気になるのであれば、改行しても良いです。

scala> for (x <- List(1,2,3);
     |      y <- List(10, 100, 1000))
     |   yield x + y
res22: List[Int] = List(11, 101, 1001, 12, 102, 1002, 13, 103, 1003)

()の代わりに{}を使うこともできます。この場合ジェネレータの後ろの;を書かなくてよくなります。

scala> for {
     |   x <- List(1,2,3)
     |   y <- List(10, 100, 1000)
     | } yield x + y
res22: List[Int] = List(11, 101, 1001, 12, 102, 1002, 13, 103, 1003)

コレクションの変換ではなく、コレクションの各要素に対する繰り返し処理でもfor式は使えます。例えば各要素をコンソールに出力するような処理です。この場合のようにfor式が値を返す必要がない場合、yieldは不要です。for式は()を返します。

scala> for (s <- List("Apple", "Banana")) {
     |   println(s)
     | }
Apple
Banana

タプル

コレクションは1つの型の値を複数格納するのに便利。タプルは複数の違う型の値を格納するのに便利です。また、任意の数を格納できるコレクションとは異なり、タプルは要素数が固定になります。

scala> val pair1 = (1, 3)
pair1: (Int, Int) = (1,3)

scala> val pair2 = (1, 'a')
pair2: (Int, Char) = (1,a)

pair1pair2は両方と要素数が2のタプルであることには変わりはないですが、型は(Int, Int)(Int, Char)で異なっています。

タプルはメソッドから複数の値を返すのに便利です。またシンプルにデータを表現するのにも便利です。

例えば、2次元ベクトルを表すのにリストを使ってしまうと、List(1, 2)List(4,2,3)とが同じList[Int]型になってしまい、2次元ベクトルと3次元ベクトルの違いが分かりません。タプルを使えば、(1, 2)(4, 2, 3)とは、型が(Int, Int)(Int, Int, Int)になり異なるため取り扱いを間違えることはありません。

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

scala> val vector1: (Int, Int) = (1, 2)
scala> val vector2: (Int, Int) = (4, 2, 3)
<console>:7: error: type mismatch;
 found   : (Int, Int, Int)
 required: (Int, Int)
       val vector2: (Int, Int) = (4, 2, 3)
                                 ^

要素を取得するときは_1です。

scala> (1, 3)._1
res23: Int = 1

scala> (1, 3, 7, 9)._3
res24: Int = 7

要素数が2のタプルをペアと呼んだりもします。ペアは2つのリストをくっつけてペアのリストにしたり、リストに添字をつけてペアのリストにしたり、という場面でも便利です。

scala> List(5, 5, 6, 6, 7, 7) zip List("abc", "def", "ghi", "jkl", "mno", "pqr")
res25: List[(Int, String)] = List((5,abc), (5,def), (6,ghi), (6,jkl), (7,mno), (7,pqr))

scala> List("abc", "def", "ghi", "jkl", "mno", "pqr").zipWithIndex
res26: List[(String, Int)] = List((abc,0), (def,1), (ghi,2), (jkl,3), (mno,4), (pqr,5))

メソッド

これまでIntやString, Listのメソッドを呼び出して使ってきました。REPL上で自分で新しくメソッドを定義することができるます。

メソッドというのはオブジェクトに属するものです。オブジェクトなしで単独では存在できません。

しかし、REPLでは特別に何らかのオブジェクトに属する形じゃなくてもメソッドを定義できます(REPLに属している、というかグローバルなオブジェクトに属してると考えてもいいかも・・・)。

では定義してみましょう。引数に指定した数値を2倍にして返すメソッドです。メソッドを定義するときはdefキーワードを使います。続いてメソッド名、()の中に引数、返り値の型、=、メソッドの本体です。

scala> def twice(x: Int): Int = x * 2
twice: (x: Int)Int

scala> twice(10)
res27: Int = 20

メソッド本体が複数行になる場合は{}で囲みます。最後の行の結果がメソッドの結果になります。明示的にreturnのようなものを書く必要はありません。

scala> def plusOneTwice(x: Int): Int = {
     |   val n = x + 1
     |   n * 2
     | }
plusOneTwice: (x: Int)Int

scala> plusOneTwice(10)
res28: Int = 22

メソッドが副作用を起こす場合、返り値の型がUnitになります。Unitを返すメソッドは慣例として返り値の型と=は省略して書きます。

scala> def printTwice(x: Int) {
     |   println(x * 2)
     | }
printTwice: (x: Int)Unit

scala> printTwice(10)
20

副作用を起こす処理とそうではない処理は別のメソッドとして、メソッドの返り値の型で副作用を起こすメソッドなのかどうかが判断できるようにしておくことが大事です。副作用を起こす処理が局所化されていると、メソッドを使う側としてはプログラミングが簡単になりますし、テストもしやすくなります。

副作用を起こさず、かつ、引数をとらない場合、引数リストを省略しましょう。()つきで呼び出すことができなくなるので、副作用が起こさないメソッドを呼び出していると後でコードを呼んだときに分かりやすくなります。

scala> def twiceTen: Int = 10 * 2
twiceTen: Int

scala> twiceTen
res29: Int = 20

scala> twiceTen()
<console>:9: error: Int does not take parameters
              twiceTen()
                      ^

ここでファイルに定義したScalaコードをREPLに読み込んでみましょう。negate.scalaというファイルをつくり、以下を書いてください。

def negate(x: Int): Int = - x

これをREPLに読み込むには、scala> :loadを使います。

scala> :load negate.scala
scala> negate(15)
res30 : Int = -15

negateメソッドをちょっと書き換えてみます。

def negate(x: Int): Int = "hogehoge"

返り値がIntのメソッドで"hogehoge"というStringを返すようにしてみました。

scala> :load negate.scala
Loading negate.scala...
<console>:7: error: type mismatch;
 found   : String("hogehoge")
 required: Int
       def negate(x: Int): Int = "hogehoge"
                                 ^

エラーになりました。Scalaは静的型づけなので、メソッドの実行時ではなく、コードをREPLに読み込んだ時点で型によるエラーを検知できます。REPLを使用していない場合はコンパイル時にエラーを検出できます。これは実行時より早くエラーを検出できることに加えて、型によるエラーがないことをコンパイラが保証してくれることになります。この例はシンプル過ぎてありがたみがないですが、大きなコードになっても同じようにコンパイラがチェックしてくれるのは大変助かります。コップ本にはこう書いてあります。

静的型システムは単体テストの代わりになるわけではないが、プログラムの性質を確認するために普通なら必要だった単体テストの数を減らしてくれる。さらに、単体テストは静的型付けの代わりにはならない。Edsger Dijkstra(エドガー・ダイクストラ)が言ったように、テストが証明できるのはエラー存在であって、エラーの不在ではない。静的型付けが与えてくれる保証は単純なものかもしれないが、どれだけテストをしたとしても得られない本物の保証なのである。

[引用: Scalaスケーラブルプログラミング]

練習問題

  1. List(1,2,3)とList(10,100,1000)の各要素を掛け合わせた結果からなるコレクションをつくるfor式を書いてください
  2. 掛け合わせた結果が50以上になるコレクションをつくるfor式を書いてください。
  3. 100までの偶数の中で、7で割った余りが3になる数値からなるコレクションをつくってください。
  4. List("Apple", "Banana", "Pineapple", "Lemon") から、各要素の頭文字からなるコレクションをつくってください。
  5. FizzBuzzを書いてください。
  6. 要素数の異なるリストをzipでつなげるとどうなりますか?
  7. 次のすべての条件を満たす直角三角形を見つけるメソッドを書いてください。直角三角形はタプルで表現してください。
    • 3辺の長さはすべて整数である
    • 各辺それぞれの長さはメソッドの引数で渡された数値以下である
    • 周囲の長さは24に等しい

ヒント:まずは各辺が10以下になるすべての三角形をつくってみましょう。その後、直角三角形となる条件、ピタゴラスの定理が成り立つ三角形だけを抽出してみましょう。

※ 7つめの問題は、すごいHaskellたのしく学ぼう!からの引用しました。

今日出てきたキーワード

  • REPL
  • val
  • イミュータブル
  • if式
  • () Unit型
  • 副作用
  • 型推論
  • リスト
  • :: cons
  • Nil
  • Range
  • for式
  • ジェネレータ
  • yield
  • コレクション
  • タプル
  • メソッド
  • 静的型付け

要注意ポイント

  • 全ての値がオブジェクト
  • 演算子もメソッド
  • メソッド呼び出しの . は省略できる
  • 引数が1つのメソッドの場合、メソッド呼び出しの () は省略できる
  • メソッド名の末尾が : の場合、演算子として使うと右結合になる
  • Listに対する末尾への要素追加は、リストサイズに比例した時間がかかる
  • applyメソッドは省略して()のみで呼び出すことができる
  • 引数なしで副作用がないメソッドを呼ぶ場合、()はつけない
  • 引数なしで副作用がないメソッドを定義する場合、()はつけない
  • 副作用のある・なしを意識してメソッドを定義することを心がける

参考資料

Day2

パターンマッチの初歩

Day1でif式をやりましたが、パターンマッチを使うと条件分岐をよりスマートに書けることがあります。パターンマッチにはmatch式を使います。

scala> val x = 7
scala> x match {
     |   case 1 => "Great!"
     |   case 2 => "Good!"
     |   case _ => "Sorry"
     | }
res0: String = Sorry

case 1 => というのが、xの値が1だった場合 => の右側の式を評価(実行)しますよ、という意味になります。case 2 => というのは、xの値が2だった場合、case _ => というのは、それ以外の場合に => の右側の式を評価しますよ、ということになります。matchも式なので値を返します。上の例だと、xの値は1でも2でもないので"Sorry"という文字列が返ってますね。

パターンマッチはリストにも使えます。条件分岐しつつリストの先頭要素と残りのリストを変数に束縛することもできます。よく分からないかもしれませんが、コードを見れば分かると思います。

リストの先頭要素を取得するheadメソッドをパターンマッチを使ってつくってみましょう。

// head.scala
def head(list: List[Int]): Int = {
  list match {
    case Nil     => throw new IllegalArgumentException
    case x :: xs => x
  }
}

Nilは空のリストでしたね。case Nillistが空のリストだったら、という意味です。このheadメソッドではエラーを発生させています(例外はいずれ・・・)。

case x :: xslistが空じゃない場合にマッチしますが、マッチさせると同時に変数xに先頭要素を、変数xsに先頭を取り除いた残りのリストを代入します。

::は前回cons演算子のところで値とリストをつなげるときに使っていました。パターンマッチのときでも同じような意味になります。case x :: xsというのは、パターンマッチの対象(list)がxxsをcons(::)でつなげたものだったら、という意味になるわけです。xsは空のリストでもそうでなくても構いません、どちらにしろconsでつなげることができますからね。

case x :: xs => というのは以下のように書いた場合と同じことをしてくれてると考えれば分かるのではないでしょうか。

case _ => {
  val x  = list.head
  val xs = list.tail
}

REPLに読み込んで使ってみます。

scala> :load head.scala

scala> head(List(1,2,3))
res1: Int = 1

scala> head(List(5))
res2: Int = 5

scala> head(List())
java.lang.IllegalArgumentException
  at .head(<console>:9)
  ... 32 elided

ちゃんと動いてます。

xs=>の右側で使っていないので、_で置き換えてしまいましょう。置き換えなくてもいいですが、_で置き換えることで先頭要素以外は使ってないんだな、と分かりやすくなります。

def head(list: List[Int]): Int = {
  list match {
    case Nil    => throw new IllegalArgumentException
    case x :: _ => x
  }
}

パターンマッチを使って条件分岐しつつ変数に値を代入することができました。パターンマッチに限りませんが変数に値を代入することを「束縛する」と言うこともできます。

headメソッドは結局1つのmatch式で出来上がってます。1つの式からなるメソッドの場合、外側の{}はいらないので消してしまいましょう。

def head(list: List[Int]): Int = list match {
  case Nil => throw new IllegalArgumentException
  case x :: _ => x
}

パターンマッチはタプルにも使えます。条件分岐しつつタプルの要素を変数に束縛することもできます。

例えば、3要素のタプルがあり、1つめの要素が動物の種類、2つ目の要素が名前、3つ目の要素が年齢だとします。

scala> val a = ("dog", "papico", 3)

そして、種別が犬("doc")の場合とクモ("spider")の場合とそれ以外で処理を分けたいとき、こう書けます。

def f(x: (String, String, Int)): String = x match {
  case ("dog",    name, age)  => "I like dog! " + name + " is " + age + " years old!"
  case ("spider", name, _)    => "I don't like spider. Sorry " + name
  case _                      => "I have no interest"
}
scala> val a = ("dog", "papico", 3)
scala> f(a)
res0: String = I like dog! papico is 3 years old!

scala> f(("spider", "papico", 3))
res1: String = I don't like spider. Sorry papico

scala> f(("cat", "papico", 3))
res2: String = I have no interest

これをif式で書くとすると、条件式を書くところでx._1 == "dog"って書いて、本体のところでもx._1って書くことになって、ちょっと面倒だしスマートじゃない感じがします。タプルでパターンマッチを使って分岐すれば、場合分けしつつタプルの中身の値を変数に代入できることが分かりました。

タプルとリストを組み合わせることだってできます。さっきつくったfメソッドを改良してみましょう。年齢の後ろに好きな食べ物をリストで持たせます。

def f(x: (String, String, Int, List[String])): String = x match {
  case ("dog",    name, age, Nil)       => "I like dog! " + name + " is " + age + " years old!"
  case ("dog",    name, age, food :: _) => "I like dog! " + name + " is " + age + " years old! " + name + " likes " + food + "!"
  case ("spider", name, _, _)           => "I don't like spider. Sorry " + name
  case _                                => "I have no interest"
}
scala> f(("dog", "papico", 3, List("meet", "fish")))
res6: String = I like dog! papico is 3 years old!papico like meet!

これの何が便利かって入れ子になってるリストにもパターンマッチできるところです。入れ子になってるケースクラスももちろん対応できます。こういった分岐をif式だけで書くと複雑になってしまうでしょう。

String interpolation

さっきのfメソッドですが、文字列結合の部分がちょっと面倒でしたよね。これはString interpolationと呼ばれる仕組みできれいに書けます。いわゆる変数展開みたいに使えます。文字列リテラルの先頭に"s"という書きます。リテラル内で変数を使うときは $変数名 です。

def f(x: (String, String, Int, List[String])): String = x match {
  case ("dog",    name, age, Nil)       => s"I like dog! $name is $age years old!"
  case ("dog",    name, age, food :: _) => s"I like dog! $name is $age years old! $name likes $food !"
  case ("spider", name, _, _)           => s"I don't like spider. Sorry $name"
  case _                                => s"I have no interest"
}

String interpolation は変数展開よりも色々できるのですが、ここではやりません。こちらが参考になります -> 文字列の補間

再帰

これまで変数宣言のときにvalを使っていました。変数に一度値を代入したらもう二度と代入することはできませんでした。

Scalaではvalの他にvarで変数宣言をすることもできます。varで宣言した変数には何度でも値を代入することができてしまいます。

Intのリストに格納されている数値の中から一番大きな値を取得するメソッドを考えてみましょう。

def maximum(xs: List[Int]): Int = {
  if (xs.size == 0) throw new IllegalArgumentException
  var max = 0
  for(x <- xs) {
    if (max < x) max = x
  }
  max
}
scala> val xs = List(3, 6, 1, 7, 2, 5)
scala> maximum(xs)

varを使ってmaximumメソッドを実装できました。しかし、副作用のない処理を書くには再代入可能な変数は邪魔になることが多く、また予期しないところ代入してしまってバグとなってしまったりします。理由がない限りはvarではなくvalを使うべきでしょう。

では、maximumメソッドのようなループ処理をvarを使わずに定義するにはどうしたらいいでしょうか?再帰を使いましょう。

再帰とはメソッド内で自分自身を呼び出すことです。下のmaximumメソッドでは、メソッド本体で自分自身であるmaximumメソッドを呼び出しています。

def maximum(list: List[Int]): Int = list match {
  case Nil      => throw new IllegalArgumentException
  case x :: Nil => x
  case x :: xs  => {
    val y = maximum(xs)
    if (x > y) x else y
  }
}

再帰を使うことでvarをなくしつつループ処理を実現できました。慣れるまでは1ステップずつじっくりと変数の値の変化をメモしながらトレースするといいと思います。

再帰を書くにはコツがあります。

再帰を使う際の定跡は、まず基底部を見極め、次に解くべき問題をより小さな部分問題へと分割する方法を考えることです。基底部と部分問題さえ正しく選んだなら、全体として何が起こるかの詳細を考える必要はありません。部分問題の解が正しいという保証をもとに、より大きな最終問題の解を構築すればよいだけです。

[引用元 : すごいHaskll楽しく学ぼう!]

リストの中に特定の要素が含まれているかを調べるメソッドを再帰で書いてみます。基底部を見極め、そして部分問題へと分割します。基底部は、Nilの場合です。部分問題への分割はリストのパターンマッチを使いましょう。

def contains(list: List[Int], a: Int): Boolean = list match {
  case Nil     => false
  case x :: xs =>
    if (x == a) true
    else        contains(xs, a)
}

実際に使ってみましょう。イメージが掴みづらい場合は簡単な例から1ステップずつイメージしていくと分かりやすいです。

scala> contains(1, List())
scala> contains(1, List(1))
scala> contains(1, List(2,3))
scala> contains(1, List(2,3,1))

階乗の計算も再帰で書いてみましょう。BigIntは大きな数値でも扱うことができる型です。

def fact(n: Int): BigInt = n match {
  case 0 => 1
  case _ => n * fact(n - 1)
}

クイックソートを書いてみましょう。リストの要素のどれか1つをピボットとします。ピボットよりも小さい値のリストと大きい値のリストに分けてそれぞれソートします。それぞれソートした結果とピボットを結合すればソート済みのリストが手に入ります。簡単にするためにピボットは先頭要素で固定としましょう。

def quickSort(list: List[Int]): List[Int] = list match {
  case Nil      => Nil
  case x :: Nil => List(x)
  case x :: xs  => {
    val smallerOrEqual = for (y <- xs; if y <= x) yield y
    val larger         = for (y <- xs; if y > x ) yield y
    quickSort(smallerOrEqual) ++ List(x) ++ quickSort(larger)
  }
}

どうでしょうか。やりたいことをそのまま宣言的に書けたのではないでしょうか。部分問題への分割とリストのパターンマッチが相性抜群ですね。ループではどうやって計算するかという手続きを書くことになりますが、再帰とパターンマッチを使うとやりたいことを宣言的に書けることが多いです。

末尾再帰

再帰を使うとループ処理と比べて宣言的に書くことができ、かつ、再代入可能な変数がないため、間違えにくく読みやすいコードになることが多いです。しかし、ループと比べてメソッド呼び出しの回数が増えます。これはパフォーマンスの劣化につながりますし、大量のメソッド呼び出しによりエラー(スタックオーバーフロー)が発生する可能性もあります。

実際、factに大きな値を指定するとスタックオーバーフローが起きます。エラーが起きる数値は環境によって異なるので小さな値から徐々に大きくしてみてください。

scala> fact(10000)
java.lang.StackOverflowError

再帰処理を「末尾再帰」で書くことにより、この問題を回避できます。末尾再帰とは、関数の最後で自分自身を呼び出す再帰のことです。末尾再帰はコンパイル時にコンパイラによってループ処理に置き換えられるため、問題を回避できるのです。

今まで出てきた例の中では、containsメソッドが末尾再帰です。それ以外は末尾再帰ではないです。factメソッドも末尾再帰に見えますが、最後に評価されるのは*であるため末尾再帰ではありません。

単純なループで書けるものは変数を追加することでただの再帰を末尾再帰に置き換えることができます。factメソッドを末尾再帰の形に書き換えてみましょう。

def fact2(n: Int, acc: BigInt): BigInt = n match {
  case 0 => acc
  case _ => fact2(n - 1, acc * n)
}

accという変数が増えました。これはアキュムレータ(蓄積変数)と呼ばれます。元々のfactメソッドの場合はn * fact(n - 1)というように再帰呼び出しの結果を掛け合わせていました。fact2メソッドの場合はfact2(n - 1, acc * n)というようにアキュムレータに掛け合わせてから再帰呼び出しをしています。これによって関数の最後で再帰呼び出しをすることに、つまり末尾再帰になりました。

fact2メソッドは末尾再帰なのでコンパイル時にループ処理に置き換えられるためスタックオーバーフローが発生しません。また、ループよりもパフォーマンスが悪かったりすることもありません。

ただ、引数が増えたことにより、メソッドを使う側がちょっと使いづらくなってしまいましたね。fact2メソッドを使う人は適切なアキュムレータの初期値を指定しないといけません。fact2メソッドの場合は1です。

scala> fact(10)
scala> fact2(10, 1)

アキュムレータの初期値はメソッドの性質によってことなります。掛け合わせる場合は1ですが、足し合わせる場合は0が適切です。このような判断をメソッドを使う側が考えるのは使い勝手が悪いですし、間違えてしまうかもしれません。なので以下のようにfactからfact2を呼び出すようにします。

( 以下のコードをREPLで:load使って読み込んでもうまく動きません。:loadの読み込みは1行ずつ評価するためfactメソッドが定義されたタイミングではfact2メソッドは存在していない、と判断されてしまいます。:loadの代わりに、:paste -rawで読み込むとこの問題を解決できます)。

def fact(n: Int): BigInt = fact2(n, 1)

def fact2(n: Int, acc: BigInt): BigInt = n match {
  case 0 => acc
  case _ => fact2(n - 1, acc * n)
}

これでメソッドを使う側は、factメソッドを使っている限りアキュムレータを意識する必要はありません。さらにfactメソッドの内部では末尾再帰のfact2メソッドを呼び出しているのでスタックオーバーフローが発生したり、ループに比べてパフォーマンスが劣るということもありません。

まだ不十分です。fact2メソッドを直接呼び出してしまうかもしれません。さらに、末尾再帰なメソッドを定義するたびに余分なメソッドが増えてしまいます。メソッドを使う側にはfact2メソッドの存在が分からない方がいいでしょう。これを実現する方法は3つあります。

1つ目は、private修飾子を使う方法です。JavaやRubyでもおなじみの方法です。privateをつけたメソッドは外部から呼び出せなくなります

def fact(n: Int): BigInt = fact2(n, 1)

private def fact2(n: Int, acc: BigInt): BigInt = n match {
  case 0 => acc
  case _ => fact2(n - 1, acc * n)
}

2つ目は、メソッド内にメソッドを定義する方法です。defの中でdefを使えるのです。メソッド内で定義されたメソッドはprivate扱いになり外部から呼び出すことはできません。

def fact(n: Int): BigInt = {
  def loop(n: Int, acc: BigInt): BigInt = n match {
    case 0 => acc
    case _ => loop(n - 1, acc * n)
  }
  loop(n, 1)
}

3つ目は、引数のデフォルト値を使う方法です。Scalaでは引数が未指定の場合のデフォルト値を設定できます。ただ、この方法では間違ったアキュムレータの初期値を与えることができる状態なので好ましくないでしょう。

def fact(n: Int, acc: BigInt = 1): BigInt = n match {
  case 0 => acc
  case _ => fact(n - 1, acc * n)
}

maximumメソッドも末尾再帰に書き直してみましょう。

def maximum(list: List[Int]): Int = {
  def loop(list: List[Int], acc: Int): Int = list match {
    case Nil      => throw new IllegalArgumentException
    case x :: Nil => if (x > acc) x else acc
    case x :: xs  => loop(xs, if (x > acc) x else acc)
  }
  loop(list, 0)
}

クラス

以前、2次元ベクトルを表すのにタプルを使いました。長方形を表すにはどうしたらいいでしょうか?Intを4つ持つタプルを使うのもいいですが、自分で長方形を表す型を定義してしまう方がより良いでしょう。

自分で型をつくってみましょう。型を定義する方法はいくつかありますが、クラスを使う方法を見ていきます。

まず、クラスを定義するときはclassを使います。インスタンス化するときはnewを使います。Rectangleクラスをインスタンス化してつくったオブジェクトの型はRectangle型になります。

scala> class Rectangle
scala> val x = new Rectangle

Rectangleクラスをいろいろいじっていくので、Shape.scalaに書いて使うときはREPLにロードして使うことにします。

長方形を左下の座標と右上の座標で表すことにしましょう。コンストラクタで指定できるようにします。コンストラクタの引数は、クラス名の右側に書きます。

// Rectangle.scala
class Rectangle(x1: Double, y1: Double, x2: Double, y2: Double)
scala> :load Rectangle.scala
scala> val a = new Rectangle(0,0,2,3)
a: Rectangle = Rectangle@7cef4e59

REPLの出力が何やら分かりづらいですね。REPLの出力ではオブジェクトのtoStringメソッドが呼ばれます。なので、RectangleクラスにもtoStringメソッドを追加しましょう。

// Rectangle.scala
class Rectangle(x1: Double, y1: Double, x2: Double, y2: Double) {
  def toString = s"Rectangle($x1, $y1, $x2, $y2)"
}

REPLに読み込んでみましょう。

scala> :load Rectangle.scala
Loading Rectangle.scala...
<console>:8: error: overriding method toString in class Object of type ()String;
 method toString needs `override' modifier
         def toString: String = s"Rectangle($x1, $y1, $x2, $y2)"
             ^

コンパイルエラーになってしまいました。"method toString needs override modifier" と表示されています。ScalaではすべてのクラスはAnyクラスを継承しています。そしてAnyクラスでtoStringメソッドが定義されているため、RectangleクラスでtoStringメソッドを定義するとオーバーライドすることになります。Scalaではオーバーライドするときはoverridedefに左側に書く必要があります。

class Rectangle(x1: Double, y1: Double, x2: Double, y2: Double) {
  override def toString = s"Rectangle($x1, $y1, $x2, $y2)"
}

これで無事読み込むことができます。REPLの出力も分かりやすくなりました。

scala> :load Rectangle.scala
scala> val a = new Rectangle(0,0,2,3)
a: Rectangle = Rectangle(0.0, 0.0, 2.0, 3.0)

メソッドだけでなくメンバー変数を定義することもできます。

class Rectangle(ax: Double, ay: Double, bx: Double, by: Double) {
  val x1 = ax
  val y1 = ay
  val x2 = bx
  val y2 = by
  override def toString = s"Rectangle($x1, $y1, $x2, $y2)"
}

上のように単純にコンストラクタのパラメータを公開したいだけの場合は、コンストラクタのパラメータにvalをつけることで、外部からパラメータにアクセスできるようになります。

valじゃなくてvarも使えますが、理由がない限りvalにしましょう。varにしてしまうとオブジェクトがミュータブルになってしまい扱いづらくなります。

class Rectangle(val x1: Double, val y1: Double, val x2: Double, val y2: Double) {
  override def toString = s"Rectangle($x1, $y1, $x2, $y2)"
}

外部からメンバ変数にアクセスする場合は、メソッド呼び出しと同様に.の後にメンバ変数名を指定するだけです。

scala> val a = new Rectangle(0,0,2,3)
a: Rectangle = Rectangle(0.0, 0.0, 2.0, 3.0)

scala> a.x1
res6: Double = 0.0

面積を求めるメソッドを追加してみましょう。副作用のないメソッドなのでメソッド定義時に()はつけない方がいいでしょう。

class Rectangle(x1: Double, y1: Double, x2: Double, y2: Double) {
  override def toString = s"Rectangle($x1, $y1, $x2, $y2)"
  def area: Double = math.abs(x2 - x1) * math.abs(y2 - y1)
}

Rectangleという型をクラスを使ってつくることができました。自分でつくった型のリストをつくったりももちろんできます。Rectangle型のリストを受け取って面積の合計を返すメソッドをつくって試してみましょう。

scala> def sumArea(list: List[Rectangle]): Double = {
     |   def loop(list: List[Rectangle], acc: Double): Double = list match {
     |     case Nil     => acc
     |     case x :: xs => loop(xs, acc + x.area)
     |   }
     |   loop(xs, 0)
     | }
scala> val xs = List(Rectangle(0, 0, 2, 2), Rectangle(0, 0, 3, 3), Rectangle(0, 0, 2, 4))
scala> sumArea(xs)

継承

さて、長方形だけでなく円もつくりたくなりました。しかもsumAreaメソッドでは長方形と円と両方を受け取って面積の合計を返したいです。こういう場合は、長方形と円を抽象化して図形として扱えるようにすればいいです。JavaやRubyでも継承ってものがありますが、Scalaにもあります。

抽象化して抽出された図形という型はインスタンス化はする必要がないので、abstractというキーワードをclassの前につけます。abstractがついたクラスを抽象クラスと呼び、インスタンス化することはできません。図形をShapeという型で表すことにします。そして、RectangleクラスとCircleクラスは、Shapeクラスを継承します。継承はextendsキーワードを使います。

抽象クラスを継承することにより、Rectangle型とCircle型は、Shape型のサブ型となります。逆に、Shape型は、Rectangle型とCircle型のスーパー型となります。

abstract class Shape {
  def area: Double
}

class Rectangle(val x1: Double, val y1: Double, val x2: Double, val y2: Double) extends Shape {
  override def toString = s"Rectangle($x1, $y1, $x2, $y2)"
  override def area = math.abs(x2 - x1) * math.abs(y2 - y1)
}

class Circle(val x: Double, val y: Double, val r: Double) extends Shape {
  override def toString = s"Circle($x, $y, $r)"
  override def area = r * r * math.Pi
}

Rectangleクラスは先ほどと同じように、CircleクラスもRectangleクラスと同じように使えます。インスタンス化してつくったオブジェクトはそれぞれ、Rectangle型とCircle型になります。

scala> val rec = new Rectangle(0, 0, 2, 2)
scala> val circle = new Circle(2, 4, 3)
scala> circle.x
scala> circle.r

Rectangle型とCircle型はShape型のサブ型なので、Shape型のオブジェクトとして扱うこともできます。

scala> def sumArea(list: List[Shape]): Double = {
     |   def loop(list: List[Shape], acc: Double): Double = list match {
     |     case Nil     => acc
     |     case x :: xs => loop(xs, x.area + acc)
     |   }
     |   loop(list, 0)
     | }
scala> val ss = List(rec, circle, new Rectangle(0, 0, 2, 4))
scala> sumArea(ss)

ケースクラス

クラスを使って長方形や円を表す型をつくってみました。クラスを使わずにケースクラスというものを使って型をつくることもできます。しかも、ケースクラスを使った場合は、toStringや==メソッドなどを自動でつくってくれます。さらに、パターンマッチで使うこともできます。

ケースクラスを使うには、classと書いたところをcase classにするだけです。toStringも削除してしまいましょう。あとはコンストラクタ引数のvalも不要です。

abstract class Shape {
  def area: Double
}

case class Rectangle(x1: Double, y1: Double, x2: Double, y2: Double) extends Shape {
  def area: Double = math.abs(x2 - x1) * math.abs(y2 - y1)
}

case class Circle(x: Double, y: Double, r: Double) extends Shape {
  def area: Double = r * r * math.pi
}

使ってみましょう。ケースクラスをインスタンス化して、Rectangle型のオブジェクトを手に入れるにはnewを使うのではなく、applyメソッドを呼び出します。applyメソッドは省略できるんでしたね。

scala> :load Shape.scala
scala> val rec = Rectangle.apply(1, 2, 5, 6)
scala> val rec = Rectangle(1, 2, 5, 6)
scala> rec.x1
scala> rec == Rectangle(1, 2, 5, 6)
scala> rec == Rectangle(3, 2, 5, 6)
scala> val circle = new Circle(2, 4, 3)
scala> val ss = List(rec, circle, Rectangle(0, 0, 2, 4))
scala> sumArea(ss)

ケースクラスはパターンマッチできます。条件分岐しつつ変数束縛するってやつです。

scala> def f(x: Shape): String = x match {
     |   case Circle(0.0, 0.0, _) => s"Center is origin. Area is ${x.area}."
     |   case _: Circle           => "this is circle."
     |   case _: Rectangle        => "this is rectangle."
     | }
scala> f(rec)
scala> f(circle)
scala> f(Circle(0,0,5))

sealed

ケースクラスの場合だけに限った話ではないですが、パターンマッチするときはパターンに漏れがないようにすることが大事です。これは結構大変なことです。例えば三角形をShape型のサブ型として追加した場合、Shape型でmatch式している箇所全体が影響してしまいます。

例えば、先ほどのfメソッドからRectangle型にマッチする1行を削除して定義し直して使ってみましょう。

scala> def f(x: Shape): String = x match {
     |   case Circle(0.0, 0.0, _) => s"Center is origin. area is ${x.area}."
     |   case _: Circle => "this is circle."
     | }
f: (x: Shape)String

scala> f(Circle(0, 0, 5))
res10: String = Center is origin. area is 78.53981633974483.

scala> f(Rectangle(0, 0, 3, 4))
scala.MatchError: Rectangle(0.0,0.0,3.0,4.0) (of class Rectangle)
  at .f(<console>:10)
  ... 32 elided

パターンから漏れていたRectangle型を指定するとエラーが発生しました。実行時エラーです。

この問題に対応するのがsealed修飾子です。sealedabstract classの前に書くことで、パターンマッチに漏れがある場合にコンパイラが警告を出してくれます。

では、Shape型にsealedをつけてみます。

sealed abstract class Shape {
   ・・・

もう一度fメソッドをRectangle型にはマッチしない形で定義してみます。

scala> def f(x: Shape): String = x match {
     |   case Circle(0.0, 0.0, r) => s"Center is origin. radius is $r."
     |   case _: Circle => "this is circle."
     | }
<console>:10: warning: match may not be exhaustive.
It would fail on the following input: Rectangle(_, _, _, _)
       def f(x: Shape): String = x match {
                                 ^
f: (x: Shape)String

コンパイラが警告を出してくれます。これは非常に助かります。ケースクラスが継承するabstract classにはsealedをつけた方がいいでしょう。

ケースクラスはクラスと比べて非常に便利です。データを表すようなクラスを作る場合は基本的にケースクラスとsealedを使うといいです。

ケースクラスを使って定義された型、RectangleやCircleは代数的データ型で言うところの直積型になります。そしてsealedを使って定義された型、Shape型は直和型なります。ケースクラスで定義されたクラスはProductというクラスを自動的に継承するのですが、このProductというのは直積型を表しているようです。代数的データ型については深堀りできないのでしませんが(知識が足りなくてできません・・・)、調べてみるとおもしろいと思います。

this

図形を受け取り、自分の面積を比較して大きい方を返すメソッドをつくってみましょう。Scalaでは自分自身を表すオブジェクトが必要なときは、thisを使います。

abstract class Shape {
  def area: Double
  def max(a: Shape): Shape = if (a.area > area) a else this
}

練習問題

  1. 数値のリストを受け取って全ての要素の合計を返すsumメソッドを再帰を使って書いてください。

  2. sumメソッドを末尾再帰で書いてください。

  3. ソート済みの数値のリストから2分探索をするメソッドを書いてください。以下のようなシグニチャのメソッドです。

    def binarySearch(list: List[Int], a: Int): Boolean
  4. 2分探索木をつくってみましょう。注意点があります。REPLに読み込むときは、:loadではなく:pasteで読み込んでください(:loadだとファイル単位でなく1行ずつ評価するため、この後の問題を解いてるとどこかでエラーになると思います)。

    sealed abstract class Tree
    case class Empty() extend Tree
    case class Node(a: Int, left: Tree, right: Tree) extend Tree

    REPLから使う場合は、scala> Empty()というように使います。

    2分探索木に値を追加するinsertメソッドをつくりましょう。insertメソッドを使うことで次のように2分探索木をつくることができます。scala> Empty().insert(2).insert(5)

    sealed abstract class Tree {
      def insert(x: Int): Tree
    }
  5. 上でつくった2分探索木に、指定された値が含まれているかを探すメソッドをつくりましょう。例えば次のような場合はtrueが返ってくるはずです。scala> Empty().insert(2).insert(5).contains(2)

    sealed abstract case class Tree {
      def insert(x: Int): Tree
      def contains(x: Int): Boolean
    }
  6. 上でつくった2分探索木に、指定された値を持つノードを削除するメソッドをつくりましょう。ノードを削除するときのアルゴリズムについては調べてください。

    sealed abstract case class Tree {
      def insert(x: Int): Tree
      def contains(x: Int): Boolean
      def remove(x: Int): Tree
    }

今日出てきたキーワード

  • match式、パターンマッチ
  • String interpolation
  • var
  • 再帰
  • 末尾再帰
  • アキュムレータ(蓄積変数)
  • private修飾子
  • 引数のデフォルト値
  • クラス
  • override
  • abstractextends
  • ケースクラス
  • 代数的データ型

要注意ポイント

  • パターンマッチで条件分岐しつつ変数束縛ができる
  • String interpolationを使うと文字列結合をスッキリ書ける
  • ループ処理を再帰で書くとvarをなくせる
  • 再帰はスタックオーバーフローが発生する危険があり、ループで書いたときよりパフォーマンスが落ちる可能性がある
  • 再帰処理にアキュムレータを導入することで末尾再帰に書き換えることができる
  • 末尾再帰はコンパイル時にループ処理に置き換えられる
  • 継承元のクラスが実装してるメソッドをオーバーライドするときはoverrideをつける
  • クラスよりケースクラスの方がかなり便利である
  • sealedをつけたabstract classを継承することでパターンマッチに漏れがある場合にコンパイラが注意してくれる
  • REPLで:loadだと1行ずつ読み込まれる、:pasteだとファイル単位で読み込まれる

参考資料

Day3

型パラメータを使ったメソッド

前回リストの先頭要素を返すheadメソッドを自分で作ってみました。

def head(xs: List[Int]): Int = xs match {
  case Nil    => throw new IllegalArgumentException
  case x :: _ => x
}

このメソッドはList[Int]型にしか対応していませんが、リストの先頭要素を返すだけなので、List[String]やList[Double]など、他の型のリストにも対応できるはずです。

xs: List[Int]というのをxs: List[A]としてAの部分にIntやStringなど、色々な型をうけとる可能性がありますよ、という風にしたいです。これには型パラメータを使います。型パラメータはメソッド名のすぐ後ろ、引数の前に書きます。

def head[A](xs: List[A]): A = xs match {
  case Nil    => throw new IllegalArgumentException
  case x :: _ => x
}

def head[A](xs: List[A]): A というのは、headメソッドは型パラメータAを持ちますよ、メソッドの引数はList[A]型ですよ、返り値はA型ですよ、という意味になります。

型パラメータを受け取るメソッドを使うときは型パラメータに何かしらの型を指定します。型パラメータは、引数の型から推論可能なので省略できます。例えば、List(1,2,3)が指定された場合は、型がList[A]である引数xsに、List[Int]型の値を渡しているので、型パラメータAIntであると推論できます。

scala> head[Int](List(1,2,3))
res21: Int = 1

scala> head[String](List("aaa", "bbb", "ccc"))
res22: String = aaa

scala> head(List(1,2,3))
res23: Int = 1

scala> head(List("aaa", "bbb", "ccc"))
res24: String = aaa

これでheadメソッドは色々な型に対応できるようになりました。このように色々な型を受け取れるメソッドは、多相である、と言います。

多相には2種類あります。1つめは今回のように型パラメータを使った多相です。2つめは、Day2でShapeを継承した2種類のRectangleとCircle、そしてShape型を受け取れるsumAreaメソッドをつくったことを思い出してください。このsumAreaメソッドもShapeのサブ型であれば受け取れるため多相であると言えます。

型コンストラクタ

型パラメータはメソッドだけでなく、クラスなどでも使えます。

これまでList[Int]型のリストをたくさん使ってきました。List型というのは存在せず、存在するのはList[Int]型です。Listは型ではありません。Listは型コンストラクタと呼ばれるものです。

型コンストラクタに型パラメータを与えると型になります。コンストラクタにパラメータを渡すとインスタンスをつくることができるのと同じようなイメージです。Listという型コンストラクタにIntという型パラメータを渡すと、List[Int]型という型ができあがります。

型コンスタクタを自分でつくってみましょう。型パラメータの定義では[]を使います。例えばケースクラスで型パラメータを使い、Capsuleという型コンストラクタをつくってみましょう。インスタンス化すると、型がCapsule[Int]Capsule[String]Capsule[List[Int]]となり、型コンストラクタから複数の型をつくれることが分かります。

scala> case class Capsule[A](x: A)
defined class Capsule

scala> Capsule(2)
res25: Capsule[Int] = Capsule(2)

scala> Capsule("ABC")
res26: Capsule[String] = Capsule(ABC)

scala> Capsule(List(1,2,3))
res27: Capsule[List[Int]] = Capsule(List(1, 2, 3))

関数

これまでに色々なメソッドを使ってきました。メソッドは何かしらのオブジェクトに属するもので単独では存在することができません(REPL上は例外です)。

Scalaではメソッドと似ていますが、オブジェクトに属さずに単独で存在できる「関数」があります。Scalaにおいて関数はファーストクラスオブジェクトです。

ファーストクラスオブジェクトであるとは、なんらかのオブジェクトに属さずにそれ単独で値として扱えるというようなことです。値として扱えるので、変数に代入したり、他のメソッドの引数に関数を渡したり、リストに複数の関数を格納したり、ということができます。

では、関数をつくってみましょう。関数をつくるための関数リテラルが用意されています。引数として1つのIntを受け取り、2倍にして返す関数を変数fに代入してみます。

scala> val f = (x: Int) => x * 2
f: Int => Int = <function1>

関数リテラルは、(引数リスト) => { 関数本体 }というようなつくりです。関数本体が1つの式である場合は{}を省略できます。

REPLの出力を見てみましょう。f: Int => Int = <function1>というのは、「fという変数は Int => Int 型で、値は <function1> です」という意味になります。変数fには、Int => Int型のオブジェクトが代入されているということになります。

Int => Int 型というのは、引数として1つのInt型をとり、返り値がInt型である関数を表します。値の <function1> というのは関数本体を書きたいところだけど書くと長くなっちゃうのでと書いて値が関数であることを示してるんだと解釈すればいいかと思います。

では、fに代入した関数を使ってみましょう。関数にある適当な数値を渡してみましょう。関数fに数値10を適用する、と言うこともできます。関数を表すオブジェクトのapplyメソッドを呼び出すことで関数を実行できます。Scalaでは、applyメソッドは省略できることを思い出しましょう。

scala> f.apply(10)
res1: Int = 20

scala> f(10)
res2: Int = 20

関数を定義し、関数で定義した処理を実行することができました。

関数をつくる方法は関数リテラルの他に、メソッドに部分適用をする方法があります。例えば、2 + 3というのはInt型の+メソッドを呼び出していました。この+メソッドから、「2を加算する関数」をつくってみます。メソッドを呼び出すときに具体的な値を指定する代わりに、_を指定することで部分適用することができます。

scala> val f = 2.+(_: Int)
scala> f(3)

scala> val f = 2 + (_: Int)
scala> f(3)

scala> val f: (Int) => Int = 2 + _
scala> f(3)

部分適用するときは、型を明示的に指定する必要があります。型を明示的に指定するのは、_の型でも良いし、fの型でも良いです。

メソッドのレシーバーも_で置き換えることができます(これを部分適用と呼ぶかどうか分からないです、ごめんなさい・・・)。この場合は2つの引数を受け取ることになります。1つめの引数がレシーバーで、2つめの引数が+メソッドの引数になります。

scala> val f: (Int, Int) => Int = _ + _

3つの引数を受け取るsumメソッドをつくってみましょう。部分適用もしてみます。

scala> def sum(a: Int, b: Int, c: Int) = a + b + c
scala> val f: (Int) => Int = sum(_, 1, 2)
scala> val f: (Int, Int) => Int = sum(_, _, 2)
scala> val f: (Int, Int, Int) => Int = sum(_, _, _)

全ての引数を_で指定する場合は、()ごと_で置き換えることで短く書けます。

scala> val f: (Int, Int, Int) => Int = sum _

コレクションの高階メソッド

関数は、Int => Int 型というような型を持つオブジェクトであることは分かりました。関数がファーストクラスオブジェクトだと何に役立つのでしょうか?

役立つシーンの1つに関数を受け取るメソッドがあります。関数を受け取るメソッドを高階メソッドと呼びます。高階メソッドの中でもコレクションが持つ高階メソッドはよく使うことになるでしょう。

コレクションの高階メソッドの1つ、foreachメソッドを使ってみます。foreachメソッドに渡せる関数の型は、A => Unit 型です。foreachメソッドは、受け取った関数にコレクションの要素をそれぞれ順番に適用します。

scala> val cs = ('A' to 'Z')
scala> cs.foreach((c: Char) => println(c))

csがCharのコレクションなので、関数の引数であるcがCharであることはScalaが推論できます。なので、cの型は省略できます。

scala> cs.foreach((c) => println(c))

関数の引数が1つの場合は()も省略できます。

scala> cs.foreach(c => println(c))

さらに、printlnに部分適用することで、A => Unit 型の関数をつくれるため、関数リテラルを使う必要もないです。

scala> cs.foreach(println(_))

全ての引数を部分適用することになるので、()ごと_で置き換えても同じです。

scala> cs.foreach(println _)

さらに、()ごと_で置き換えてつくった関数を別のメソッドに渡すとき、_自体を省略することが可能です。

scala> cs.foreach(println)

だいぶすっきりしますね。省略ルールは以前も紹介したこのページが参考になります -> Scala の省略ルール早覚え

次は、mapメソッドを使ってみます。mapメソッドは、引数で受け取った関数にコレクションそれぞれの要素を適用した結果からなる新たなコレクションを作ります。mapに渡せる関数の型は A => B 型となります。

scala> val xs = (1 to 10)
scala> xs.map(x => x * 2)
scala> xs.map(_ * 2)

このmapメソッド、あるコレクションを別のコレクションに変換しています。これってどこかで聞き覚えないですか?そうです、Day1でやったfor式です。上のmapを使った例はfor式でも書けますね。

scala> for(x <- xs) yield x * 2

どちらを使っても同じことができます。どちらを使うべきかというのは特にないので読みやすいを使えばいいかと思います。

foreach、 mapの他に、filterという高階メソッドがあります。filterにはBooleanを返す関数を渡すことができます。Booleanを返す関数のことを「述語」と呼んだりします。述語関数に各要素を適用した結果がtrueになる要素のみからなるコレクションを取得できます。filterに渡せる関数の型は A => Boolean 型です。

scala> xs.filter(x => x > 6)
scala> xs.filter(_ > 6)

mapとfilterを組み合わせてみます。filterの後ろに続くのがmapかforeachの場合はfilterより高速に動作するwithFilterが使えます。

scala> xs.filter(_ > 6).map(_ * 2)
scala> xs.withFilter(_ > 6).map(_ * 2)

このfilter、withFilterもfor式で同じことができますね。for式ではifを使ってフィルタできるのでした。

scala> for(x <- xs; if x > 6) yield x * 2

もう1つ、flatMapメソッドを見てみましょう。flatMapはmapと同じように値変換するような感じなのですが、ちょっと違います。flatMapに渡す変換のための関数では、コレクションを返す必要があるのです。flatMapに渡せる関数の型は A => List[B] 型となります。

scala> List(1,2,3,4).flatMap(a => 1 to a)
scala> List(1,2,3,4).flatMap(1 to _)

仮にコレクションを返す関数をmapに渡すと入れ子のコレクションが出来上がるでしょう。入れ子のリストを解消するにはflattenメソッドを使います。flatMapは、map+flattenを行った結果と同じになります。

scala> List(1,2,3,4).map(1 to _)
scala> List(1,2,3,4).map(1 to _).flatten
scala> List(1,2,3,4).flatMap(1 to _)

このflatMapもfor式で書けます。ジェネレータを複数回続けた場合と同じことになります。2つのコレクションをfor式の2つのジェネレータに書くと、各要素を組み合わせた処理を書けました。

例えば前回の練習問題で三角形を3要素のタプルで表現するのがありました。for式、flatMap、両方を使った場合はこうなります。

scala> for (c <- 1 to 10;
     |      a <- 1 to c;
     |      b <- 1 to a)
     |   yield (a,b,c)

scala> (1 to 10).flatMap {
     |   c => (1 to c).flatMap {
     |     a => (1 to a).map(b => (a,b,c))
     |   }
     | }

直角三角形だけを抽出したい場合はこうなります。

scala> for (c <- 1 to 10;
     |      a <- 1 to c;
     |      b <- 1 to a;
     |      if c*c == a*a + b*b)
     |   yield (a,b,c)

scala> (1 to 10).flatMap {
     |   c => (1 to c).flatMap {
     |     a => (1 to a).withFilter(b => c*c == a*a + b*b).map(b => (a,b,c))
     |   }
     | }

うーん、これはfor式を使った方が見やすい気がしますね。

その他の高階メソッドをいくつか見てみましょう。

scala> List(8,2,6,9,8,3,5).partition(_ % 2 == 0)
res70: (List[Int], List[Int]) = (List(8, 2, 6, 8),List(9, 3, 5))

scala> List(8,2,6,9,8,3,5).find(_ % 2 != 0)
res71: Option[Int] = Some(9)

scala> List(8,2,6,9,8,3,5).takeWhile(_ % 2 == 0)
res72: List[Int] = List(8, 2, 6)

scala> List(8,2,6,9,8,3,5).dropWhile(_ % 2 == 0)
res73: List[Int] = List(9, 8, 3, 5)

scala> List(8,2,6,9,8,3,5).span(_ % 2 == 0)
res74: (List[Int], List[Int]) = (List(8, 2, 6),List(9, 8, 3, 5))

scala> List(8,2,6,9,8,3,5).forall(_ % 2 == 0)
res75: Boolean = false

scala> List(8,2,6,9,8,3,5).forall(_ > 0)
res76: Boolean = true

scala> List(8,2,6,9,8,3,5).exists(_ % 2 == 0)
res77: Boolean = true

scala> List(8,2,6,9,8,3,5).exists(_ < 0)
res78: Boolean = false

scala> List(8,2,6,9,8,3,5).sortWith((a, b) => a > b)
res79: List[Int] = List(9, 8, 8, 6, 5, 3, 2)

scala> List(8,2,6,9,8,3,5).sortWith(_ < _)
res80: List[Int] = List(2, 3, 5, 6, 8, 8, 9)

ローンパターン

関数は、高階メソッド以外でも色々なところで役に立ちます。例えば、ファイルをつくるメソッドを書いてみましょう。

import java.nio.file._

def writeTo(fileName: String, body: String): Unit = {
  val writer = Files.newBufferedWriter(Paths.get(fileName))
  try {
    writer.write(body)
  } finally {
    writer.close
  }
}

Scalaの標準ライブラリにはファイル書き込みを行うクラスが用意されておらず、ライブラリを使用しない場合はJavaのjava.nio.file.Filesクラスなどを使います。クラス名より前の部分"java.nio.file"の部分をパッケージと言います。これは名前空間で、同じクラス名でも別パッケージであれば定義できるようになっています(例えば、hoge.FilesクラスをつくってもOK)。上の例だとFilesとPathsがjava.nio.fileパッケージなのですが、パッケージ名から書くのは大変です。こういうときはimportでパッケージを指定すると、指定したパッケージ内のクラスはクラス名だけで使うことができるようになります。


注目したいのはtry { } finally { }の部分です。try { }で囲まれたコードでエラーが発生してもfinally { }で囲まれたコードが実行されます。エラーが発生しなくてもfinally { }で囲まれた部分は実行されます。ファイルのクローズのようにエラーが発生しようがしまいが必ず実行したいコードはfinally { }で囲みます。

ファイルの書き込み処理を行う箇所では、このtry { } finally { }を書く必要があります。都度書いているとDRYではなくなります。こういう場合に便利なのがローンパターンと呼ばれる方法です。try { } finally { }という構造を別メソッドで定義し、try { }で囲みたい処理は、java.io.Writer => Any 型の関数で渡してあげるようにします。

import java.nio.file._

def using(fileName: String, f: java.io.Writer => Any): Unit = {
  val writer = Files.newBufferedWriter(Paths.get(fileName))
  try {
    f(writer)
  } finally {
    writer.close
  }
}

def writeTo(fileName: String, body: String): Unit = {
  using(fileName, (writer) => {
    writer.write(body)
  })
}

これでファイルを書き込むときはusingメソッドを使えばよく、try { } finally { }をいろんなところに書く必要はなくなりました。リソースのオープン・クローズはusingメソッド内で行うことで、usingメソッドを使う人はリソースのオープン・クローズを気にしなくてよくなります。このようにオープンしたリソースを関数に貸し出すような動きをするのでローンパターンと呼ばれているようです。ローンパターンの実装についてこちらに色々載ってます -> Scala using(Hishidama's Scala loan-pattern Memo)

複数の引数リスト

usingメソッドを使うことでファイルのオープン・クローズをDRYにできました。usingメソッドですが、言語の組み込み機能であってもよさそうですよね。残念ながらScalaには最初からusingは用意されていません。しかし、usingをあたかも組み込みの構文のように見せることができます。usingを定義するときの引数リスト、メソッド名の右にある()の部分を2つに分けてみましょう。

def using(fileName: String)(f: java.io.Writer => Any): Unit = {
  ・・・

この例のように、Scalaでは複数引数がある場合に、引数リストを複数個に分けることができます。このようなメソッドを呼び出す方も2つの引数リストを指定します。

def writeTo(fileName: String, body: String): Unit = {
  using(fileName)((writer) => {
    writer.write(body)
  })
}

さらに、Scalaでは引数が1つの引数リストの(){}と書くことができます。これを使うとusingが組み込みの構文であるかのように使えます。

def writeTo(fileName: String, body: String): Unit = {
  using(fileName) { writer =>
    writer.write(body)
  }
}

ここでは深く追いませんが、このように複数の引数リストを持つメソッドをカリー化されたメソッド、と呼んだりします。

畳み込み

コレクションの高階メソッドに戻ります。ここで紹介するのはfoldLeftとfoldRightです。これらの高階メソッドは畳み込みと呼ばれます。

Day2で再帰をやりましたが、再帰でよく出てきたパターンがありました。リストに対して先頭要素と先頭要素以外のリストに分割し、先頭要素以外のリストを部分問題として扱う、というパターンです。よく出るパターンなので抽象化して扱えると楽です。畳み込みとはこのパターンのためにあります。

Intのリストの各要素の合計を計算するsumメソッドを思い出しましょう。

def sum(xs: List[Int]): Int = xs match {
  case Nil       => 0
  case x :: tail => x + sum(tail)
}

末尾再帰で書き換えたのがこちらです。

def sum(xs: List[Int], acc: Int): Int = xs match {
  case Nil       => acc
  case x :: tail => sum(tail, x + acc)
}

これをfoldLeftメソッドで書き換えたのがこちらです。foldLeftは2つの引数リストを持ちます。1つ目の引数リストにはアキュムレータの初期値を、2つ目の引数リストにはアキュムレータとリストの要素を使う計算を行う関数を指定します。

def sum(xs: List[Int]): Int = xs match {
  xs.foldLeft(0)((acc, x) => x + acc
}

なんと、sumメソッドは、リストに用意されているfoldLeftメソッドを使うだけで実現できてしまいます。わざわざsumメソッドを定義するまでもないかもしれません。REPLで試してみましょう。List[A]型のfoldLeftの引数は (B)((B, A) => B) という感じになります。アキュムレータの型がBで、関数の型がアキュムレータとリストの要素受け取るので、(B, A) => B 型になります。

scala> val sum = List(1,2,3,4,5).foldLeft(0)((acc, x) => acc + x)
sum: Int = 15

関数内で、引数が1度しか使われていないのであれば、_で置き換えることができるのでした。これは引数が複数個でも同様です。つまり、こう書けます。

scala> val sum = List(1,2,3,4,5).foldLeft(0)(_ + _)
sum: Int = 15

このようにリストを先頭とそれ以外のリストに分割するような再帰を畳み込みで実現できました。この式がどのように評価されるのかイメージしてみましょう。

List(1,2,3,4,5).foldLeft(0)((acc, x) => acc + x)
(0 + 1) + List(2,3,4,5).foldLeft(0)((acc, x) => acc + x)
((0 + 1) + 2) + List(3,4,5).foldLeft(0)((acc, x) => acc + x)
(((0 + 1) + 2) + 3) + List(4,5).foldLeft(0)((acc, x) => acc + x)
((((0 + 1) + 2) + 3) + 4) + List(5).foldLeft(0)((acc, x) => acc + x)
(((((0 + 1) + 2) + 3) + 4) + 5) + List().foldLeft(0)((acc, x) => acc + x)
(((((0 + 1) + 2) + 3) + 4) + 5)
15

アキュムレータを1番左端として左側に畳込むように評価するのでfoldLeftを使った処理は左畳み込みと呼ばれます。

逆にfoldRightを使った場合は右畳み込みとなります。

scala> val sum = List(1,2,3,4,5).foldLeft(0)(_ + _)
sum: Int = 15

右畳み込みをイメージしてみましょう。

List(1,2,3,4,5).foldRight(0)((acc, x) => acc + x)
1 + List(2,3,4,5).foldRight(0)((acc, x) => acc + x)
1 + (2 + List(3,4,5).foldRight(0)((acc, x) => acc + x))
1 + (2 + (3 + List(4,5).foldRight(0)((acc, x) => acc + x)))
1 + (2 + (3 + (4 + List(5).foldRight(0)((acc, x) => acc + x))))
1 + (2 + (3 + (4 + (5 + List().foldRight(0)((acc, x) => acc + x)))))
1 + (2 + (3 + (4 + (5 + 0))))

先ほど出てきたmapメソッドと同じことを畳み込みを使って実装してみましょう。アキュムレータは空のリストになります。

scala> val xs = List(1,2,3,4,5).foldLeft(Nil)((acc, x) => acc ++ List(x * 2))
<console>:13: error: type mismatch;
 found   : List[Int]
  required: scala.collection.immutable.Nil.type
         val xs = List(1,2,3,4,5).foldLeft(Nil)((acc, x) => acc ++ List(x * 2))

アキュムレータの型をうまく推論してくれないので、明示的にList[Int]と指定してあげましょう。

scala> val xs = List(1,2,3,4,5).foldLeft(Nil: List[Int])((acc, x) => acc ++ List(x * 2))
xs: List[Int] = List(2, 4, 6, 8, 10)

mapと同じ動きができました。リストの++メソッドによる連結は問題があります。計算速度が、++メソッドのレシーバであるリストの長さに依存しており、長いリストに対してこの処理を行うと遅くなります。

上のfoldLeftを使った式がどのように評価されるのかイメージしてみましょう。

List(1,2,3,4,5).foldLeft(Nil)((acc, x) => acc ++ List(x * 2))
(Nil ++ List(2)) ++ List(2,3,4,5).foldLeft(Nil)((acc, x) => acc ++ List(x * 2))
((Nil ++ List(2)) ++ List(4)) ++ List(3,4,5).foldLeft(Nil)((acc, x) => acc ++ List(x * 2))
(((Nil ++ List(2)) ++ List(4)) ++ List(6)) ++ List(4,5).foldLeft(Nil)((acc, x) => acc ++ List(x * 2))
((((Nil ++ List(2)) ++ List(4)) ++ List(6)) ++ List(8)) ++ List(5).foldLeft(Nil)((acc, x) => acc ++ List(x * 2))
(((((Nil ++ List(2)) ++ List(4)) ++ List(6)) ++ List(8)) ++ List(10)) ++ List().foldLeft(Nil)((acc, x) => acc ++ List(x * 2))
Nil ++ List(2) ++ List(4) ++ List(6) ++ List(8) ++ List(10)
List(2) ++ List(4) ++ List(6) ++ List(8) ++ List(10)
List(2, 4) ++ List(6) ++ List(8) ++ List(10)
List(2, 4, 6) ++ List(8) ++ List(10)
List(2, 4, 6, 8) ++ List(10)
List(2,4,6,8,10)

++メソッドのレシーバーがどんどん大きくなっていくのが分かりますね。

こういう場合はfoldRightによる右畳み込みが使えます。右畳み込みはレシーバであるリストの右から順番に処理します。そのため、++メソッドよりも効率よく処理できる::メソッドを使うことができます。foldRightの引数は (B)((A, B) => B) という感じです。アキュムレータの型がBで、関数がリストの要素とアキュムレータを受け取るため (A, B) => B 型となります。

scala> val xs = List(1,2,3,4,5).foldRight(Nil: List[Int])((x, acc) => (x * 2) :: acc)
xs: List[Int] = List(2, 4, 6, 8, 10)

右畳み込みを使った式がどのように評価されるのかイメージしてみましょう。

2 :: List(1,2,3,4,5).foldRight(Nil)((x, acc) => (x * 2) :: acc)
2 :: (4 :: List(3,4,5).foldRight(Nil)((x, acc) => (x * 2) :: acc))
2 :: (4 :: (6 :: List(4,5).foldRight(Nil)((x, acc) => (x * 2) :: acc)))
2 :: (4 :: (6 :: (8 :: List(5).foldRight(Nil)((x, acc) => (x * 2) :: acc))))
2 :: (4 :: (6 :: (8 :: (10 :: List().foldRight(Nil)((x, acc) => (x * 2) :: acc)))))
2 :: (4 :: (6 :: (8 :: (10 :: Nil))))
2 :: (4 :: (6 :: (8 :: (10 :: Nil))))
2 :: (4 :: (6 :: (8 :: List(10))))
2 :: (4 :: (6 :: List(8, 10)))
2 :: (4 :: List(6, 8, 10))
2 :: List(4, 6, 8, 10)
List(2, 4, 6, 8, 10)

Option

型コンストラクタはList以外にも色々あります。ここではOptionを使ってみましょう。

Optionはリストのように値を格納することができますが、値を1つだけ持つか、もしくは何も持たないかです。値を持つ場合はOptionのサブクラスであるSome、値を持たない場合は同じくOptionのサブクラスであるNoneを使います。

scala> val opt = Some(2)
opt: Some[Int] = Some(2)

scala> val opt = None
opt: None.type = None

それぞれSome[Int]型とNone型です。これらの型はOption[Int]型としても扱えます。

scala> val opt: Option[Int] = None
scala> val opt: Option[Int] = Some(2)

また、Someはケースクラスであるため、パターンマッチでマッチさせつつ中身の値を取り出すことができます。

scala> opt match {
     |   case Some(x) => println(x)
     |   case None    => println("none")
     | }
2

Optionは値が存在しないかもしれない場合や、計算が失敗することがあるかもしれない、という場合に使います。正常に計算できた場合はSomeを、k結果が存在しない、もしくは計算に失敗した場合はNoneを返すようなメソッドを定義します。そうするとメソッドの返り値の型がOptionであることから、このメソッドは結果が存在しないことがあるんだな、と分かります。

さっきつくったheadメソッドは、空のリストを渡すとエラーになってしまっていました。headメソッドの返り値の型をOption[A]にして、空のリストの場合はNoneを返すようにしましょう。空でない場合はSome[A]を返します。

scala> def head[A](list: List[A]): Option[A] = list match {
     |   case Nil => None
     |   case x :: _ => Some(x)
     | }
head: [A](xs: List[A])Option[A]

scala> val xs = List(1,2,3)
scala> val res = head(xs)
res: Option[Int] = Some(1)

scala> res match {
     |   case None    => "error"
     |   case Some(x) => s.toString
     | }

scala> val xs = Nil
scala> val res = head(xs)
res: Option[Nothing] = None

scala> res match {
     |   case None    => "error"
     |   case Some(x) => s.toString
     | }

Optionから結果を取り出すときはパターンマッチを使いましたが他にも色々あります。こちらの記事が参考になります -> ScalaのOptionステキさについてアツく語ってみる - ( ꒪⌓꒪) ゆるよろ日記

scala> Some("ABC").getOrElse("DEFAULT")
scala> Some("ABC").foreach(println(_))
scala> Some("ABC").exits(_ == "AAA")

デフォルトの値をOptionで返すorElseメソッドもあります。

scala> val o: Option[String] = Some("ABC").orElse(Some("DEFAULT"))
scala> val o: Option[String] = None.orElse(Some("DEFAULT"))

Optionには、リストの高階メソッドで出てきたmapメソッドを持っています。

scala> val xs = List(1,2,3)
scala> head(xs).map(_ * 2)
res36: Option[Int] = Some(2)

scala> val xs: List[Int] = Nil
scala> head(xs).map(_ * 2)
res37: Option[Int] = None

mapメソッドの便利なところはOptionがSomeなのかNoneなのか気にしなくていいところです。パターンマッチを使って書くとこうなっているところでした。

scala> head(xs) match {
     |   case Some(x) => Some(x * 2)
     |   case None    => None
     | }

filterメソッドもあります。withFilterメソッドもあるのでmapとつなげる場合はこちらの方が高速でいいです。

scala> Some(5).filter(_ > 3)
res49: Option[Int] = Some(5)

scala> Some(5).filter(_ > 6)
res50: Option[Int] = None

scala> Some(5).withFilter(_ > 3).map(_ * 2)
res51: Option[Int] = Some(10)

scala> Some(5).withFilter(_ > 6).map(_ * 2)
res52: Option[Int] = None

flatMapメソッドもあります。これはOptionを返すメソッドを複数個つなげたい場合に便利です。例えば、割り算をするメソッドを用意します。0で割ろうとした場合にNoneを返します。これをさっきのheadメソッドとつなげてみましょう。

scala> def div(n: Int, d: Int): Option[Int] = if (d == 0) None else Some(n / d)

scala> head(List(2,4)).flatMap(div(8, _)).withFilter(_ > 3).map(_ * 5)
res57: Option[Int] = Some(20)

scala> head(List()).flatMap(div(8, _)).withFilter(_ > 3).map(_ * 5)
res58: Option[Int] = None

scala> head(List(0,4)).flatMap(div(8, _)).withFilter(_ > 3).map(_ * 5)
res59: Option[Int] = None

scala> head(List(2,4)).flatMap(div(6, _)).withFilter(_ > 3).map(_ * 5)
res60: Option[Int] = None

2つ目、3つ目、4つ目の結果がNoneになっています。しかし、Noneになった原因はそれぞれ異なります。2つ目は最初のhead(List())の時点でNoneになり計算全体としての結果がNoneになります。3つ目はflatMap(div(8, _)の時点でNoneになります。3つ目はwithFilter(_ > 3)の時点でNoneになります。

これらの高階メソッドを使うことで、計算の途中結果がSomeなのかNoneなのか気にしなくてよくなり、パターンマッチを何度も書かなくて済みます。

コレクション以外でもfor式

リストのmap、withFilter、flatMapメソッドはfor式でも同じことができると言いました。実際、Scalaではmap、filter、withFilter、flatMap、foreachメソッドをfor式で書くことができます。

自分で定義した型でもこれらのメソッドを持っていればfor式で使えるのです。

case class Capsule[A](x: A) {
  def map[B](f: A => B): Capsule[B] = Capsule(f(x))
  def flatMap[B](f: A => Capsule[B]): Capsule[B] = f(x)
}

普通にメソッドを呼び出してみましょう。

scala> Capsule(2).map(x => x * 5)
res81: Capsule[Int] = Capsule(10)

scala> Capsule(2).flatMap(x => Capsule(x * 5)).map(x => x + 3)
res82: Capsule[Int] = Capsule(13)

for式で使ってみます。

scala> for (x <- Capsule(2);
     |      y <- Capsule(x * 5))
     |   yield y + 3
res83: Capsule[Int] = Capsule(13)

使えましたね。ScalaのAPIドキュメントなどを読んでいてmap、filter、withFilter、flatMap、foreachなどを見つけたら注目しましょう。これらを使ってfor式で書いてるコードがあるかもしれないですし、自分で書くときもfor式を使うと読みやすくなるかもしれません。for式への置き換えについては、こちらのサイトに分かりやすく書いてあります -> Scala for実体メモ(Hishidama's Scala for-convert Memo)

Optionにもmap、filterWith、flatMapメソッドがありました。つまり、Optionをfor式で使えるということです。

flatMapを使った例をもう一度見てみましょう。

scala> head(List(2,4)).flatMap(div(8, _)).withFilter(_ > 3).map(_ * 5)
scala> head(List()).flatMap(div(8, _)).withFilter(_ > 3).map(_ * 5)
scala> head(List(0,4)).flatMap(div(8, _)).withFilter(_ > 3).map(_ * 5)
scala> head(List(2,4)).flatMap(div(6, _)).withFilter(_ > 3).map(_ * 5)

Optionは値を1つだけ入れられる入れ物のようですが、計算に失敗するかもしれないという文脈に入れていると捉えることもできます。map、filterWith、flatMapを使うことで、文脈から値を取り出すことなく文脈内の値を様々な関数に適用することができます。

上の例をfor式で書き直してみましょう。for式の1番外側の括弧は{}で書くこともできます。

scala> for(x <- head(List(2,4));
     |     y <- div(8, x);
     |     if (y > 3))
     |   yield y * 5

scala> for {
     |   x <- head(List());
     |   y <- div(8, x);
     |   if (y > 3)
     | } yield y * 5

scala> for {
     |   x <- head(List(0,4));
     |   y <- div(8, x);
     |   if (y > 3)
     | } yield y * 5

scala> for {
     |   x <- head(List(2,4));
     |   y <- div(6, x);
     |   if (y > 3)
     | } yield y * 5

for式で書いたものを上から順に読んでいくと、headの結果をxに代入して、div(8, x)を実行して結果をyに代入して・・・というように、手続き的に書いたプログラムのように読めます。forが関数の始まり、yieldがreturnだと考えると手続き的に書いた関数のようです。

OptionにmapとflatMapがあることで、Option内に格納されてる値に対して色々な関数を適用でき、for式に置き換えることでそれが手続き的なプログラミングように見えます。失敗するかもしれない計算が続く場合、つまりflatMapが何回も続くような場合に、for式で書いた方が読みやすく感じるかもしれません。

ScalaでmapとflatMapメソッド持つ型をモナドと呼んだりします。例えばOptionがモナドです。for式はコレクションのための構文ではなく、もっと抽象化されたモナドのための構文ということになります。モナドの厳密な話はできないのでここではしません(知識が足りないです、ごめんなさい・・・)。興味ある人はWebで調べたり、Scala関数型デザイン&プログラミングを読んでみるとおもしろいと思います。

Map

連想配列として使えるMapも型コンストラクタです。これは型パラメータを2つ受け取る型コンストラクタです。キーの型と値の型の2種類です。使ってみましょう。

scala> val m = Map[String, Int](("key1", 1), ("key2", 2))
scala> val m = Map(("key1", 1), ("key2", 2))
scala> val m = Map("key1" -> 1, "key2" -> 2)
m: scala.collection.immutable.Map[String,Int] = Map(key1 -> 1, key2 -> 2)

scala> m.get("key1")
res94: Option[Int] = Some(1)

scala> m.get("hogehoge")
res95: Option[Int] = None

MapシングルトンオブジェクトのapplyメソッドでMapのインスタンスをつくっています。引数にはキーと値をタプルで渡してます。メソッド呼び出し時の型パラメータは省略可能でした。MapのインスタンスをつくるときはMap(("key1", 1), ("key2", 2))ではなくMap("key1" -> 1, "key2" -> 2)という風に書くことが多いです。この書き方についてはDay4で説明します。

Mapから値を取り出すにはgetメソッドを使いますが、getメソッドの返り値はOptionです。指定したキーが存在しない場合はNoneが返ります。

一度つくったマップに要素を追加してみましょう。追加は+メソッドです。Mapもイミュータブルであるため、要素が追加されたMapをつくるだけで、元々のMapに変更はありません。

scala> m + ("key3" -> 3)
res96: scala.collection.immutable.Map[String,Int] = Map(key1 -> 1, key2 -> 2, key3 -> 3)

scala> m
res97: scala.collection.immutable.Map[String,Int] = Map(key1 -> 1, key2 -> 2)

練習問題

  1. List[Int]の要素全てを掛け合わせるメソッドを畳み込みを使ってつくってください。
  2. リストの長さを返すメソッドを畳み込みを使ってつくってください。型パラメータを使って多相なメソッドにしてください。
  3. リストの並び順を逆順にするメソッドを畳み込みを使ってつくってください。
  4. 2つの同じ型のリストを結合するメソッドを畳み込みを使ってつくってください。
  5. 2人のユーザーの年齢の合計を返す処理を考えます。Web API の1つだと仮定してください。リクエストが来ると以下のメソッドが呼ばれます。リクエストパラメータは指定されてくるかどうか分からないのでOption型となっています。
def addAges(userIdParam1: Option[Int], userIdParam2: Option[Int], minAgeParam: Option[Int]): Int

ユーザーはケースクラスで表します。ユーザー情報を取得するにはfindByIdメソッドを使います。DBはMapで代用します。

case class User(id: Int, age: Int)

private val db = Map(
  1 -> User(1, 19),
  2 -> User(2, 25),
  3 -> User(3, 30)
)
def findById(id: Int): Option[User] = {
  db.get(id)
}

addAgesメソッドを実装してください。minAgeパラメータのデフォルト値は20としてください。minAgeパラメータで指定された年齢を下回るユーザーの年齢は0とみなしてください。ユーザーIDがどちらか一方でも指定されなかった場合は0としてください。

こんな感じの結果が返ってくるはずです。

scala> addAges(Some(1), Some(2), None)
res0: Int = 25

scala> addAges(Some(1), Some(2), Some(1))
res1: Int = 44

今日出てきたキーワード

  • 型パラメータ
  • 多相
  • 型コンストラクタ
  • ファーストクラスな関数
  • 関数リテラル
  • 部分適用
  • 高階メソッド
  • ローンパターン
  • 複数の引数リスト、カリー化されたメソッド
  • 左畳み込み、右畳み込み
  • Option

要注意ポイント

  • ListやOptionなどは型ではなく型コンストラクタ
  • Optionの高階メソッドを使うことで、Optionの結果を都度パターンマッチしなくて済む
  • map、filter、withFilter、flatMap、foreachメソッドをfor式で書き直すことができる

参考サイト

Day4

暗黙の型変換

MapをつくるときMapのapplyメソッドを使いました。Map(("key1", 1), ("key2", 2))という風に使いますが、Map("key1" -> 1, "key2" -> 2)とも書けました。

("key1", 1)というタプルの代わりに"key1" -> 1を指定しています。"key1"というStringの->メソッドを呼び出し、その結果がタプルになっていれば辻褄が合います。しかし、ScalaのStringはJavaのStringを使っていて、JavaのStringには->メソッドは存在しません。

Scalaでは暗黙の型変換(implicit conversion)という仕組みを利用して、Stringに->メソッドがあるかのように振る舞わせることができます。

まず、暗黙の型変換とはなんでしょうか。Day2で出てきたRectangle型を思い出しましょう。

scala> case class Rectangle(x1: Double, y1: Double, x2: Double, y2: Double)
scala> val rec = Rectangle(0, 0, 2, 3)

Rectangleケースクラスは、Double型の値を4つ受け取るように定義されていますが、インスタンス化するときにInt型の値を指定しています。Int型の値を指定してるのにコンパイルが通るのは、暗黙の型変換によりInt型がDouble型に変換されてるためです。

Rectangleケースクラスに、Rectangle型の値を受け取り、自分自身と面積を比べて大きい方を返すmaxメソッドを追加してみます。

scala> case class Rectangle(x1: Double, y1: Double, x2: Double, y2: Double) {
     |   val area = math.abs(x2 - x1) * math.abs(y2 - y1)
     |   def max(a: Rectangle): Rectangle = if (a.area > this.area) a else this
     | }

scala> val rec = Rectangle(0, 0, 2, 3)
scala> rec.max(Rectangle(0, 0, 1, 2))

4要素のタプルをmaxメソッドに渡してみましょう。

scala> rec.max((0, 0, 1, 2))
<console>:11: error: type mismatch;
 found   : (Int, Int, Int, Int)
 required: Rectangle
              rec.max((0, 0, 1, 2))
                      ^

当然コンパイルエラーになりますね。ここで、Int型が4つのタプルを、Rectangle型へ変換するメソッドをつくってみます。

scala> def toRectangle(t: (Int, Int, Int, Int)): Rectangle = Rectangle(t._1, t._2, t._3, t._4)
scala> rec.max(toRectangle(0, 0, 1, 2))

このtoRectangle(0, 0, 1, 2)というメソッド呼び出しを暗黙的に実行させ、暗黙の型変換を行うようにします。暗黙の型変換は、変換処理を行うメソッドにimplicitをつけます。

scala> implicit def toRectangle(t: (Int, Int, Int, Int)): Rectangle = Rectangle(t._1, t._2, t._3, t._4)
scala> rec.max((0, 0, 1, 2))

4要素のタプルをRectangle型へ変換を行う暗黙の型変換を定義できました。

この暗黙の型変換を使うと、元々存在している型にメソッドを追加する、ということもできます。

例えば、数値に文字列を渡すと、数値の回数だけ渡された文字列を繰り返すrepeatというメソッドを、Intにつけ足してみましょう。3.repeat("Hoge")というように呼べたらOKです。

まずは、RepeatorというIntとは別のクラスを作ってみましょう。

scala> class Repeater(x: Int) {
     |   def repeat(s: String) = (0 until x).foldLeft("")((acc, a) => acc + s)
     | }

scala> new Repeater(3).repeat("hogehoge")

new Repeator(3)というのを暗黙的に行うようにします。クラスにimplicitをつけます。

scala> implicit class Repeater(x: Int) {
     |   def repeat(s: String) = (0 until x).foldLeft("")((acc, a) => acc + s)
     | }
defined class Repeater

scala> 3.repeat("hogehoge")

Int型に新たにrepeatメソッドをつけ足すことができました。

(クラスにimplicitをつけるやり方はScala2.10以降にできるようになりました。それ以前はメソッドにimplicitをつける方法を使用するPimp My Libraryと呼ばれるパターンが使われていました。参考サイト -> Scala 2.10.0 M3の新機能を試してみる(2) - SIP-13 - Implicit classes

implicit classをつけた方法は毎回Repeaterクラスをnewしてしまいます。AnyValを継承すると効率を上げることができます。こちらを参考してください。Scalaメソッド定義メモ(Hishidama's Scala def Memo) # 暗黙クラス(implicit class)

Mapで使った->メソッドも暗黙の型変換によって実現されています。"key1" -> 1と書くと暗黙の型変換によりStringである"key1"に->メソッドがあるかのように振る舞い、("key1", 1)というタプルを返します。

この暗黙の型変換は、Predefというシングルトンオブジェクトに定義されています。ScalaではデフォルトでPredefシングルトンオブジェクトのメソッドをimportしています。シングルトンオブジェクトというのは初めて出てきましたが、ちょっと後でやります。ここではいくつかの暗黙の型変換メソッドがデフォルトで用意されている、ということを分かってください。Prefefには便利な暗黙の型変換が用意されています。 Scala Predefオブジェクトメモ(Hishidama's Scala Predef Memo)

このようにScalaでは暗黙の型変換を使うことで、既存のクラスを拡張できます。

暗黙の型変換について、コップ本に注意点が書いてあります。

暗黙の型変換の使い方を誤ると、クライアントコードを読みにくく理解しにくいものにしてしまう危険がある。暗黙の型変換はソースコードに明示的に書き出されるのではなく、コンパイラが暗黙のうちに適用するので、クライアントプログラマーからは、どのような暗黙の型変換が適用されているのかはっきりとはわからない。 (中略) 簡潔さは読みやすさの大きな構成要素だが、簡潔すぎてわからないということもある。効果的な簡潔さをもたせて、わかりやすいクライアントコードを書けるライブラリーを設計すれば、クライアントプログラマーたちが生産的に仕事を進めるのを後押しできる。

引用元:Scalaスケーラブルプログラミング第2版

暗黙の引数

暗黙の型変換で利用したimplicitというキーワードですが、型変換とは別のところでも利用します。それが暗黙の引数です。メソッドの引数リストのうち一番最後の引数リストにimplicitをつけることができます。

scala> def greet(name: String)(implicit s: String) = s"${s}, ${name}!"

このメソッドは今まで通り使うことができます。2つ目の引数を省略したらエラーですね。

scala> greet("Taro")("Hello")
res0: String = Hello, Taro

scala> greet("Taro")
<console>:9: error: could not find implicit value for parameter s: String
              greet("Taro")
                   ^

エラーメッセージをよく読むと、「引数が足りません」ではなく、「implicit value が見つかりません」というようなことが書いてあります。「implicit value」とは、変数宣言にimplicitをつけたものです。

implicitがついた変数を用意してみましょう。

scala> implicit val x = "Hello"
x: String = Hello

scala> greet("Taro")
res2: String = Hello, Taro

2つ目の引数を指定しなくてもgreetメソッドが動きました。このようにimplictがついた引数リストを暗黙の引数、implictがついた値を暗黙の値といいます。

これが何の役に立つのでしょうか。1つ例を挙げるとDBへのコネクションなどを色々なメソッドで使いまわしたいオブジェクトがあるときに便利です。DBコネクションを使うメソッドを呼び出す度に何度も引数に渡すことになるのでグローバル変数で扱えるようにしたくなりますが、それをやってしまうと引数以外の状態(しかもグローバル!)に依存することになりメソッドの複雑さが増し、テストしづらくなったりします。グローバル変数で扱う代わりに、DBコネクションを暗黙の値として定義し、DBコネクションを利用するメソッドは暗黙の引数としてDBコネクションを受け取るようにすることで、グローバルに依存することを回避しつつ何度も引数にDBコネクションを指定する面倒さも回避できます。

シンプルなのでありがたみが余りないですが、

val connection = connectDb
val user = findById(userId, connection)
user.modifyName("Jiro")
update(user, connection)

というコードを以下のように書き換えることができる、という意味です。

implicit val connection = connectDb
val user = findById(userId)
user.modifyName("Jiro")
update(user)

activator(sbt)

sbtというのはScalaのビルドツールです。activatorというのはsbtにプロジェクトのテンプレート作成機能などを追加したツールです。

以下のコマンドでヘルプが出力されます。

$ activator -h

以下のコマンドでテンプレート一覧が表示されます。かなりの量です。

$ activator list-templates

"hello-scala-2_11"というテンプレートでプロジェクトをつくってみましょう。

$ activator new hello-project hello-scala-2_11

このようなファイル達が自動で生成されます。

$ tree -a hello-project/
hello-project/
├── .gitignore
├── LICENSE
├── activator
├── activator-launch-1.3.2.jar
├── activator.bat
├── build.sbt
├── project
│   └── build.properties
└── src
    ├── main
    │   └── scala
    │       └── Hello.scala
    └── test
        └── scala
            └── HelloSpec.scala

以下のファイルたちはプロジェクトで使うactivatorです。これらをGitなどのバージョン管理ツールに入れておくことで、プロジェクトメンバー間やCIツールで同じバージョンのactivatorを使うことができます。

  • activator
  • activator-launch-1.3.2.jar
  • activator.bat
  • project/build.properties

以下のファイルがactivator(sbt)で使うファイルです。ビルドの設定ファイルです。

  • build.sbt

build.sbtの中身を見てましょう。中身はScalaのDSLになっています。namelibraryDependenciesといったキーに対して値を設定することでビルドの設定を行います。キーは値を1つだけ設定できるものと複数設定できるものがあります。nameは前者でlibraryDependenciesは後者です。

name := """hello-project"""

version := "1.0"

scalaVersion := "2.11.0"

libraryDependencies += "org.scalatest" % "scalatest_2.11" % "2.1.3" % "test"

build.sbt内の各キーに対する設定は空行で区切ってください。

:=というのは代入になります。nameversionという設定を行っています。これらはビルドの成果物のファイル名に使われます。

+=というのはコレクションへ要素を追加しています。libraryDependenciesというコレクションに"org.scalatest" % "scalatest_2.11" % "2.1.3" % "test"を追加しています。libraryDependenciesというのは名前の通り依存するライブラリを設定しておくコレクションです。ここに設定されたライブラリがビルド時にダウンロードされます。

複数のライブラリを使う場合は+=を複数回書く必要があります。build.sbtはScalaのDSLですので、Scalaのコードを書くことができます。そのため、++=を使ってlibraryDependenciesにSeqでつけたすこともできます。例えば以下のように書けます。

libraryDependencies ++= Seq(
  "org.scalatest" % "scalatest_2.11" % "2.1.3" % "test",
  "com.typesafe.play" %% "play-slick" % "0.8.1"
)

以下のファイルがソースコードです。src/main/scalaディレクトリにあるソースコードがプロダクションコードです。src/text/scalaディレクトリにあるソースコードがテストコードです。これらのディレクトリ内にコードを追加していきます。

  • src/main/scala/Hello.scala
  • src/test/scala/HelloSpec.scala

ソースコードを見てみましょう。

object Hello {
  def main(args: Array[String]): Unit = {
    println("Hello, world!")
  }
}

Scalaではコンパイル後のファイルを実行するとき、シングルトンオブジェクトのmain(args: Array[String]): Unit メソッドが実行されます。ソースコードを見てみるとテンプレートで生成されたアプリケーションは"Hello, world!"と標準出力に出力するだけですね。

activatorコマンド使ってビルド・実行をしてみましょう。build.sbtがあるディレクトリでactivatorコマンドを実行すると対話モードになります。対話モードでrunコマンドを実行しましょう。初回はライブラリなどのダウンロードがあるためかなり時間が掛かります。

$ ./activator

> run

[info] Running Hello
Hello, world!
[success] Total time: 63 s, completed 2015/04/05 10:46:56

動きましたね!対話モードを抜けるときはexitと入力しましょう。

> exit

sbtのドキュメントはこちらにあるので読んでみましょう。 始めるsbt

パッケージ

今までつくってきたクラスはすべてグローバル空間に定義していました。プログラムが大きくなると管理するのが大変なので、Scalaではパッケージを使って、メソッド名やクラス名の衝突を防いだり、別パッケージからは見えないメソッドを作ったりします。パッケージはpackageで指定します。ファイルの先頭に指定するのが一般的です。

// src/scala/Hoge.scala
package example.hoge

case class Hoge(s: String) {
  def hello = "Hello, " + s + "!"
}

異なるパッケージのクラスはクラス名だけで使えません。

object Hello {
  def main(args: Array[String]): Unit = {
    println(new Hoge("World").hello)
  }
}

この状態でactivator runしてみましょう。コンパイルエラーになりますね。

$ activator run
[error] /・・・/hello-project/src/main/scala/Hello.scala:4: not found: type Hoge
[error]     println(new Hoge("World").hello)
[error]                 ^
[error] one error found
[error] (compile:compile) Compilation failed

使う場合はexample.hoge.Hogeというようにパッケージ+クラス名で書くか、インポートします。インポートはimport example.hoge.Hogeというように書きます。インポート時にimport example.hoge._書くことでexample.hogeパッケージ内のすべてをインポートすることもできます。

import example.hoge._

object Hello {
  def main(args: Array[String]): Unit = {
    println(Hoge("World").hello)
  }
}

Scalaは、暗黙のうちに以下のインポートをすべてのプログラムに対して行っています。

import java.lang._
import scala._
import Predef._

アクセス修飾子

定義したクラスやメソッドを外部からアクセスできないようにしたいときに、アクセス修飾子を使います。privateをつけると同じクラスのみからしかアクセスできなくなります。protectedをつけると同じクラスかサブクラスからしかアクセスできなくなります。

メソッドにprivateをつけてみます。

// Hoge.scala
package example.hoge

class Hoge(s: String) {
  private def hello = "Hello, " + s + "!"
}

privateをつけたメソッドは呼べなくなりました。

$ activator run
[error] /・・・/hello-project/src/main/scala/Hello.scala:5: method hello in class Hoge cannot be accessed in example.hoge.Hoge
[error]     println(new Hoge("World").hello)
[error]                               ^
[error] one error found
[error] (compile:compile) Compilation failed

コンストラクタにprivateをつけてみます。

package example.hoge

class Hoge private (s: String) {
  def hello = "Hello, " + s + "!"
}

privateをつけたコンストラクタにはアクセスできなくなりました。

$ activator run
[error] /・・・/hello-project/src/main/scala/Hello.scala:5: constructor Hoge in class Hoge cannot be accessed in object Hello
[error]     println(new Hoge("World").hello)
[error]             ^
[error] one error found
[error] (compile:compile) Compilation failed

もっと細かくアクセス制御したい場合は限定子というものを使います。限定子というのはprivate[com.example.hoge]というようにアクセス修飾子の後ろに[]で公開するパッケージやクラス名を指定します。こちらを参考にしてください -> Scalaクラスメモ(Hishidama's Scala class Memo)

シングルトンオブジェクトとコンパニオンオブジェクト

クラスやケースクラス以外にも型をつくる方法はいくつかありますが、そのうちの1つがシングルトンオブジェクトです。これはその名の通りシングルトンオブジェクトをつくります。インスタンス化する、というような考え方はないです。定義したらそれが型でありオブジェクトになります。

シングルトンオブジェクトは、objectを使って定義します。

object OriginPoint {
  val x = 0.0
  val y = 0.0
}

シングルトンオブジェクトは何か1つしかないデータを表すのに使うことよりも、他の使い方で使われることが多いです。

先ほど、Hogeケースクラスのコンストラクタをprivateにしました。そうするとことでHoge("world")ができなくなりました。つまりどこからもインスタンス化できません。しかし、コンパニオンオブジェクトを使うことでインスタンス化できます。

コンパニオンオブジェクトとは、同名のクラス定義と同じファイル内で定義されたシングルトンオブジェクトのことです。コンパニオンオブジェクトは、同名のクラス(これをコンパニオンクラスと言う)のprivateなメソッドやコンストラクタを呼び出すことができます。Hogeクラスのコンパニオンオブジェクトを定義してみましょう。

package example.hoge

case class Hoge private (s: String) {
  def hello = "Hello, " + s + "!"
}
object Hoge {
  def apply(s: String) = new Hoge(s)
}

使ってみましょう。applyメソッドはメソッド名を省略できましたね。これはコンパイルできます。

import example.hoge._

object Hello {
  def main(args: Array[String]): Unit = {
    println(Hoge("World").hello)
  }
}

コンパニオンオブジェクトは結構使われています。例えば今まで何回も使ってきたList(1,2,3,4,5)というのもListコンパニオンオブジェクトのapplyメソッドを呼び出しています。Listはクラスではなく抽象クラスです。Listコンパニオンオブジェクトのapplyメソッドでは、Listのサブクラスをインスタンス化して返却しています。コンパニオンオブジェクトを使うことで、使う側が実態のクラスを意識しなくて済むようになっています。

可変長引数

List(1,2,3,4,5)というのはListコンパニオンオブジェクトのapplyメソッドであることが分かりました。このメソッドは可変長引数を受け取るようになっています。そのため、List(1)List(1, 2, 3, 4, 5)というように引数の数を可変にできます。

Int型を複数受け取って合計を返すsumメソッドをつくってみましょう。可変長引数を使う場合は引数の型をInt*というように*をつけます。可変長引数で受け取った引数はSeq型として扱うことができます。Seq型はリストのスーパークラスです。

scala> def sum(xs: Int*): Int = xs.foldLeft(0)(_ + _)

scala> sum()
scala> sum(1,2,3)

可変長引数にList型やSeq型を渡すときは、: _*を使います。

scala> val xs = Seq(1,2,3)
scala> sum(xs: _*)

トレイトの紹介

これまでに型をつくる方法として、クラス、抽象クラス、ケースクラス、シングルトンオブジェクトを紹介しました。他の方法として、トレイトがあります。トレイトは抽象クラスと同様にインスタンス化できません。抽象クラスと異なるのは、抽象クラスは1つしか継承できないのに対し、トレイトの場合は複数継承できます。トレイトを継承するときは「継承」ではなく「ミックスイン」と言います。ミックスインするときはextendsもしくはwithを使います。

抽象クラスとトレイトの使い分けに関する参考資料です。 trait と abstract class の使い分け

ScalaにはOrderedトレイトが用意されています。Orderedトレイトをミックスインして、compareメソッドをオーバーライドするだけで、オブジェクト比較するメソッドを使うことができます。

Day2で定義したShapeを使ってみましょう。ShapeにOrderedトレイトをミックスインします。Orderedトレイトは1つ型パラメータを1つとる型コンストラクタです。オーバーライドするcompareメソッドの引数の型が型パラメータによって決まります。

abstract class Shape extends Ordered[Shape]{
  def area: Double
  override def compare(that: Shape) = (this.area - that.area).toInt
}

case class Rectangle(x1: Double, y1: Double, x2: Double, y2: Double) extends Shape {
  def area: Double = math.abs(x2 - x1) * math.abs(y2 - y1)
}

case class Circle(x: Double, y: Double, r: Double) extends Shape {
  def area: Double = r * r * math.Pi
}

使ってみましょう。比較演算子が使えるようになりました。

scala> val c = Circle(0, 0, 5)
scala> val a = Rectangle(0, 0, 3, 4)
scala> a > c
scala> a >= c
scala> a < c
scala> a <= c

型パラメータの境界

Day2の練習問題でやったバイナリサーチのコードを思い出してみましょう。ソート済みのリストから値を検索します。

def binarySearch(list: List[Int], a: Int): Boolean = list match {
  case Nil      => false
  case x :: Nil => x == a
  case _ => {
    val p = list.size / 2
    val m = list(p)
    if (a == m)     true
    else if (a < m) binarySearch(list.take(p), a)
    else            binarySearch(list.drop(p), a)
  }
}

これはList[Int]型にしか対応していませんが、List[Double]やList[Float]などにも対応して多相性を持たせたくなりました。では単純にList[Int]を型パラメータを使ってList[A]に書き換えてみましょう。

def binarySearch[A](list: List[A], a: A): Boolean = list match {
  ・・・

REPLで読み込んでみます。

scala> :lo binarySearch.scala
Loading binarySearch.scala...
<console>:16: error: value < is not a member of type parameter A
           else if (a < m) binarySearch(list.take(p), a)
                                 ^

比較演算子が存在しないよというコンパイルエラーになりました。もっともな指摘ですね。型パラメータAを導入し、引数listの型をList[A]型にしたため、listの要素であるA型のオブジェクトに比較演算子が定義されていない可能性があります。型パラメータAに指定できる型が、Orderedトレイトをミックスインした型に限定できればコンパイルできるはずです。

このように型パラメータに指定できる型を限定する方法が型パラメータの境界です。ある型のサブクラスに限定したい場合は、<:を使います。これを上限境界と呼びます。

def binarySearch[A <: Ordered[A]](list: List[A], a: A): Boolean = list match {
  ・・・

REPLで使ってみましょう。先ほどOrderedトレイトをミックスインしたShapeのリスト、List[Shape]型に対して2分探索できるようになりました。

scala> val xs: List[Shape] = List(Circle(0,0,5), Circle(0,0,8), Rectangle(0,0,2,3))
scala> binarySearch(xs, Circle(0,0,5))

しかし、List[Int]型の2分探索ができません!

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

scala> binarySearch(xs, 3)
<console>:11: error: inferred type arguments [Int] do not conform to method binarySearch's type parameter bounds [A <: Ordered[A]]
              binarySearch(xs, 3)
              ^
<console>:11: error: type mismatch;
 found   : List[Int]
 required: List[A]
              binarySearch(xs, 3)
                           ^
<console>:11: error: type mismatch;
 found   : Int(3)
 required: A
              binarySearch(xs, 3)
                               ^

Int型はOrderedのサブ型ではないことが原因です。Int型はPredefでRichInt型への暗黙の型変換が定義されています。そしてRichInt型はOrderedのサブ型になっています。つまり、Int型はOrderedのサブ型ではないですが、暗黙の型変換によりOrderedのサブ型になることができます。

継承だけでなく、暗黙の型変換まで含めて限定したい場合は、<:ではなく<%を使います。これを可視境界と呼びます。可視境界を使うことでList[Int]型も2分探索できるようになります。

def binarySearch[A <% Ordered[A]](list: List[A], a: A): Boolean = list match {
  ・・・

型パラメータの変位指定アノテーション

型パラメータを使ったすごく簡単な例を以前書きました。Capsuleというケースクラスです。

scala> case class Capsule[A](x: A)

このCapsuleにShape達を格納してみましょう。そして、それらのオブジェクトをList[Capsule[Shape]]型のリストに格納してみましょう。

scala> val rec = Capsule(Rectangle(0,0,2,3))
rec: Capsule[Rectangle] = Capsule(Rectangle(0.0,0.0,2.0,3.0))

scala> val cir = Capsule(Circle(0,0,5))
cir: Capsule[Circle] = Capsule(Circle(0.0,0.0,5.0))

scala> val xs: List[Capsule[Shape]] = List(rec, cir)
<console>:17: error: type mismatch;
 found   : Capsule[Rectangle]
 required: Capsule[Shape]
Note: Rectangle <: Shape, but class Capsule is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
       val xs: List[Capsule[Shape]] = List(rec, cir)
                                           ^
<console>:17: error: type mismatch;
 found   : Capsule[Circle]
 required: Capsule[Shape]
Note: Circle <: Shape, but class Capsule is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
       val xs: List[Capsule[Shape]] = List(rec, cir)
                                                ^

変数reccirにオブジェクトを束縛できたところはいいです。問題はList[Capsule[Shape]]型のリストの要素として、Capsule[Rectangle]型とCapsule[Circle]型のオブジェクトを格納できないところです。原因はどこにあるのでしょうか?

シンプルなところから考えてみましょう。Capsule[Shape]型の変数に、Capsule[Rectangle]型のオブジェクトを束縛してみます。

scala> val shape: Capsule[Shape] = rec
<console>:16: error: type mismatch;
 found   : Capsule[Rectangle]
 required: Capsule[Shape]
Note: Rectangle <: Shape, but class Capsule is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
       val shape: Capsule[Shape] = rec
                                   ^

なんとエラーになってしまいました!RectangleはShapeのサブクラスであるにも関わらずです。エラーメッセージをよく見ると"type mismatch"と出力されています。RectangleがShapeのサブ型だからと言って、Capsule[Rectangle]がCapsule[Shape]のサブ型というわけではないのです。

このようにRectangleがShapeのサブ型なのにCapsule[Rectangle]型がCapsule[Shape]型のサブ型にならないことを非変(nonvariant)と呼びます。逆に、RectangleがShapeのサブ型である場合にCapsule[Rectangle]型がCapsule[Shape]型のサブ型になることを共変(covariant)と呼びます。

デフォルトでは非変ですが、変位アノテーションを使うことで共変にすることができます。共変にするには型パラメータの左側に"+"マークをつけます。このマークが変位アノテーションです。

scala> case class Capsule[+A](x: A)
scala> val rec: Capsule[Rectangle] = Capsule(Rectangle(0,0,2,3))
scala> val cir: Capsule[Circle] = Capsule(Circle(0,0,5))
scala> val shape: Capsule[Shape] = rec
scala> val xs: List[Capsule[Shape]] = List(rec, cir)

共変にしたことで、Capsule[Shape]型の変数にCapsule[Rectangle]型のオブジェクトを束縛することができました。

共変とは逆に、"-"マークをつけると反変(contravariance)になります。反変の場合はCapsule[Shape]型がCapsule[Rectangle]型のサブクラスになるような状態です。

いったん変位アノテーションを外して、メソッドを追加してみましょう。

scala> case class Capsule[A](x: A) {
     |   def replace(y: A) = Capsule(y)
     | }
scala> val a = Capsule(Rectangle(0,0,2,3))
scala> val b = a.replace(Rectangle(0,0,4,5))

この状態で変位アノテーションをつけてみます。

scala> case class Capsule[+A](x: A) {
     |   def replace(y: A) = Capsule(y)
     | }
<console>:16: error: covariant type A occurs in contravariant position in type A of value y
         def replace(y: A) = Capsule(y)
                     ^

コンパイルエラーです。エラーメッセージをよく読むと、"contravariant position"と出ています。このメッセージが出たら引数yには下限境界を指定しろ、という意味です。下限境界は先よどやった上限境界の逆、すまり、Aのスーパー型を指定できるような型パラメータです。下限境界は>:を使います。

scala> case class Capsule[+A](x: A) {
     |   def replace[B >: A](y: B) = Capsule(y)
     | }
defined class Capsule

scala> val rec: Capsule[Rectangle] = Capsule(Rectangle(0,0,2,3))
scala> val a: Capsule[Shape] = rec
scala> val b: Capsule[Shape] = a.replace(Circle(0,0,5))

さて、なぜ引数yに下限境界が必要なのか考えてみましょう。

Capsule[Rectangle]型のオブジェクトをCapsule[Shape]型の変数に束縛できました。これはRectangleがShapeのサブ型でありCapsuleが共変だからです。

次に、a.replace(Circle(0,0,5))とreplaceメソッドにCircle型のオブジェクトを指定しました。これは変数aの型がCapsule[Shape]型なので指定できてくれないと困ります。これを適切に処理するには、replaceメソッドに下限境界が指定されている必要があります。

仮に下限境界がなかったとしたら、Capsule[Rectangle]型のオブジェクト、つまり変数aに束縛されているオブジェクトのreplaceメソッドの引数の型はRectangle型になります。これではCircle型をa.replaceに渡すことができません。

この問題に対応できるようにするため、a.replaceはRectangle型のスーパー型を受け取れるようにしておく必要があります。そのため、A型を下限境界とする型パラメータBをreplaceメソッドの引数の型として定義します。

ややこしく感じますが、心配いりません。下限境界を指定し忘れてしまった場合は、先ほど見たようにコンパイラが教えてくれます。まずはコンパイラが教えてくれた内容に対応できるようになりましょう。

ScalaのAPIドキュメントを読んでみる

ScalaのAPIドキュメントでListを調べてみましょう。http://www.scala-lang.org/api/current/

コンパニオンオブジェクトとクラスに分かれていることが分かると思います。コンパニオンオブジェクトのapplyメソッドや、クラスのこれまでに使ってきたメソッドを見てみましょう。

またListの宣言部を見るとabstractなクラスであり、様々なトレイトを実装していることが分かります。Listクラス自体はabstractで実態はなんでしょうか。"Type Hierarchy"という項目を見ると、::というクラスとNilというシングルトンオブジェクトがサブクラスとなっており、それらが実態となります。

Intクラスも見てみましょう。"Type Hierarchy"という項目を見ると、RichIntなどへ暗黙の型変換されることが分かります。

今後の学習

お疲れさまです!Scala入門ハンズオンはこれで終了です!今後さらにScalaを勉強するにはどうしたらいいでしょうか?

Scalaの豊富なコレクションについては知っておいた方がいいです。性能特性も確認しましょう。

トレイトはほんの少ししかやってないですし、抽出子など入門ハンズオンでは説明できなかったScalaの機能がまだあります。Scalaという言語自体をもっと知りたい場合は以下の書籍を読みましょう。通称コップ本と呼ばれておりScalaのバイブル的な書籍です。Scala公式ドキュメントも載せておきます。

Scalaで何かつくるときはScala逆引きレシピという書籍があるとリファレンスとして役立つでしょう。ひしだまさんのサイトにもすごくお世話になります。

Webアプリケーションをつくりたい場合はPlay2というフレームワークがよく使われます。ハンズオン用のドキュメントがWeb上にあるのでやってみると良さそうです。

関数型プログラミングについてもっと学びたい、という場合は以下の書籍を読むといいと思います。Scalaを使って関数型プログラミングについて説明されています。

Twitter社によるベストプラクティスが書かれたEffective Scalaもおすすめです。

日本のScalaコミュニティのサイトはこちらです。

練習問題

  1. Day2でやったクイックソートを行うメソッドをListより抽象的なSeqに対応してください。
def quickSort(list: List[Int]): List[Int] = list match {
  case Nil      => Nil
  case x :: Nil => List(x)
  case x :: xs  => {
    val smallerOrEqual = for (y <- xs; if y <= x) yield y
    val larger         = for (y <- xs; if y > x ) yield y
    quickSort(smallerOrEqual) ++ List(x) ++ quickSort(larger)
  }
}
  1. quickSortメソッドを多相的なメソッドにしてください。List[Int]やList[Shape]、List[Double]で試してみてください。
  2. Day2の練習問題でやった2分探索木を多相的なデータ構造にしてください。
sealed abstract class Tree {
  def insert(x: Int): Tree
  def contains(x: Int): Boolean
}
case class Empty() extends Tree {
  override def insert(x: Int): Tree = Node(x, Empty(), Empty())
  override def contains(x: Int): Boolean = false
}
case class Node(a: Int, left: Tree, right: Tree) extends Tree {
  override def insert(x: Int): Tree =
    if (x <= a) Node(a, left.insert(x), right)
    else        Node(a, left, right.insert(x))

  override def contains(x: Int): Boolean =
    if (x == a)      true
    else if (x <= a) left.contains(x)
    else             right.contains(x)
}

今日出てきたキーワード

  • implicit
  • 暗黙の型変換
  • AnyVal
  • 暗黙の引数
  • activator
  • sbt
  • package
  • アクセス修飾子
  • import
  • object
  • シングルトンオブジェクト、コンパニオンオブジェクト
  • 可変長引数
  • トレイト
  • 上限境界、下限境界、可視境界
  • 変位指定アノテーション

要注意ポイント

  • implicitをつけたメソッドで暗黙の型変換できる
  • Predefシングルトンオブジェクトにいくつかの暗黙の型変換が定義されてる
  • 暗黙の型変換を使うと既存のクラスにメソッドを後付けできる
  • 暗黙の引数を使うとオブジェクトを色々なメソッドに引き廻すのが楽になる
  • activatorというツールでScalaアプリケーションをビルドしたり実行したりできる
  • activatorは、sbtというツールのうすくラップしたもの
  • List(1,2,3,4,5)というのは、Listシングルトンオブジェクトのapplyメソッドを呼び出している

Day1

問題1

scala> for(x <- List(1,2,3);
     |     y <- List(10,100,1000))
     |   yield x * y
res0: List[Int] = List(10, 100, 1000, 20, 200, 2000, 30, 300, 3000)

問題2

scala> for(x <- List(1,2,3);
     |     y <- List(10, 100, 1000)
     |     if x * y > 50)
     |   yield x * y
res1: List[Int] = List(100, 1000, 200, 2000, 300, 3000)

問題3

scala> for(x <- (2 to 100 by 2) if x % 7 == 3) yield x
res2: scala.collection.immutable.IndexedSeq[Int] = Vector(10, 24, 38, 52, 66, 80, 94)

問題4

scala> for(s <- List("Apple", "Banana", "Pineapple", "Lemon")) yield s.substring(0, 1)
res3: List[String] = List(A, B, P, L)

問題5

scala> for(x <- 1 to 100) yield {
     |   if (x % 15 == 0) "FizzBuzz"
     |   else if (x % 5 == 0) "Buzz"
     |   else if (x % 3 == 0) "Fizz"
     |   else x.toString
     | }
res4: scala.collection.immutable.IndexedSeq[String] = Vector(1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16, 17, Fizz, 19, Buzz, Fizz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, FizzBuzz, 31, 32, Fizz, 34, Buzz, Fizz, 37, 38, Fizz, Buzz, 41, Fizz, 43, 44, FizzBuzz, 46, 47, Fizz, 49, Buzz, Fizz, 52, 53, Fizz, Buzz, 56, Fizz, 58, 59, FizzBuzz, 61, 62, Fizz, 64, Buzz, Fizz, 67, 68, Fizz, Buzz, 71, Fizz, 73, 74, FizzBuzz, 76, 77, Fizz, 79, Buzz, Fizz, 82, 83, Fizz, Buzz, 86, Fizz, 88, 89, FizzBuzz, 91, 92, Fizz, 94, Buzz, Fizz, 97, 98, Fizz, Buzz)

問題6

scala> List("a", "b", "c", "d", "e", "f", "g") zip List(1, 2, 3)
res5: List[(String, Int)] = List((a,1), (b,2), (c,3))

問題7

直角三角形の斜辺をc、残りの2辺のうち長い方をa、短い方をbとする。

まずは三角形を3要素のタプルで表して、全ての組み合わせをつくってみる。

scala> for(c <- 1 to 10; a <- 1 to 10; b <- 1 to 10) yield (a, b, c)
res6: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((1,1,1), (1,2,1), (1,3,1), (1,4,1), (1,5,1), (1,6,1), (1,7,1), (1,8,1), (1,9,1), (1,10,1), (2,1,1), (2,2,1), (2,3,1), (2,4,1), (2,5,1), (2,6,1), (2,7,1), (2,8,1), (2,9,1), (2,10,1), (3,1,1), (3,2,1), (3,3,1), (3,4,1), (3,5,1), (3,6,1), (3,7,1), (3,8,1), (3,9,1), (3,10,1), (4,1,1), (4,2,1), (4,3,1), (4,4,1), (4,5,1), (4,6,1), (4,7,1), (4,8,1), (4,9,1), (4,10,1), (5,1,1), (5,2,1), (5,3,1), (5,4,1), (5,5,1), (5,6,1), (5,7,1), (5,8,1), (5,9,1), (5,10,1), (6,1,1), (6,2,1), (6,3,1), (6,4,1), (6,5,1), (6,6,1), (6,7,1), (6,8,1), (6,9,1), (6,10,1), (7,1,1), (7,2,1), (7,3,1), (7,4,1), (7,5,1), (7,6,1), (7,7,1), (7,8,1), (7,9,1), (7,10,1), (8,1,1), (8,2,1), (8,3,1), (8,4,1), (8,5,1), (8,6,1), (8,7,1), (8,8,1), (8,9,1), (8,10,1),...

ピタゴラスの定理が成り立つタプルだけを抽出するためフィルタを追加。さらに、aはcより短く、bはaよりも短くなることを考慮する。

scala> for(c <- 1 to 10;
     |     a <- 1 to c;
     |     b <- 1 to a;
     |     if a*a + b*b == c*c)
     |   yield (a, b, c)
res7: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((4,3,5), (8,6,10))

周囲の長さが24になる条件を追加。

scala> for(c <- 1 to 10;
     |     a <- 1 to c;
     |     b <- 1 to a;
     |     if a*a + b*b == c*c && a+b+c == 24)
     |   yield (a, b, c)
res8: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((8,6,10))

Day2

問題1

def sum(l: List[Int]): Int = l match {
  case Nil => 0
  case x :: xs => x + sum(xs)
}

問題2

def sum(l: List[Int]): Int = {
  def loop(l: List[Int], acc: Int): Int = l match {
    case Nil => acc
    case x :: xs => loop(xs, acc + x)
  }
  loop(l, 0)
}

問題3

def binarySearch(list: List[Int], a: Int): Boolean = list match {
  case Nil      => false
  case x :: Nil => x == a
  case _ => {
    val p = list.size / 2
    val m = list(p)
    if (a == m)     true
    else if (a < m) binarySearch(list.take(p), a)
    else            binarySearch(list.drop(p), a)
  }
}

問題4

sealed abstract class Tree {
  def insert(x: Int): Tree
}
case class Empty() extends Tree {
  override def insert(x: Int): Tree = Node(x, Empty(), Empty())
}
case class Node(a: Int, left: Tree, right: Tree) extends Tree {
  override def insert(x: Int): Tree =
    if (x <= a) Node(a, left.insert(x), right)
    else        Node(a, left, right.insert(x))
}

問題5

sealed abstract class Tree {
  def insert(x: Int): Tree
  def contains(x: Int): Boolean
}
case class Empty() extends Tree {
  override def insert(x: Int): Tree = Node(x, Empty(), Empty())
  override def contains(x: Int): Boolean = false
}
case class Node(a: Int, left: Tree, right: Tree) extends Tree {
  override def insert(x: Int): Tree =
    if (x <= a) Node(a, left.insert(x), right)
    else        Node(a, left, right.insert(x))

  override def contains(x: Int): Boolean =
    if (x == a)      true
    else if (x <= a) left.contains(x)
    else             right.contains(x)
}

問題6

sealed abstract class Tree {
  def insert(x: Int): Tree
  def contains(x: Int): Boolean
  def remove(x: Int): Tree
}
case class Empty() extends Tree {
  override def insert(x: Int): Tree = Node(x, Empty(), Empty())
  override def contains(x: Int): Boolean = false
  override def remove(x: Int): Tree = Empty()
}
case class Node(a: Int, left: Tree, right: Tree) extends Tree {
  override def insert(x: Int): Tree =
    if (x <= a) Node(a, left.insert(x), right)
    else        Node(a, left, right.insert(x))

  override def contains(x: Int): Boolean =
    if (x == a)      true
    else if (x <= a) left.contains(x)
    else             right.contains(x)

  override def remove(x: Int): Tree = (left, right) match {
    case (_: Empty, _: Empty) =>
      if (x == a) Empty()
      else        Node(a, Empty(), Empty())
    case (l: Node, _: Empty) =>
      if (x == a) l
      else        Node(a, l.remove(x), Empty())
    case (_: Empty, r: Node) =>
      if (x == a) r
      else        Node(a, Empty(), r.remove(x))
    case (l: Node, r: Node) => {
      val maxFromRight = maxValue(r)
      if (x == a)     Node(maxFromRight, l, r.remove(maxFromRight))
      else if (x < a) Node(a, l.remove(x), r)
      else            Node(a, l, r.remove(x))
    }
  }

  private def maxValue(node: Node): Int = node match {
    case Node(v, _: Empty, _: Empty) => v
    case Node(v, l: Node, _: Empty)  => List(v, maxValue(l)).max
    case Node(v, _: Empty, r: Node)  => List(v, maxValue(r)).max
    case Node(v, l: Node, r: Node)   => List(v, maxValue(l), maxValue(r)).max
  }
}

Day3

問題1

def product(xs: List[Int]) = xs.foldLeft(1)(_ * _)

問題2

def length[A](xs: List[A]): Int = xs.foldLeft(0)((acc, _) => acc + 1)

問題3

def reverse[A](xs: List[A]): List[A] = xs.foldLeft(Nil: List[A])((acc, x) => x :: acc)

問題4

def append[A](as: List[A], bs: List[A]): List[A] = as.foldRight(bs)(_ :: _)

問題5

case class User(id: Int, age: Int)

def findById(id: Int): Option[User] = {
  val db = Map(
    1 -> User(1, 19),
    2 -> User(2, 25),
    3 -> User(3, 30)
  )
  db.get(id)
}

def addAges(userIdParam1: Option[Int], userIdParam2: Option[Int], minAgeParam: Option[Int]): Int = {
  val result = for {
    minAge  <- minAgeParam.orElse(Some(20));
    userId1 <- userIdParam1;
    userId2 <- userIdParam2;
    user1   <- findById(userId1);
    user2   <- findById(userId2)
  } yield {
    val age1 = if (user1.age > minAge) user1.age else 0
    val age2 = if (user2.age > minAge) user2.age else 0
    age1 + age2
  }
  result.getOrElse(0)
}

Day4

問題1

def quickSort(list: Seq[Int]): Seq[Int] = list match {
  case Nil      => Nil
  case x :: Nil => Seq(x)
  case x :: xs  => {
    val smallerOrEqual = for (y <- xs; if y <= x) yield y
    val larger         = for (y <- xs; if y > x ) yield y
    quickSort(smallerOrEqual) ++ Seq(x) ++ quickSort(larger)
  }
}

問題2

def quickSort[A <% Ordered[A]](list: Seq[A]): Seq[A] = list match {
  case Nil      => Nil
  case x :: Nil => Seq(x)
  case x :: xs  => {
    val smallerOrEqual = for (y <- xs; if y <= x) yield y
    val larger         = for (y <- xs; if y > x ) yield y
    quickSort(smallerOrEqual) ++ Seq(x) ++ quickSort(larger)
  }
}

問題3

sealed abstract class Tree[A] {
  def insert(x: A): Tree[A]
  def contains(x: A): Boolean
}
case class Empty[A <% Ordered[A]]() extends Tree[A] {
  override def insert(x: A): Tree[A] = Node(x, Empty(), Empty())
  override def contains(x: A): Boolean = false
}
case class Node[A <% Ordered[A]](a: A, left: Tree[A], right: Tree[A]) extends Tree[A] {
  override def insert(x: A): Tree[A] =
    if (x <= a) Node(a, left.insert(x), right)
    else        Node(a, left, right.insert(x))

  override def contains(x: A): Boolean = 
    if (x == a)      true
    else if (x <= a) left.contains(x)
    else             right.contains(x)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.