21081
コメント:
|
50987
|
削除された箇所はこのように表示されます。 | 追加された箇所はこのように表示されます。 |
行 549: | 行 549: |
{{{#!highlight scala (x:Int) => x + more }}} * 上の関数リテラルでxを束縛変数、moreを自由変数と呼ぶ * xは関数の引数でInt型であるという事がわかっていて、関数の中で文脈的意味を与えられている * moreは突然出てきた変数で、文脈的意味が与えられていない * 自由変数も関数リテラルの外でちゃんと意味を与えてやれば(束縛するという)機能する {{{#!highlight scala var more = 1 (x:Int) => x + more }}} === 開いた項と閉じた項 === * 自由変数がある関数リテラルを開いた項という。 * 自由変数がない関数リテラルを閉じた項という。 === クロージャの定義 === * 開いた項の関数リテラルが束縛された自由変数を掴んで閉じた項になった結果生まれた関数 {{{#!highlight scala var more = 1 val f = (x:Int) => x + more //これがクロージャ }}} * クロージャはmoreの値を持っているわけではなく、more自体への参照を持っているのでmoreをいじればクロージャの中でのmoreの値も変わる。 {{{#!highlight scala var more = 1 var f = (x:Int) => x + more println(f(3)) //4が出力される more = 10 println(f(3)) //13が出力される }}} * 逆にクロージャから自由変数を変更することもできる {{{#!highlight scala val list = List(1,2,3) var sum = 0 list.foreach(sum += _) println(sum) //6が出力される }}} === ローカル関数としてクロージャを定義する === * ローカル関数としてクロージャを定義し、そのクロージャの自由変数をクロージャの外側の関数の引数として与えてみる * 自由変数moreはmyadd関数の中でのみ通用する変数になっていて、クロージャが生成された時にmyaddに与えられた値がmoreの値になっている {{{#!highlight scala def myadd(more:Int) = (x:Int) => x + more val add10 = myadd(10) val add100 = myadd(100) println(add10(10)) //20が出力される println(add100(10)) //110が出力される }}} == 連続パラメータ == * Scalaでは関数の「最後の」引数として可変長引数が渡せる * 型指定の後ろにアスタリスクをつける {{{#!highlight scala def echo(args:String*) = args.foreach(println) }}} * 連続パラメータは内部的にはArrayとして扱われているが、Arrayを引数として渡そうとすると怒られる * Arrayごと渡したいときは後ろに:_*とかく == 末尾再帰 == * Scalaは関数型言語なのでループを書く際にはwhileなどを使うのではなくなるべく再帰するのがよい * ただし末尾再帰になるように書けば再帰で書いても内部的にはただのループに置き換えられるので関数呼び出しのオーバヘッドが発生しないのでおすすめ * もちろんforeachメソッドなどが使えるのであればそちらを使うほうが良い |
|
行 552: | 行 619: |
* 関数を引数にとったり関数を返したりする関数 * foreachメソッドは紛れもない高階関数! === 高階関数の例 === * 膨大な量の文字列のリストlistがあると仮定する * これに対して次のようなものを探したい * 先頭が指定文字列と合致する文字列 * どこかしらが指定文字列と合致する文字列 * 指定した正規表現とマッチする文字列 * 普通に書くとこのようになる {{{#!highlight scala def listEnds(query:String) = { for(str <- list; if str.endsWith(query)) yield str } def listContains(query:String) = { for(str <- list; if str.contains(query)) yield str } def fileRegex(query:String) = { for(str <- list; if str.matches(query)) yield str } }}} * ぶっちゃけ一部を除いてほとんど同じ内容の関数なので、何とかして共通な部分をくくりだしたい * 次のような関数を作る * matcherはStringを2つ受け取ってBooleanを返す関数式 {{{#!highlight scala def listMatching(query:String, matcher:(String, String) => Boolean) = { for(str <- list; if matcher(str, query)) yield str } }}} * 上のlistMatchingを使えばlistEnds関数は次のように書ける {{{#!highlight scala def listEnding(query:String) = { listMatching(query, (str:String, query:String) => str.endsWith(query) } }}} * プレースホルダーを使ってfilesMatching関数の2引数は省略することが出来て {{{#!highlight scala def listEnding(query:String) = listMatching(query, _.endsWith(_)) }}} * クロージャを使えば更に短縮することができる {{{#!highlight scala def listMatching(matcher:String => Boolean) = { for(str <- list; if matcher(str)) yield str } def listEnding(query:String) = listMatching(_.endsWith(query)) }}} * listMatching関数で使われるqueryをlistMatching関数で束縛するのではなくて外側のlistEnding関数で束縛している。 === 高階関数もう一例 === * existsメソッドというのがある。existsメソッドはコレクションに適用されるメソッドで、コレクションの要素の中に一つでも与えられた関数式を満足するものがあればTrueを返し、そうでなければFalseを返すというものである。 * 数値のリストが与えられた時負の数が含まれているかどうかを判定する {{{#!highlight scala list.exists(_ < 0) }}} * existsの引数が関数式になっているのでこれも高階関数 * 嘘だと思う人はこのように書き換えてみよう {{{#!highlight scala list.exists(x => x < 0) }}} |
|
行 554: | 行 681: |
* 一つの引数リストではなく複数の引数リストに対応できるような関数をカリー化された関数という {{{#!highlight scala def plainSum(x:Int, y:Int) = x + y //カリー化されてない plainSum(2, 3) def curriedSum(x:Int)(y:Int) = x + y //カリー化されてる curriedSum(2)(3) }}} * カリー化されている関数はどうなっているのか * curriedSum関数は引数xをとってyという1引数の関数にまず化けている * そのあと引数yをとってInt型の数値を返している * 2段階になってるわけね * だから以下の関数とcurriedSumは同じ意味 * curriedSum2はxを引数にとって「yを引数にとる関数リテラル」を返している {{{#!highlight scala def curriedSum2(x:Int) = (y:Int) => x + y }}} === カリー化された関数にプレースホルダーを使ってみる === * カリー化された関数はプレースホルダーを使って引数を固定できる {{{#!highlight scala val onePlus = curriedSum(1)_ //一つ目の引数に1を与えて固定してる onePlus(2) // curriedSum(1)(2)と同じ意味になる }}} === 3引数以上で試してみる === * プレースホルダーは引数全体を一つのアンダースコアで置き換えるので、複数の引数を置き換えるときも1つだけ書けば良い {{{#!highlight scala def hoge(x:Int)(y:Int)(z:Int) = x + y + z val fuga = hoge(1)_ fuga(2, 3) // hoge(1, 2, 3)と同じ意味 }}} === カリー化するメリット === * カリー化された関数は部分適用がしやすくなる * 例えば足し算をする関数をカリー化しておけば、簡単に適当な数値を部分適用して、その数値の分インクリメントする関数を生成できる == 制御構造の生成 == * ファイルを開いて書き込んで閉じる操作を行う関数を定義してみる {{{#!highlight scala def withPrintWriter(file:File, op:PrintWriter => Unit){ val writer = new PrintWriter(file) try{ op(writer) } finally { writer.close() } } withPrintWriter(new File("date.txt"), writer => writer.println(new java.util.Date)) }}} * opはファイル書き込みのためのPrintWriterオブジェクトを引数にとってファイルに実際に書き込み操作を行う操作を定義している関数 * 操作を定義している関数を外から引数として与えられるようにしたので、ファイルを開いて何か操作をしてファイルを閉じるという操作だけを切り出した関数を作ることができる * このような書き方をopに対してファイルを開いて貸してあげているようにみえるため、「ローンパターン」という * 引数が1つだけのメソッドは丸括弧の代わりに中括弧を使ってもいい * 上のwithPrintWriterをカリー化して中括弧を使えるようにしてみよう {{{#!highlight scala def withPrintWriter(file:File)(op:PrintWriter => Unit){ val writer = new PrintWriter(file) try{ op(writer) } finally { writer.close() } } withPrintWriter(new File("date2.txt")){ writer => writer.println(new java.util.Date) } }}} * この関数に与えている処理を表す関数が中括弧で囲まれたことで、この部分が処理に対応していることが一目瞭然になった |
|
行 556: | 行 754: |
* 次のようなプログラムを考える {{{#!highlight scala object MyLog { val isDebug = true def main(args: Array[String]) { log(heavyFunc) } def log(s: String) = { //heavyFuncのデバッグログを取るための関数 if(isDebug) { println("call log") println(s) } } def heavyFunc() = { println("call heavyFunc") //実際には重い処理が用意されていると考える "hoge" } } }}} * このプログラムを実行すると次のような結果になる {{{#!highlight scala call heavyFunc call log hoge }}} * main関数でlog(heavyFunc)を実行するときにheavyFuncが評価されてしまうためこうなってしまう * isDebugをfalseにしてもheavyFuncはやっぱり評価されるので、"call heavyFunc"が出力されて、"hoge"が返される * "hoge"はいらないのに!!! * heavyFuncの返り値が必要になるまでheavyFuncの評価をしないでほしい * 名前渡しする {{{#!highlight scala object MyLog { val isDebug = true def main(args: Array[String]) { log(heavyFunc) } def log(s: => String) = { //ここが書き換わっている if(isDebug) { println("call log") println(s) } } def heavyFunc() = { println("call heavyFunc") "hoge" } } }}} * 引数に=>をつけて関数を定義するとそこは名前渡しとなって、実際に戻り値が必要になるまで評価されなくなる === こんなことしなくても関数を渡すようにすればいいんじゃね === {{{#!highlight scala def log(s: () => String) = { //ここが書き換わっている if(isDebug) { println("call log") println(s) } } }}} * この書き方だと関数じゃなくて文字列を直接渡したいときに苦労する {{{#!highlight scala log("hoge") //エラー log(() => "hoge")) //関数リテラルとして書かないといけない }}} == Scala的オブジェクト指向 == * コップ本10章に則って、Scalaにおけるオブジェクト指向の実現手法について見てみる * 簡単な2D描画をするライブラリを作る === ライブラリの仕様 === * 2Dオブジェクトはelemというメソッドで生成する * 生成したオブジェクトはaboveやbesideというメソッドを使って2つを結合することができる * 2Dオブジェクトは文字列表現の2次元矩形として定義する === 抽象クラス === * 2Dオブジェクトに対応するElementというクラスを用意する {{{#!highlight scala abstract class Element { def contents:Array[String] } }}} * contentsは矩形を表現するための文字列の配列 * Elementは抽象クラスになっている === 引数なしメソッドの定義 === * 要素の高さを調べるheightメソッドと幅を調べるwidthメソッドを用意する {{{#!highlight scala abstract class Element { def contents:Array[String] def height:Int = contents.length def width:Int = if(height == 0) 0 else contents(0).length } }}} * 別にdefで定義する必要はなくて、valで定義してしまってもいい * 関数がフィールドになりますね * このようにScalaではフィールドとメソッドのアクセス方法に区別がない {{{#!highlight scala abstract class Element { def contents:Array[String] val height:Int = contents.length val width:Int = if(height == 0) 0 else contents(0).length } }}} * フィールドにすると初回にその結果を保持するので高速になるがメモリを食う * メソッドだと毎回計算数するが、メモリは食わない === クラスの継承 === * Elementを継承してArrayElementをつくる {{{#!highlight scala class ArrayElement(conts:Array[String]) extends Element { def contents:Array[String] = conts } }}} * Elementは抽象クラスなのでcontentsメソッドを実装しないといけない === メソッドとフィールドのオーバーライド === * Scalaではメソッドとフィールドのアクセス方法が同じなので、メソッドとフィールドを相互にオーバーライドできる === コンストラクタ === * Scalaではコンストラクタのための専用の構文はない * クラス定義の直下にベタ書きした式がそのままコンストラクタになる {{{#!highlight scala class Hoge { println("hoge") //この部分はHogeを生成した時に実行される } }}} * 引数付きコンストラクタを定義したいときは次のように書く {{{#!highlight scala class Hoge(msg : String) { println(msg) } }}} === 補助コンストラクタ === * 複数の引数リストのためのコンストラクタを定義したいときは補助コンストラクタを使う {{{#!highlight scala class Hoge(msg : String) { def this() = this("hoge") 引数を与えなかった時のコンストラクタを定義 println(msg) } }}} * 補助コンストラクタは複数定義できて、基本コンストラクタか別の補助コンストラクタを必ず呼び出さないといけない === パラメータでフィールドを初期化してしまう === * コンストラクタのパラメータがフィールド初期化のためのパラメータなら次のように略して書くこともできる {{{#!highlight scala class ArrayElement(val contents:Array[String]) extends Element //valをつけることでフィールドと同じ名前のパラメータを定義できて、直接フィールド初期化ができる }}} * valじゃなくてvarにすれば再代入できるフィールドにできる * privateとかをつけることもできる {{{#!highlight scala class ArrayElement(private val contents:Array[String]) extends Element }}} === スーパークラスのコンストラクタ呼び出し === * 高さが1の(即ち線になっている)ようなオブジェクトのためのクラスを作ると便利かも * LineElementというのを作ります {{{#!highlight scala class LineElement(s:String) extends ArrayElement(Array(s)){ //extends ArrayElement(Array(s))と書くことで親クラスのコンストラクタにArray(s)を与えることができる override def width = s.length //親クラスをオーバーライドするのでoverrideをつけます override def height = 1 } }}} === override修飾子 === * 親クラスで実装済みのメソッドをオーバーライドする時は必ずメソッドの前にoverrideを書かないといけない * 親クラスのメソッドが抽象メソッドなら別に要らない * さっきも付けなかったよね === ポリモーフィズム === * 指定された幅と高さの範囲を、指定された文字で埋め尽くすElement形式であるUniformElementを定義してみる {{{#!highlight scala class UniformElement( // 範囲を埋め尽くす文字を指定 ch:Char override val width:Int, override val height:Int ) extends Element { private val line = ch.toString * width def contents = Array.make(height, line) } }}} * 今まで定義したクラスはElementを拡張して作ってきたのでElementの一族として扱える {{{#!highlight scala val e1:Element = new ArrayElement(Array("hoge", "huga")) val e2:Element = new LineElement("hello") val e3:Element = new UniformElement('x', 3, 4) }}} * Scalaでは呼び出されるメソッドは変数の型ではなく、オブジェクトがどのクラスから生成されたかに依存する {{{#!highlight scala abstract class Element { def demo() {println("Element's implementation invoked")} } class ArrayElement extends Element { override def demo() {println("Array Element's implementation invoked")} } class LineElement extends ArrayElement { override def demo() {println("Line Element's implementation invoked")} } class UniformElement extends Element }}} {{{#!highlight scala val e = new ArrayElement e.demo() //Array Element's implementation invokedが出力される val e = new LineElement e.demo() //Line Element's implementation invokedが出力される val e = new UniformElement e.demo() //Element's implementation invokedが出力される }}} === final === * サブクラスにオーバーライドさせたくないものにはfinalをつける * クラスそのものをそもそも拡張させたくないときはクラス自体にfinalをつければよい === 合成と継承 === * 既存のクラスを利用する方法には合成と継承がある * 合成はクラスの持つフィールドのデータ型として別のクラスを指定することで、あるクラスから別のクラスを利用しようとする方法 * オーバーライドを伴わないので、スーパークラスを書き換えるとサブクラスが使えなくなる、という問題を回避できる * オーバーライドを多用するより抽象メソッドの実装という形で実装を心がけるようにしたい * 上の例だとLineElementはArrayElementに対して合成の関係ではない * ArrayElementにcontentsフィールドの定義を任せてそれを継承して使用しているため * 合成の関係にするためにはLineElementをElementの直下に来るようにする * contentsはArrayElementで定義されたものを継承したのではなく、自前でArrayクラスをデータ型として指定したものとなっている {{{#!highlight scala class lineElement(s:String) extends Element { val contents = Array(s) override def width = s.length override def height = 1 } }}} * この辺の感覚はトレイトを学ぶとつかめるようになる気がする === 引数をとるメソッドを定義 === * オブジェクトを連結するメソッドabove, besideを定義します {{{#!highlight scala def above(that: Element): Element = { new ArrayElement(this.contents ++ that.contents) // ++ は配列を結合するための演算子 } def beside(that: Element):Element = { new ArrayElement( for ((line1, line2) <- this.contents zip that.contents) yield line1 + line2 ) } }}} * zip演算子は2つの配列から1つずつ要素を取り出してそれを組み合わせた要素を生成し、新しい配列を作るという演算子 * 出力用のtoStringメソッドも定義しょう * toStringはScalaのオブジェクト全てに定義されているので、overrideしないといけない {{{#!highlight scala override def toString = contents mkString "\n" //mkStringは引数にとった文字列を糊にしてcontentsの要素を結合するメソッド }}} === ファクトリーメソッド === * コンパニオンオブジェクトを使って実装するのが一番いい {{{#!highlight scala object Element { def elem(contents:Array[String]):Element = { new ArrayElement(contents) } def elem(char:Char, width:Int, height:Int):Element = { new UniformElement(char, width, height) } def elem(line:String):Element = { new LineElement(line) } } }}} * 引数リストの違いに応じて呼び出すコンストラクタを変えることによって、コンストラクタの違いを意識すること無くオブジェクトを生成できる * ArrayElement, UniformElement, LineElementはコンパニオンオブジェクトからコンストラクタを呼び出せればいいのでコンパニオンオブジェクトの中で定義してしまうことにする {{{#!highlight scala object Element { private class ArrayElement(val contents:Array[String]) extends Element private class LineElement(s:String) extends Element{ val contents = Array(s) override def width = s.length override def height = 1 } private class UniformElement( ch:Char, override val width:Int, override val height:Int ) extends Element { private val line = ch.toString * width def contents = Array.make(height, line) } def elem(contents:Array[String]):Element = { new ArrayElement(contents) } def elem(char:Char, width:Int, height:Int):Element = { new UniformElement(char, width, height) } def elem(line:String):Element = { new LineElement(line) } } }}} * 最後にElementクラスの定義でコンストラクタを呼び出しているところをファクトリーメソッドに置き換えれば完成 * 省略するのでじぶんでやってみよう == Scalaのクラス構造 == === Anyクラス === * Scalaのすべてのクラスの親クラス、頂点 * AnyクラスにはtoStringやequalsメソッドなどが実装されている * Anyクラスの直下にはAnyValクラスとAnyRefクラスがある === AnyValクラス === * Scalaにおける値をあらわすクラスの親クラス * ByteとかIntとかいうクラスの親 === AnyRefクラス === * Scalaにおける参照クラスの親クラス * Scalaでクラスを定義するとこいつの子になる === Javaのプリミティブ型との関連 === * Scalaでの数値演算は内部的にはJavaのプリミティブ型に変換して行われている模様 * 数値をオブジェクトとして扱う必要性が出てきたときは、自動的にオブジェクトとして扱ってくれる * Scalaでは==はオブジェクトの値の等価性を判定するためのメソッド * 内部的にJavaのequalsメソッドを呼び出している * Javaの==と同じように同じオブジェクトであるかどうかを判定したいときはeqメソッドを使う === Nullクラス === * すべての参照クラスのサブクラスになる * 参照になにもないことを表す * 参照クラスのサブクラスだから値型とは互換性がない * 例えばInt型の変数にNullを突っ込もうとすると怒られる === Nothingクラス === * すべてのクラスのサブクラスになっている * どんな型にも化けられる! * エラーが発生した時にこいつを返すようにするとエラー判定に便利 {{{#!highlight scala def error(message:String):Nothing = throw new RuntimeException(message) }}} == トレイト == === トレイトを定義する === * 見た感じクラスと同じ感じ {{{#!highlight scala trait Philosophical { def philosophize(){ println("I consume memory, therefore I am!") } } }}} === トレイトを使う === * 既存のクラスにミックスインして使う * Java風に言えば実装する * すでにextendsを使っているときはwith, 使ってないときはextends {{{#!highlight scala class Kyoko extends Philosophical val kyoko = new Kyoko kyoko.philosophize() I consume memory, therefore I am! }}} === スーパークラスを持っているクラスにミックスイン === {{{#!highlight scala class Human class Kyoko extends Human with Philosophical val kyoko = new Kyoko kyoko.philosophize() I consume memory, therefore I am! }}} === 複数トレイトのミックスイン === * クラスと違いトレイトは複数ミックスインできる {{{#!highlight scala class Human trait hasLeg class Kyoko extends Human with Philosophical with hasLeg val kyoko = new Kyoko kyoko.philosophize() I consume memory, therefore I am! }}} === トレイトのメソッドのオーバーライド === * トレイトで定義されたメソッドはオーバーライドできる === トレイトとは結局何か === * コンストラクタ引数をとれないクラスぐらいに考えてしまっていい * トレイトは道具箱のようなもの * クラスという作業場を用意しておいて、必要なメソッド(道具)をトレイトから引っ張り出してくるイメージ * トレイトを使うことで貧弱なインタフェースに処理を付け足してリッチなインターフェースを作ることができる === トレイトを実際に使ってみる === * Scalaに標準で用意されているOrderedトレイトというのを使ってみる * 有理数を提供するクラスRationalというものがあるとする * こいつに>, >=, <, <=というメソッドを用意したい * 2つのRationalを引数にとってBooleanを返すメソッド * 普通に頑張ってそれぞれのメソッドを実装してもいいけど、Orderedトレイトを使うと簡単に実装できる {{{#!highlight scala class Rational(n:Int, d:Int) extends Ordered[Rational]{ def compare(that:Rational) = //compareの値が正、負、0のどれかになるかという結果をもとに比較演算子の結果を求めるような仕組みになっている (this.numer * that.denom) - (that.numer * this.denom) //numerは分子, denomは分母に対応するフィールド } }}} === 積み重ね可能な変更 === * 整数のキューを考える {{{#!highlight scala abstract class IntQueue { def get():Int def put(x:Int) } import scala.collection.mutable.ArrayBuffer class BasicIntQueue extends IntQueue { private val buf = new ArrayBuffer[Int] //mutableになっている def get() = buf.remove(0) def put(x:Int){ buf += x } } }}} * 整数を追加するときに整数を2倍にして追加できるようにするトレイトをつくってみる {{{#!highlight scala trait Doubling extends IntQueue { abstract override def put(x:Int){super.put(2 * x)} } }}} * トレイト宣言でextendsキーワードを用いるとIntQueueクラスを拡張したクラスにしかミックスインできないようになる * extendsで明示的に親クラスを指定することでsuperキーワードを使って親クラスのメソッドを参照できる * 実際にはIntQueueクラスではputメソッドは抽象メソッドになっているので、具体的な実装を行なっているBasicIntQueueクラスのputメソッドが呼び出される * という認識でいいのかな * abstract overrideと書くことによってコンパイラに対して、既存のputメソッドに対して更に変更を加えているんだよということを示している {{{#!highlight scala class MyQueue extends BasicIntQueue with Doubling val queue = new MyQueue queue.put(10) queue.get() //20が出てくる }}} * いちいち使い捨てのクラスを作るのもアレだよね * newするときに一緒にミックスインの指定ができる {{{#!highlight scala val queue = new BasicIntQueue with Doubling queue.put(10) queue.get() //20が出てくる }}} * 複数のトレイトで積み重ねをやってみる {{{#!highlight scala // 待ち行列の値に1加えるトレイト trait Incrementing extends IntQueue { abstract override def put(x:int){super.put(x +1)} } // 待ち行列に負の数を加えないようにするトレイト trait Filtering extends IntQueue { abstract override def put(x:int){ if(x >= 0) super.put(x)} } }}} {{{#!highlight scala val queue = new BasicIntQueue with Incrementing with Filtering queue.put(-2);queue.put(-1);queue.put(0); queue.get() //1が出てくる queue.get() //エラー }}} * 上の挙動から複数のミックスインは右から行われることがわかる * つまりトレイトを適用する順番によって挙動が変わる!! * これは強力だけど、同時に危険性もある === 多重継承ではなくトレイトによるミックスインを採用した理由 === * 菱型継承をした時の問題を解決するため === ミックスインの線形化 === * 複数のトレイトをミックスインするときどの順番にミックスインされるのか {{{#!highlight scala class Animal trait Furry extends Animal trait HasLegs extends Animal trait FourLegged extends HasLegs class Cat extends Animal with Furry with FourLegged }}} {{{#!highlight scala Animal→AnyRef→Any //Animalはこのような階層構造になっている Furry → Animal → AnyRef → Any //Furryトレイトは次のような構造 Cat→FourLegged→HasLegs→Furry→Animal→AnyRef→Any //Catクラスはこのような構造 }}} * 自分から始めて右から順番に適用されると思っておけばよいかと |
目次
なにこれ
- コップ本にだいたい則ってScalaについてまとめたもの。
- Scala勉強会とかで使いたい。
- Scala処理系が入っている前提。
- コップ本19章ぐらいまで読んだのでその辺りまでとりあえずまとめたい(無理か)
とりあえず触ってみる
- 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するときに一緒にミックスインの指定ができる
- 複数のトレイトで積み重ねをやってみる
- 上の挙動から複数のミックスインは右から行われることがわかる
- つまりトレイトを適用する順番によって挙動が変わる!!
- これは強力だけど、同時に危険性もある
多重継承ではなくトレイトによるミックスインを採用した理由
- 菱型継承をした時の問題を解決するため
ミックスインの線形化
- 複数のトレイトをミックスインするときどの順番にミックスインされるのか
- 自分から始めて右から順番に適用されると思っておけばよいかと