目次
- なにこれ
- とりあえず触ってみる
- 変数の定義
- varとval
- 関数定義
- 単独で動くプログラムをつくってみる
- Scalaらしいループ
- 配列
- リスト
- タプル
- クラス
- メソッド
- シングルトンオブジェクト
- コンパニオンクラスとコンパニオンオブジェクト
- 基本型
- 基本リテラル
- 演算子
- 制御構造
- ローカル関数
- 関数リテラル
- 第一級オブジェクトとしての関数リテラル
- プレースホルダー
- 部分適用
- クロージャ
- 連続パラメータ
- 末尾再帰
- 高階関数
- カリー化
- 制御構造の生成
- 名前渡しパラメータ
- Scala的オブジェクト指向
- Scalaのクラス構造
- トレイト
- case class
- パターンマッチ
- Option型
- 応用的なパターンマッチ
- Listの実装
なにこれ
- コップ本にだいたい則ってScalaについてまとめたもの。
- Scala勉強会とかで使いたい。
- Scala処理系が入っている前提。
- コップ本19章ぐらいまで読んだのでその辺りまでとりあえずまとめたい(無理か)
- 13と14章は飛ばしました
- 18章も飛ばす
とりあえず触ってみる
- scalaコマンドを実行するとインタプリタが立ち上がる。
- 出力にはprintlnメソッドを使う。
- printlnメソッドはUnit型の唯一のメンバである()を返す→Javaのvoidに相当
変数の定義
- 上の定義の時にわざわざ型名(String)って書かなかった
型が明らかなときは型名を書かなくてもいい→型推論
- もちろん書いてもいい
- 変数の後ろにコロンを書いて型名
varとval
- 変数を定義するときにはvarとvalというキーワードが使える
- varは何度でも変更できる
- valは一度しか代入できない
- varはなるべく使わないほうがいい
- 変数の値をいつでも変えられるというのは強力だけど、同時にどこで変数の値が変わってしまったのかわかりにくくなる
- 極力valを使い、どうしてもパフォーマンスなどでvarのほうが良い時だけvarを使う。
関数定義
1 def 関数名(引数名:引数型, 引数名:引数型, ...):戻り値型 = 関数の式
- 具体的な例
- returnは省略できる
- 最後の式の結果が返される
- returnを省略して、かつ戻り値の型が明らかなら戻り値の方も省略できる
- 一行ならそもそも中括弧もいらない
1 def hello(name:String) = "Hello! " + name
単独で動くプログラムをつくってみる
- テンプレ
- 今はおまじないだと思いましょう
- argsはStringの配列になっていてmainメソッドの引数です。
- かけたらコンパイルします
scalac hoge.scala
- 実行します
scala hoge
- scalacじゃなくてfscを使うと便利です
- fscを使うと2回目以降コンパイルが速くなる
Scalaらしいループ
- argsの中身を順番に表示するプログラムを書いてみる
- こんなプログラムを書いてみよう
- scalaでは配列の添字は丸括弧
- インクリメントの演算子がないのでi += 1で我慢する
- 行末のセミコロンはいらない
- このプログラムは良くない
- varがあるから
- 配列にはforeachメソッドというのが用意されている
- 関数を引数にとってその関数を配列の要素に対して実行する
1 args.foreach((arg:String) => println(arg))
- argの型はStringなのはわかりきってるのでargの型名は省略できる
1 args.foreach(arg => println(arg))
配列
- 同じ型しか要素にとれない
- valで宣言しても要素の変更は自由
配列の生成
1 val hoge = Array[String](3)
配列への代入と参照
便利な配列の生成方法
1 val hoge = Array("Akari", "Kyoko", "Yui", "Chinatsu")
リスト
- 同じ型しか要素にとれない
- 要素の変更は(通常)不可能
- mutableなリストというのが特別に用意してあって、それを使えば良い。もちろん推奨はされない。
リストの生成
1 val hoge = List(2, 3, 5, 7)
リストの結合
リストの先頭に要素を追加
- これは実は下の糖衣構文になっている。つまりListの::というメソッドを呼び出してる
1 hoge.::(2)
- ::や:::のことをconsという
タプル
- 違う型の要素も持てる
- 要素の変更は不可能
- 言語の制約上22個しか要素が持てない
タプルを作る
1 val hoge = (1, "Kyoko", 2, "Akari")
タプルを使う
- タプルの要素番号はHaskellなどの慣習から1番スタートになっている
クラス
- Scalaはオブジェクト指向言語なのでクラスを持っている
- Javaとかと同じようにnewでクラスからオブジェクトを生成できる
1 val a = new Hoge()
- オブジェクトをval宣言の変数に代入しても、そのオブジェクトのフィールドがvar宣言されていれば再代入できる
メソッド
- メソッドの引数は勝手にvalで定義される
- 勝手に変えられないようにするため
- 戻り値がUnitのメソッドは=を省略できる。ただし=を省略すると中括弧を省略できない。
- メソッドをprivateにするには次のように記述する
1 private def hello() = "Hello, World!"
シングルトンオブジェクト
- Scalaは徹底したオブジェクト指向
- オブジェクトに属していないものの存在なんて許せない!!!!
- なのでstaticがありません
- そのかわりにシングルトンオブジェクトというのがある
シングルトンオブジェクトの定義
シングルトンオブジェクトの性質
- シングルトンオブジェクトはコードが実行されるときに自動的に生成される
- またあるスコープの中でひとつしかないことが保証される
- 多分後述
- 先ほど「単独で動くプログラムをつくってみる」の所でobjectうんたらと書いたのは、プログラム実行開始時にシングルトンオブジェクトを生成させてその中のmainメソッドを実行するというふうにScalaが仕組まれているから。
コンパニオンクラスとコンパニオンオブジェクト
- シングルトンオブジェクトと同じ名前のクラスを定義するとそれぞれコンパニオンオブジェクトとコンパニオンクラスと呼ばれるようになって、対で扱われるようになる。
- コンパニオンオブジェクトからはコンパニオンクラスにアクセスすることができる。
- コンパニオンオブジェクトはシングルトンオブジェクトなので、プログラム実行開始時に勝手に生成される。
- なのでstatic代わりに使える。
コンパニオンオブジェクトの例
基本型
- Scalaには次のような型がある
- 数値型
- Byte
- Int
- Long
- Short
- Double
- Float
- Char
- String
- Boolean
- 数値型
基本リテラル
整数リテラル
- Int, Long, Short, Byte型のリテラル。
- 10進、8進、16進表記可能。
浮動小数点リテラル
- 10進表記のみ。
- 指数表記もできる。
文字リテラル
- シングルクォートでUnicode文字を囲んだもの。
文字列リテラル
- 文字列をダブルクォートで囲ったもの。
- 改行するとそれもそのまま文字列として反映される
シンボルリテラル
- シンボルもやはり文字列を表すためのリテラル
- 次のように表記する
- 文字列リテラルとの違いは文字列へのシンボルリテラルであれば必ず同じオブジェクトへの参照になっているということ
Booleanリテラル
- TrueとFalseがある
演算子
- 演算子はメソッドです
- 演算子の種類はJavaに準ずるので適当にググってね
比較演算子について
- 1つだけ注意を要するのが比較演算子の==
- Javaではオブジェクトとしての等価性を判定するものだった
- Scalaでは内部的にJavaのequalsメソッドを呼び出しているので、==はオブジェクトの持っている値そのものの比較になる
演算子の優先順位について
- Scalaでは演算子の優先順位は演算子の名前で決まっている
- 演算子は特別な存在ではなく、ただのメソッドだから
- 例えば*メソッドは+メソッドよりも優先順位が高いとかいう決め方をしている
リッチラッパー
- ScalaではJavaの演算子に加えてもっと賢い演算子が使えるようになっている
- 例えば
- これはリッチラッパーというクラスによって用意されている。ふだん使うぶんにはリッチラッパーという存在を意識せずにそのまま使うことが出来る。
制御構造
if式
- if文じゃなくてif「式」です
- 値を返すから
- 条件式が真のときは「真の時の式」の結果が返ってくる
- 条件式が偽のときは「偽の時の式」の結果が返ってくる
- 関数定義の時と同じように式が複数あるときは最後の式の結果が返ってくる
- なので次のように書ける
while式, do-while式
- CとかJavaと同じ。
- こいつらはUnit型を返す
- 一応言っておくとwhileを使うような構文はたいていvarを伴うのでうんこ
- 使わないようにしましょう
- 再帰を使うのが常道
- というわけで練習がてら再帰で上のプログラムを書き換えてみましょう
for式
- for式を使いこなすのがScalaマスターへの第一歩です
- for「式」なのでやっぱり値を返す
- forが値を返すとはどういうことなのかは下のyieldに関する所を読んでね
for式の基本的な使い方
- コレクションの反復操作
- コレクションというのは配列とかListとかそのあたり
「member <- yuruyuri」の部分をジェネレータと呼ぶ
- memberはvalになっていて、ループが1巡するごとにyuruyuriから1つ要素を取り出してmemberという変数を新たに初期化しているイメージ
- memberという変数を生成しているのでジェネレータ
Range
- 順番に並んでいる数値に対して処理をしたいとき
フィルタリング
- for式では取り出す要素に対してif式で条件を追加できる
- ifはセミコロンで区切って何個でも並べられる
入れ子のfor式
<-を複数書くことで入れ子のfor式を1つのfor式でまとめて書くことができる。
1 for(i <- 1 to 9; j <- 1 to 9) println(i*j)
yield
- yieldはfor式の中に記述して使う。
- 下の例の場合for文が実行されるごとにyield i+1というのが実行される。これが実行されるとi+1の結果がfor文の返り値として返ってくるようになる。for文は繰り返し実行されるのでyieldによって返ってきた戻り値は蓄積されていって新しいコレクションになる。つまりこの結果hogeには(1, 2, 3, 4, 5, 6)という配列が格納されることになる。
1 val hoge = for (i <- 0 to 5) yield i+1
- yieldの後ろで関数を呼び出すこともできるし、ブロックを書くこともできる。
try-catch-finally
- ScalaではJavaと違って例外をかならずcatchしなくても良い
- 次のように書きます
- catch節での例外名の記述を端折るとすべての例外をcatchする式になります
- caseは複数並べて複数の例外に対応出来ます
- tryもcatchもブロック式なので、最後の式の結果が全体の結果として返る
- 例外が発生しなかったときはtry節の最後の式の結果
- 例外が発生したときは対応する例外の処理の最後の式の結果
- finally節には例外の有無にかかわらず行いたい処理を書く
- finally節に値を返すような処理を書くべきではない
- tryやcatchで返された値を上書きしてしまう可能性があるから
- 例外の有無にかかわらず行いたい副作用を伴う処理に限って書くべき
- finally節に値を返すような処理を書くべきではない
match式
- こいつも強力な式なのだが、とりあえず最も基本的な使い方であるswitch文の代わりとしての使用法を見てみる
- match式は式なので次のように書くこともできる
- 副作用を伴う処理をmatch式の外に出してしまえるのでこっちのほうがいいかも
ローカル関数
- Scalaでは関数の中に関数を定義することができる→ローカル関数
- 上のaddとsubという関数はcalc関数の中でしか使用できない
- ローカル関数からローカル関数の外の変数にアクセスすることは可能
関数リテラル
- 「Scalaらしいループ」というところでforeachメソッドを用いた例を示した
- そこで以下の様な表現があった
1 arg => println(arg)
- これはargという引数をとって、println(arg)という処理を実行する関数になっている
- より関数型っぽく言えばargという引数に対してprintln(arg)というものを返す関数である
- Scalaではこのように名前の無い関数をその場で生成して使うことができる。これを関数リテラルとか無名関数などと呼ぶ
第一級オブジェクトとしての関数リテラル
- 第一級オブジェクトなどというと仰々しいが、要するに関数リテラルは文字列や数値と同じようにScalaでは扱われていますよということ
- 同じように扱われているので変数に代入できる
プレースホルダー
- ある引数が関数リテラルの中で一度しか使われないときは名前を書かずアンダーバーで代用することができる。
- 型を推論できないような書き方をすると怒られる
- 型を明示すれば良い
- アンダーバーで置き換えられるのは1回だけなので、アンダーバーの個数だけ引数があるとみなされる
- だからアンダーバーが例えば2つあれば2引数の関数なのだ、という認識をコンパイラはするわけ
部分適用
- アンダーバーによる置き換えは関数の引数リストに対しても適用出来る。
- 上の操作によってsum関数の第2引数に3を与えた引数を2つとる関数を新しく作ることが出来た
- このように関数に引数の一部分だけを適用して新しい関数を得ることを部分適用という
- 上の操作をした上で下のような操作をすることができる
- 部分適用とは言うものの1つも引数を適用しないこともできる
- 上の操作の結果sum2にはsumと同じ挙動をする3引数の関数が格納されている状態になる
部分適用の内部的動作
- 次のコードを考える
- sum2にはsumに引数を0個部分適用した結果の2引数関数のオブジェクトが格納されている
- sum2に格納されているものは実は厳密にはsumで定義されている関数そのものではない
1 sum2(2, 3)
- 上の式を評価するとsum2オブジェクトが持っているapplyというメソッドが呼び出され、sum2に対応する関数、すなわちsum関数が取り出され適用されるという形になっている
アンダースコアの省略
- 「その関数式を記述する箇所が関数呼び出し必須である」ようなときアンダースコアを省略することも可能である
クロージャ
束縛変数と自由変数
1 (x:Int) => x + more
- 上の関数リテラルでxを束縛変数、moreを自由変数と呼ぶ
- xは関数の引数でInt型であるという事がわかっていて、関数の中で文脈的意味を与えられている
- moreは突然出てきた変数で、文脈的意味が与えられていない
- 自由変数も関数リテラルの外でちゃんと意味を与えてやれば(束縛するという)機能する
開いた項と閉じた項
- 自由変数がある関数リテラルを開いた項という。
- 自由変数がない関数リテラルを閉じた項という。
クロージャの定義
- 開いた項の関数リテラルが束縛された自由変数を掴んで閉じた項になった結果生まれた関数
- クロージャはmoreの値を持っているわけではなく、more自体への参照を持っているのでmoreをいじればクロージャの中でのmoreの値も変わる。
- 逆にクロージャから自由変数を変更することもできる
ローカル関数としてクロージャを定義する
- ローカル関数としてクロージャを定義し、そのクロージャの自由変数をクロージャの外側の関数の引数として与えてみる
- 自由変数moreはmyadd関数の中でのみ通用する変数になっていて、クロージャが生成された時にmyaddに与えられた値がmoreの値になっている
連続パラメータ
- Scalaでは関数の「最後の」引数として可変長引数が渡せる
- 型指定の後ろにアスタリスクをつける
1 def echo(args:String*) = args.foreach(println)
- 連続パラメータは内部的にはArrayとして扱われているが、Arrayを引数として渡そうとすると怒られる
- Arrayごと渡したいときは後ろに:_*とかく
末尾再帰
- Scalaは関数型言語なのでループを書く際にはwhileなどを使うのではなくなるべく再帰するのがよい
- ただし末尾再帰になるように書けば再帰で書いても内部的にはただのループに置き換えられるので関数呼び出しのオーバヘッドが発生しないのでおすすめ
- もちろんforeachメソッドなどが使えるのであればそちらを使うほうが良い
高階関数
- 関数を引数にとったり関数を返したりする関数
- foreachメソッドは紛れもない高階関数!
高階関数の例
- 膨大な量の文字列のリストlistがあると仮定する
- これに対して次のようなものを探したい
- 先頭が指定文字列と合致する文字列
- どこかしらが指定文字列と合致する文字列
- 指定した正規表現とマッチする文字列
- 普通に書くとこのようになる
- ぶっちゃけ一部を除いてほとんど同じ内容の関数なので、何とかして共通な部分をくくりだしたい
- 次のような関数を作る
- matcherはStringを2つ受け取ってBooleanを返す関数式
- 上のlistMatchingを使えばlistEnds関数は次のように書ける
- プレースホルダーを使ってfilesMatching関数の2引数は省略することが出来て
1 def listEnding(query:String) = listMatching(query, _.endsWith(_))
- クロージャを使えば更に短縮することができる
- listMatching関数で使われるqueryをlistMatching関数で束縛するのではなくて外側のlistEnding関数で束縛している。
高階関数もう一例
- existsメソッドというのがある。existsメソッドはコレクションに適用されるメソッドで、コレクションの要素の中に一つでも与えられた関数式を満足するものがあればTrueを返し、そうでなければFalseを返すというものである。
- 数値のリストが与えられた時負の数が含まれているかどうかを判定する
1 list.exists(_ < 0)
- existsの引数が関数式になっているのでこれも高階関数
- 嘘だと思う人はこのように書き換えてみよう
1 list.exists(x => x < 0)
カリー化
- 一つの引数リストではなく複数の引数リストに対応できるような関数をカリー化された関数という
- カリー化されている関数はどうなっているのか
- curriedSum関数は引数xをとってyという1引数の関数にまず化けている
- そのあと引数yをとってInt型の数値を返している
- 2段階になってるわけね
- だから以下の関数とcurriedSumは同じ意味
- curriedSum2はxを引数にとって「yを引数にとる関数リテラル」を返している
1 def curriedSum2(x:Int) = (y:Int) => x + y
カリー化された関数にプレースホルダーを使ってみる
- カリー化された関数はプレースホルダーを使って引数を固定できる
3引数以上で試してみる
- プレースホルダーは引数全体を一つのアンダースコアで置き換えるので、複数の引数を置き換えるときも1つだけ書けば良い
カリー化するメリット
- カリー化された関数は部分適用がしやすくなる
- 例えば足し算をする関数をカリー化しておけば、簡単に適当な数値を部分適用して、その数値の分インクリメントする関数を生成できる
制御構造の生成
- ファイルを開いて書き込んで閉じる操作を行う関数を定義してみる
opはファイル書き込みのためのPrintWriterオブジェクトを引数にとってファイルに実際に書き込み操作を行う操作を定義している関数
- 操作を定義している関数を外から引数として与えられるようにしたので、ファイルを開いて何か操作をしてファイルを閉じるという操作だけを切り出した関数を作ることができる
- このような書き方をopに対してファイルを開いて貸してあげているようにみえるため、「ローンパターン」という
- 引数が1つだけのメソッドは丸括弧の代わりに中括弧を使ってもいい
- 上のwithPrintWriterをカリー化して中括弧を使えるようにしてみよう
- この関数に与えている処理を表す関数が中括弧で囲まれたことで、この部分が処理に対応していることが一目瞭然になった
名前渡しパラメータ
- 次のようなプログラムを考える
1 object MyLog {
2 val isDebug = true
3
4 def main(args: Array[String]) {
5 log(heavyFunc)
6 }
7
8 def log(s: String) = { //heavyFuncのデバッグログを取るための関数
9 if(isDebug) {
10 println("call log")
11 println(s)
12 }
13 }
14
15 def heavyFunc() = {
16 println("call heavyFunc") //実際には重い処理が用意されていると考える
17 "hoge"
18 }
19 }
- このプログラムを実行すると次のような結果になる
- main関数でlog(heavyFunc)を実行するときにheavyFuncが評価されてしまうためこうなってしまう
- isDebugをfalseにしてもheavyFuncはやっぱり評価されるので、"call heavyFunc"が出力されて、"hoge"が返される
- "hoge"はいらないのに!!!
- isDebugをfalseにしてもheavyFuncはやっぱり評価されるので、"call heavyFunc"が出力されて、"hoge"が返される
- heavyFuncの返り値が必要になるまでheavyFuncの評価をしないでほしい
- 名前渡しする
引数に=>をつけて関数を定義するとそこは名前渡しとなって、実際に戻り値が必要になるまで評価されなくなる
こんなことしなくても関数を渡すようにすればいいんじゃね
- この書き方だと関数じゃなくて文字列を直接渡したいときに苦労する
Scala的オブジェクト指向
- コップ本10章に則って、Scalaにおけるオブジェクト指向の実現手法について見てみる
- 簡単な2D描画をするライブラリを作る
ライブラリの仕様
- 2Dオブジェクトはelemというメソッドで生成する
- 生成したオブジェクトはaboveやbesideというメソッドを使って2つを結合することができる
- 2Dオブジェクトは文字列表現の2次元矩形として定義する
抽象クラス
- 2Dオブジェクトに対応するElementというクラスを用意する
- contentsは矩形を表現するための文字列の配列
- Elementは抽象クラスになっている
引数なしメソッドの定義
- 要素の高さを調べるheightメソッドと幅を調べるwidthメソッドを用意する
- 別にdefで定義する必要はなくて、valで定義してしまってもいい
- 関数がフィールドになりますね
- このようにScalaではフィールドとメソッドのアクセス方法に区別がない
- フィールドにすると初回にその結果を保持するので高速になるがメモリを食う
- メソッドだと毎回計算数するが、メモリは食わない
クラスの継承
Elementを継承してArrayElementをつくる
- Elementは抽象クラスなのでcontentsメソッドを実装しないといけない
メソッドとフィールドのオーバーライド
- Scalaではメソッドとフィールドのアクセス方法が同じなので、メソッドとフィールドを相互にオーバーライドできる
コンストラクタ
- Scalaではコンストラクタのための専用の構文はない
- クラス定義の直下にベタ書きした式がそのままコンストラクタになる
- 引数付きコンストラクタを定義したいときは次のように書く
補助コンストラクタ
- 複数の引数リストのためのコンストラクタを定義したいときは補助コンストラクタを使う
- 補助コンストラクタは複数定義できて、基本コンストラクタか別の補助コンストラクタを必ず呼び出さないといけない
パラメータでフィールドを初期化してしまう
- コンストラクタのパラメータがフィールド初期化のためのパラメータなら次のように略して書くこともできる
- valじゃなくてvarにすれば再代入できるフィールドにできる
- privateとかをつけることもできる
1 class ArrayElement(private val contents:Array[String]) extends Element
スーパークラスのコンストラクタ呼び出し
- 高さが1の(即ち線になっている)ようなオブジェクトのためのクラスを作ると便利かも
LineElementというのを作ります
override修飾子
- 親クラスで実装済みのメソッドをオーバーライドする時は必ずメソッドの前にoverrideを書かないといけない
- 親クラスのメソッドが抽象メソッドなら別に要らない
- さっきも付けなかったよね
ポリモーフィズム
指定された幅と高さの範囲を、指定された文字で埋め尽くすElement形式であるUniformElementを定義してみる
- 今まで定義したクラスはElementを拡張して作ってきたのでElementの一族として扱える
- Scalaでは呼び出されるメソッドは変数の型ではなく、オブジェクトがどのクラスから生成されたかに依存する
1 abstract class Element {
2 def demo() {println("Element's implementation invoked")}
3 }
4
5 class ArrayElement extends Element {
6 override def demo() {println("Array Element's implementation invoked")}
7 }
8
9 class LineElement extends ArrayElement {
10 override def demo() {println("Line Element's implementation invoked")}
11 }
12
13 class UniformElement extends Element
final
- サブクラスにオーバーライドさせたくないものにはfinalをつける
- クラスそのものをそもそも拡張させたくないときはクラス自体にfinalをつければよい
合成と継承
- 既存のクラスを利用する方法には合成と継承がある
- 合成はクラスの持つフィールドのデータ型として別のクラスを指定することで、あるクラスから別のクラスを利用しようとする方法
- オーバーライドを伴わないので、スーパークラスを書き換えるとサブクラスが使えなくなる、という問題を回避できる
- オーバーライドを多用するより抽象メソッドの実装という形で実装を心がけるようにしたい
上の例だとLineElementはArrayElementに対して合成の関係ではない
ArrayElementにcontentsフィールドの定義を任せてそれを継承して使用しているため
合成の関係にするためにはLineElementをElementの直下に来るようにする
contentsはArrayElementで定義されたものを継承したのではなく、自前でArrayクラスをデータ型として指定したものとなっている
- この辺の感覚はトレイトを学ぶとつかめるようになる気がする
引数をとるメソッドを定義
- オブジェクトを連結するメソッドabove, besideを定義します
- zip演算子は2つの配列から1つずつ要素を取り出してそれを組み合わせた要素を生成し、新しい配列を作るという演算子
- 出力用のtoStringメソッドも定義しょう
- toStringはScalaのオブジェクト全てに定義されているので、overrideしないといけない
ファクトリーメソッド
- コンパニオンオブジェクトを使って実装するのが一番いい
- 引数リストの違いに応じて呼び出すコンストラクタを変えることによって、コンストラクタの違いを意識すること無くオブジェクトを生成できる
ArrayElement, UniformElement, LineElementはコンパニオンオブジェクトからコンストラクタを呼び出せればいいのでコンパニオンオブジェクトの中で定義してしまうことにする
1 object Element {
2 private class ArrayElement(val contents:Array[String]) extends Element
3
4 private class LineElement(s:String) extends Element{
5 val contents = Array(s)
6 override def width = s.length
7 override def height = 1
8 }
9
10 private class UniformElement(
11 ch:Char,
12 override val width:Int,
13 override val height:Int
14 ) extends Element {
15 private val line = ch.toString * width
16 def contents = Array.make(height, line)
17 }
18
19 def elem(contents:Array[String]):Element = {
20 new ArrayElement(contents)
21 }
22
23 def elem(char:Char, width:Int, height:Int):Element = {
24 new UniformElement(char, width, height)
25 }
26
27 def elem(line:String):Element = {
28 new LineElement(line)
29 }
30 }
- 最後にElementクラスの定義でコンストラクタを呼び出しているところをファクトリーメソッドに置き換えれば完成
- 省略するのでじぶんでやってみよう
Scalaのクラス構造
Anyクラス
AnyValクラス
- Scalaにおける値をあらわすクラスの親クラス
- ByteとかIntとかいうクラスの親
AnyRefクラス
- Scalaにおける参照クラスの親クラス
- Scalaでクラスを定義するとこいつの子になる
Javaのプリミティブ型との関連
- Scalaでの数値演算は内部的にはJavaのプリミティブ型に変換して行われている模様
- 数値をオブジェクトとして扱う必要性が出てきたときは、自動的にオブジェクトとして扱ってくれる
- Scalaでは==はオブジェクトの値の等価性を判定するためのメソッド
- 内部的にJavaのequalsメソッドを呼び出している
- Javaの==と同じように同じオブジェクトであるかどうかを判定したいときはeqメソッドを使う
Nullクラス
- すべての参照クラスのサブクラスになる
- 参照になにもないことを表す
- 参照クラスのサブクラスだから値型とは互換性がない
- 例えばInt型の変数にNullを突っ込もうとすると怒られる
Nothingクラス
- すべてのクラスのサブクラスになっている
- どんな型にも化けられる!
- エラーが発生した時にこいつを返すようにするとエラー判定に便利
1 def error(message:String):Nothing = throw new RuntimeException(message)
トレイト
トレイトを定義する
- 見た感じクラスと同じ感じ
トレイトを使う
- 既存のクラスにミックスインして使う
- Java風に言えば実装する
- すでにextendsを使っているときはwith, 使ってないときはextends
スーパークラスを持っているクラスにミックスイン
複数トレイトのミックスイン
- クラスと違いトレイトは複数ミックスインできる
トレイトのメソッドのオーバーライド
- トレイトで定義されたメソッドはオーバーライドできる
トレイトとは結局何か
- コンストラクタ引数をとれないクラスぐらいに考えてしまっていい
- トレイトは道具箱のようなもの
- クラスという作業場を用意しておいて、必要なメソッド(道具)をトレイトから引っ張り出してくるイメージ
- トレイトを使うことで貧弱なインタフェースに処理を付け足してリッチなインターフェースを作ることができる
トレイトを実際に使ってみる
- Scalaに標準で用意されているOrderedトレイトというのを使ってみる
- 有理数を提供するクラスRationalというものがあるとする
こいつに>, >=, <, <=というメソッドを用意したい
- 2つのRationalを引数にとってBooleanを返すメソッド
- 普通に頑張ってそれぞれのメソッドを実装してもいいけど、Orderedトレイトを使うと簡単に実装できる
積み重ね可能な変更
- 整数のキューを考える
- 整数を追加するときに整数を2倍にして追加できるようにするトレイトをつくってみる
トレイト宣言でextendsキーワードを用いるとIntQueueクラスを拡張したクラスにしかミックスインできないようになる
- extendsで明示的に親クラスを指定することでsuperキーワードを使って親クラスのメソッドを参照できる
実際にはIntQueueクラスではputメソッドは抽象メソッドになっているので、具体的な実装を行なっているBasicIntQueueクラスのputメソッドが呼び出される
- という認識でいいのかな
- abstract overrideと書くことによってコンパイラに対して、既存のputメソッドに対して更に変更を加えているんだよということを示している
- いちいち使い捨てのクラスを作るのもアレだよね
- newするときに一緒にミックスインの指定ができる
- 複数のトレイトで積み重ねをやってみる
- 上の挙動から複数のミックスインは右から行われることがわかる
- つまりトレイトを適用する順番によって挙動が変わる!!
- これは強力だけど、同時に危険性もある
多重継承ではなくトレイトによるミックスインを採用した理由
- 菱型継承をした時の問題を解決するため
ミックスインの線形化
- 複数のトレイトをミックスインするときどの順番にミックスインされるのか
- 自分から始めて右から順番に適用されると思っておけばよいかと
case class
- クラス宣言の前にcaseというキーワードをつけると色々と便利なクラスであるcase classを作ることができる。
ケースクラスの利点
- インスタンス化するときにnewを付けなくていい
- ケースクラスには必ずコンパニオンオブジェクトがセットで勝手についてくるから
- getterが勝手に生成される
- 以下の例の場合nameという名前のname変数のgetterが勝手に生成されている
- equals, hashCode, toStringが自動生成される
- ケースクラスはパターンマッチの条件として使うことができる
- 後述
パターンマッチ
基本的なパターンマッチ
- 基本的なパターンマッチは次のように書く。前にやったmatch式を使ったものがそれ。
型によるパターンマッチ
- 値ではなくてその変数の型によってマッチングすることもできる
変数を使ったパターンマッチ
- マッチした結果を変数に突っ込んで使うことができる
case文とmatch式の違い
- Javaのcase文とScalaのmatch式の違い
- match式は値を返す
- 該当するものがあった時、それ以降のものは評価されない
- 条件は全てのパターンを網羅しないといけない
ワイルドカードによるマッチ
- 以下次のサンプルコードを使う
- あらゆるオブジェクトにマッチするパターンはアンダースコアで表す。
- 今までにも使ってきたよね
- ワイルドカードはパターン全体ではなくパターンの条件の要素として使うこともできる
定数パターンマッチ
- リテラルを使って完全一致でパターンマッチする
定数パターンマッチの注意点
- 定数パターンマッチにはシンボル名を使うことができる
- Scalaの字句解析では小文字で始まる単語は変数、大文字で始まる単語は定数としてみなされる
- この辺りで混乱が発生することがある
- Piを小文字にすると問題が起きる
- これを防ぐためにはpiをバッククオートで囲めば、piが定数扱いになる。
シーケンスパターン
- ListやArrayとマッチングする。
- 先頭が0で要素が3つあるようなListとだけマッチングする例
- 「_*」を使うことによって任意の個数の要素とマッチングできる
- 先頭が0であるようなListとマッチングする例
タプルパターン
- Listで出来るんだからタプルでもできる。
パターンの変数への束縛
パターンを<変数名> @ [パターン]という形で変数に束縛できる
1 abstract class Expr
2 / 数値定義クラス
3 case class Number(num:Double) extends Expr
4 // 単項演算クラス
5 case class UnOp(opearator:String, arg:Expr) extends Expr
6
7 // 絶対値演算2回を1回に置き換える処理
8 // 絶対値演算は1回やっても2回やっても同じ結果になる
9 def test(expr:Expr) = expr match{
10 case UnOp("abs", e @ UnOp("abs", _)) => e
11 case _ =>
12 }
ケースクラスによるマッチング
- ケースクラスは定義するだけでマッチングに使用することができる
1 abstract class Name
2 case class FirstName(name: String) extends Name
3 case class LastName(name: String) extends Name
4 def printName(name:Name): Unit = {
5 name match {
6 case FirstName(name) => println(name)
7 case LastName(name) => println(name.toUpperCase)
8 case _ =>
9 }
10 }
11 printName(FirstName("Martin")) // "Martin"
12 printName(LastName("Odersky")) // "ODERSKY"
13
パターンガード
- パターンには変数を一度しか登場させることが出来ない
- パターンにはif式を使って条件を付け加えることができるのでそれを利用する
sealed class
- クラス宣言にsealedというキーワードを付けることで同じファイルのクラスからしか継承できないようになる
- サブクラスが必ず同じファイルにあることが保証される
- クラス型を使ったパターンマッチングの時sealed classを使うことで、ファイルの中のサブクラスを調べて、サブクラスをmatch式がきちんと網羅しているかをコンパイル時に調べてくれる
- 下の式はPiyoをmatch式が網羅していないので、コンパイル時にWarningが出る
- Piyoに関するcaseも追加すると、今度はエラーになる
- FugaにもPiyoにもマッチングしなかった時の条件が書かれていないから
- Piyoに関するcaseも追加すると、今度はエラーになる
Option型
- Option型は値があるかどうか(Nullかどうか)わからないものを扱うときに使う型
- これをうまく使えばぬるぽを回避できるぞ!!!
- Option型には2つの値がある
- 値があることを表すSome(あーだこーだ)
- 値がないことを表すNone
Option型を返す関数を作る
- ファイルが存在すればファイルに対応するオブジェクトを返し、なければNoneを返す関数
Option型のメリット
- Option型を返すような関数は確実に意味のある値を返すかどうかわからないということ
- プログラミングするときに気をつけられる
- Option型はそのままでは普通の値として扱えないので型によって未然に操作ミスを防ぐことができる
Someから値を取り出す
- 値が存在するときSomeに値を入れて返すが、このままでは取り扱えないので取り出す必要がある。
- Someにはgetというメソッドがあるのでそれを使えば取り出せる
- でもこれをNoneに対して実行すると例外が投げられる
- マジ使えねえ、うんこ。
- パターンマッチを使うのが常道
- Option型はsealed class
- Someはcase classになっている
- Some(f)という形でパターンマッチすることで、自然にfにSomeの中身を取り出すことができる
応用的なパターンマッチ
変数定義の際のパターンマッチ
- varやvalを定義するときにパターンマッチを行なって複雑な代入をすることができる
関数リテラルの中でパターンマッチする
- 関数リテラルの中にパターンマッチを記述することができる
for式でのパターンマッチ
Listの実装
List型
- List型はList[T]という型になっている。TにはStringとかIntとか他の型名が入る。
- List[S]とList[T]があって、SがTのサブ型であればList[S]はList[T]のサブ型になる
- 空のリストはList[Nothing]型
- Nothingは全てのクラスのサブクラスなので、空のリストは全てのリストの型に対してサブ型になっている
Listの生成
- リストの要素を直接突っ込む方法を今まで使ってきた
- ::(cons)を使って構成する方法もある
- 今まで使ってきた方法は全てこのconsを使った方法の言い換えだった
Listの基本操作
- head...リストの先頭要素を返す
- tail...リストの先頭を除いた要素から構成されるリストを返す
- isEmpty...リストが空リストならtrueを返す
Listの様々な操作
- length...リストの長さを返す
- last...リストの最後の要素を返す(headと比べて重い)
- init...リストの最後の要素を除いた要素から構成されるリストを返す(tailと比べて重い)
- reverse...リストを反転したリストを返す
- take...整数の引数nをとって先頭n個の要素を返す
- drop...整数の引数nをとって先頭n個以外の要素を返す
- splitAt...整数の引数nをとってそこの添字でリストを分割して2つのリストをタプルに突っ込んで返す
- apply...整数の引数nをとってn番目の要素を返す
- indices...リストの添字のリストを返す
- zip...2つのリストの各要素をまとめたタプルを要素とするリストを返す
- toArray...リストを配列に変換する
- toList...配列をリストに変換する
- copyToArray...指定された添字の位置からリストの要素を置換して配列にする
- elements...リストに対するイテレータを返す。以下イテレータのメソッド。
- hasNext...次の要素があるかどうかを調べる
- next...次の要素を返す
リストに適用する高階メソッド
- map...リストの各要素に引数に与えた関数を適用する
- foreach...リストの各要素に引数に与えた手続きを適用する
- 手続きとは戻り値がUnit型であるような関数
- 戻り値がUnit型なので結果をリストとして回収することは出来ない
- filter...引数に与えた論理式に合致する要素だけを取り出す
- partition...引数に与えた論理式に対して(合致した要素を集めたリスト, 合致しなかった要素を集めたリスト)というタプルをつくって返す
- find...引数に与えた論理式に対して合致する最初の要素を返す。これはOption型を返す関数。
- takeWhile...引数に与えた論理式に当てはまる要素を頭から順番に取れるだけ取って行って、当てはまらない要素にぶつかったらそこで終わりにする関数
- dropWhile...takeWhileは条件に当てはまるものを拾っていくが、dropWhileは逆に捨てる
- span...takeWhileした結果とdropWhileした結果をタプルにして返す
- exists...引数に与えた条件に当てはまる要素が一つでもあればtrueを返す
- forall...引数に与えた条件にすべての要素が当てはまればtrueを返す
畳み込み
- 左畳込み
- (z /: list) (二項演算) という書式で書く
- まずzとlistの1つ目の要素について二項演算を行う
- その結果とlistの2つ目の要素について二項演算を行う
- 繰り返し
- (z /: list) (二項演算) という書式で書く
- 右畳込み
(list
z)(二項演算)
- zとlistの一番最後の要素について二項演算を行う
- その結果とlistの後ろから2番めの要素について二項演算を行う
- 繰り返し
Listコンパニオンオブジェクトに実装されているメソッド
- List.apply……リストの生成を行う
- List.range……整数の範囲を生成する
- List.range(from, until)でfrom以上until未満の整数のリストを生成
- 3つ目のパラメータを指定するといくつ刻みにするかを決められる
- List.make……同じ値が並んだリストをつくる
- List.make(num, element)でnum個のelementが並んだリストが生成される
- List.unzip……zipでまとめたリストを再びバラす。バラされたリストはタプルに突っ込まれて返ってくる
- List.flatten……リストの中にリストが入れ子になっているようなものを引数にとって、その要素からなるリストを生成して返す
- List.concat……複数のリストを引数にとってひとつに結合したリストを返す
- map2, forall2, exists2……2つのリストと2つの引数をとる関数をもらって、map, forall, exists的なことをする関数