LL脳がscalaの勉強を始めたよ その15


Scalaコップ本を続けていきますよー、亀の歩みで6章8節からやっていきますデス

今回は演算子を定義したりしていきますよー

前回までの復習ー

前回までに作成した有理数演算クラスRationalはこんな感じデス、前回は補助コンストラクタと正規化の処理を追加しておりますー

class Rational(n:Int, d:Int) {
    // 分母が0で無いことをチェック
    require(d != 0)
   // 分子分母の最大公約数を取得
    private val g = gcd(n.abs, d.abs)
    // 正規化した分子分母をフィールドとして格納
    val numer = n / g
    val denom = d / g
    // 分子のみをクラスパラメータとする補助コンストラクタを設定
    def this(n:Int) = this(n, 1)
    // クラスのtoString表現を見やすく上書き
    override def toString = numer + "/" + denom
    // 2つのRationalオブジェクトを加算するメソッド
    def add(other: Rational):Rational = {
        new Rational(
            numer * other.denom + other.numer * denom,
            denom * other.denom
        )
    }
    // 2つのRationalオブジェクトを比較してレシーバのほうが小さいか判定するメソッド
    def lessThan(other:Rational) = {
        this.numer * other.denom < other.numer * this.denom
    } 
    // 2つのRationalオブジェクトの大きい方を返すメソッド
    def max(other: Rational) = {
        if(this.lessThan(other)) other else this
    }
    // 最大公約数を計算するメソッド
    private def gcd(a:Int, b:Int):Int = {
        if(b == 0) a else gcd(b, a % b)
    }
}

演算子の定義

前回までに実装したRationalクラスには加算用のメソッドaddを実装していたんだけども、演算子なら"+"だろ!ってことで変更します。ついでに"*"も追加してやります。

演算子のところでもやった通り、Scalaでは"+"も"*"も有効な識別子なのでメソッド名(ここだと演算子とも言える)として使えるのですよー、ということで作ってみますよ

ちなみに”識別子”という新しいっぽい単語については次の節でやりますー

ほんでは実装ー

演算子を実装(簡易表示デス)

class Rational(n:Int, d:Int) {
    require(d != 0)
    private val g = gcd(n.abs, d.abs)
    val numer = n / g
    val denom = d / g
    def this(n:Int) = this(n, 1)
    override def toString = numer + "/" + denom
    // 加算メソッド"+"を実装します
    def + (other:Rational):Rational = {
        new Rational(
            this.numer * other.denom + other.numer * this.denom,
            this.denom * other.denom
        )
    }
    // 乗算メソッド"*"を実装します
    def * (other:Rational):Rational = {
        new Rational(this.numer * other.numer,  this.denom * other.denom)
    }
    private def gcd(a:Int, b:Int):Int = {
        if(b == 0) a else gcd(b, a % b)
    }
}

やってることは文字列名でのメソッドと同じですねー

試しに動かしてみますよ
// 2つの有理数オブジェクトr1, r2を生成
scala> val r1 = new Rational(2,3)
r1: Rational = 2/3
scala> val r2 = new Rational(3,5)
r2: Rational = 3/5

// 加算しますー
scala> r1 + r2
res14: Rational = 19/15
// 乗算しますー
scala> r1 * r2
res15: Rational = 2/5

// 実際はこういう呼出ですけどもー
scala> r1.+(r2)
res16: Rational = 19/15
scala> r1.*(r2)
res17: Rational = 2/5
演算子の優先順序も守られる

5章の演算順序でもやったように演算子の優先順序は演算子(メソッド)名によって決まっているので、今回のように自分で定義した演算子でも優先順位 * < + は適用されます

例えばこんなふうに*が優先されますよ

// *と+の計算は
scala> r1 + r1 * r2
res18: Rational = 16/15
// コレと同じになるわけデス
scala> r1 + (r1 * r2)
res19: Rational = 16/15
// ()を使えば優先順位変えられますけどねー
scala> (r1 + r1) * r2
res20: Rational = 4/5

scalaの識別子

識別子は変数や関数を区別するための名前らしいので、個人的には変数名や関数名っていう認識

Scalaは識別子として英数字識別子、演算子識別子、ミックス識別子、リテラル識別子の4種類があるみたいねー

英数字識別子

先頭が英字かアンダースコアで、その後ろが英数字とアンダースコアが続くやつ。でもScalaではアンダースコアが識別子以外に使われることがあるらしいので、余り使わないのが良さそうね。

たとえばこんな感じかしらねー

scala> def orz () {println("もうダメぽ")}
orz: ()Unit

scala> orz
もうダメぽ

うん、アホなことやったなって若干反省してる(´・ω・`) ちなみに$はコンパイル通るけど、使っちゃダメ、絶対!とのこと(コンパイラが生成した識別子とぶつかるみたいね)

識別子の命名規則は次のようにするのが一般的みたい

  • 先頭大文字のキャメルケース
    • クラス
    • トレイト
  • 先頭小文字のキャメルケース
    • (キャメルケースの)フィールド
    • メソッドのパラメータ
    • ローカル変数
    • 関数

うん、コミュニティールール(…というより開発者ルール?)っぽいので覚えておこう

ついでに識別子の末尾にアンダースコアを使う場合は他の記号にくっつかないように注意が必要らしいデス

// コンパイラがval hoge_:を識別子として解釈してしまうのでエラー
val hoge_: Int = 3
// これならOK( _ と : の間にスペース)
val hoge_ : Int = 3

それと定数は最初の文字を大文字にするのが習慣みたい

// こういうJavaっぽい定数表現が
X_OFFSET
// scalaだとこんなふうになるです
XOffset

頭大文字キャメルケースっていう理解でいいのかな?

演算子識別子

1個以上のASCII文字である演算子文字から構成される識別子デス。演算子文字は例えば + や * や : や ? や ~ や # …他たくさんってほぼなんでもアリですな(´・ω・`)

識別子的としてはこんなのがつくれますねぇ

  • +
  • ++
  • :::
  • :->

調子にのってこんなのを作ってみた

// フィッシュボーン演算子
scala> def <^++++++++++< (s:String) { println(s) }
$less$up$plus$plus$plus$plus$plus$plus$plus$plus$plus$plus$less: (String)Unit
// 実行できますねー
scala> <^++++++++++< ("hoge")                     
hoge
||< 

でも文字だの数字だのが入るとダメですねー
>|scala|
scala> def +s() {println("hoge")}
<console>:1: error: '=' expected but identifier found.
       def +s() {println("hoge")}

ascii文字だけで作ったAAとかうまく見つけられれば(文字通り)遊べそうだなぁ

// AAが思いつかないので適当に
scala> def <-+|-|-|+-> () {println("空を飛ぶロボットに見えませんか?")}
$less$minus$plus$bar$minus$bar$minus$bar$plus$minus$greater: ()Unit
// 実行してみます
scala> <-+|-|-|+->
空を飛ぶロボットに見えませんか?

対話コンソールで識別子を定義すると出力されるように、Scalaコンパイラ演算子識別子を内部的にすり潰して使っているみたいデス

すり潰しは、例えば"<-"とかが"$less$minus"みたいに置き換えられることみたいですな。JavaコードからこれらのScalaコードにアクセスする場合は"<-"とかでアクセス出来ないので"$less$minus"といった内部表現形式を使う必要があるみたいです。

ついでに、x <- y というScalaコードは演算識別子をつなげて使う文化がないらしいJava(なのかな?)では x < - yで字句解析されるみたいですなー

ミックス演算子

英数字識別子にアンダースコアと演算子識別子が続く形式みたい

単項演算子のunary_+とかがあるみたいね、あと代入演算子の定義で使われるmyvar_=とかがあるらしい…myvar_=については18章でやるらしいのでとりあえず放置ー

とりあえずサンプルですよ

// アンダースコアの後だったら演算子識別子を続けられるみたい
scala> def hoge_= () { println("hoge") }
hoge_$eq: ()Unit
// 実行結果
scala> hoge_=                           
hoge

//  アンダースコアがないとダメですよ
scala> def hoge= () { println("hoge") } 
<console>:4: error: () of type Unit does not take parameters
       def hoge= () { println("hoge") }
                 ^
// アンダースコアのあとなら演算識別子をいくら続けてもOK
scala> def hoge_=+# () { println("hoge") } 
hoge_$eq$plus$hash: ()Unit
// でも演算識別子の後ろに英数字識別子が続くとダメですな
scala> def hoge_=+#sds () { println("hoge") }
<console>:1: error: '=' expected but identifier found.
       def hoge_=+#sds () { println("hoge") }
                   ^

まあ、普段つかうようなモノじゃないんだろうけどねー

リテラル識別子

バッククォート" ` "で囲まれた文字列を識別子として使えるみたいね

とりあえず試してみると

// こんな適当な定義も通りますよー
scala> def `hoge++|**moge1234` () {print("hoge")}      
hoge$plus$plus$bar$times$timesmoge1234: ()Unit
// 実行もできます
scala> `hoge++|**moge1234`
hoge

// バッククォートをとれば当然ダメねー
scala> hoge++|**moge1234  
<console>:5: error: not found: value hoge
       hoge++|**moge1234
       ^

どうもscala予約語を識別子としたいときによく使われるみたいですな

一番使われるパターンがJavaのThread.yield()にアクセスするときに、yieldがScala予約語なのでThread.`yield`()とするみたいですー

...と、よく使うらしいメモでした

いじょー

とりあえず今回までの成果をまとめますー

class Rational(n:Int, d:Int) {
    // 分母が0で無いことをチェック
    require(d != 0)
   // 分子分母の最大公約数を取得
    private val g = gcd(n.abs, d.abs)
    // 正規化した分子分母をフィールドとして格納
    val numer = n / g
    val denom = d / g
    // 分子のみをクラスパラメータとする補助コンストラクタを設定
    def this(n:Int) = this(n, 1)
    // クラスのtoString表現を見やすく上書き
    override def toString = numer + "/" + denom
    // 2つのRationalオブジェクトを加算するメソッド
    def + (other:Rational):Rational = {
        new Rational(
            this.numer * other.denom + other.numer * this.denom,
            this.denom * other.denom
        )
    }
    // 2つのRationalオブジェクトを乗算するメソッド
    def * (other:Rational):Rational = {
        new Rational(this.numer * other.numer,  this.denom * other.denom)
    }
    // 2つのRationalオブジェクトを比較してレシーバのほうが小さいか判定するメソッド
    def lessThan(other:Rational) = {
        this.numer * other.denom < other.numer * this.denom
    } 
    // 2つのRationalオブジェクトの大きい方を返すメソッド
    def max(other: Rational) = {
        if(this.lessThan(other)) other else this
    }
    // 最大公約数を計算するメソッド
    private def gcd(a:Int, b:Int):Int = {
        if(b == 0) a else gcd(b, a % b)
    }
}

次回はメソッド多重定義と型推論についてやりますー、次で6章終われればいいな