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


Scalaコップ本の20章の残り全部をやっていきますよー。今回は前回やり残した通貨計算クラスの仕上げをしていきますよ

ケーススタディ:通貨計算の続き

前回までに実装した通貨関連クラスはこんな感じです

// 通貨地域として大外クラスを定義しますよ
abstract class CurrencyZone {
  // 抽象型の定義です
  type Currency <: AbstractCurrency
  // ファクトリーメソッドを抽象メソッドとして定義します
  def make(x:Long):Currency
  // 実際に通貨オブジェクトの定義をします
  abstract class AbstractCurrency {
    // 金額フィールドです
    val amount:Long
    // 通貨単位格納フィールドです
    def designation:String
    // 金額 通貨単位の形式で表現しますよ
    override def toString = amount + " " + designation
    // 加法メソッドの定義はこんな感じですかねー
    def + (that:Currency):Currency = {
      make(this.amount + that.amount)
    }
    // ついでに乗法メソッドも定義してしまいますね
    def * (x:Double):Currency = {
      make((this.amount * x).toLong)
    }
  }
}

上記クラスを具象実装はUSドル的には例えば次のように行ないますねー

// オブジェクトとしてCurrencyZoneを拡張しますよ
object US extends CurrencyZone {
  // AbstractCurrencyのサブクラスを内部的に定義してやりますよ
  abstract class Dollar extends AbstractCurrency {
    def designation = "USD"
  }
  // 型を具象化します
  type Currency = Dollar
  // ファクトリーメソッドを具象実装します
  def make(x:Long) = new Dollar{val amount = x}
}
補助通貨の導入

上記サンプルで各通貨ごとのオブジェクトを生成出来るようになりましたが、通貨にはドルに対するセントのような補助通貨が存在するのでそれについて対応できるようにしますよ。具体的には値の小さいセントを基準として、標準通貨単位のドルがセント100分であるという定義をCurrencyUnitフィールドを導入することで管理します。

具体的には内部的にはセントを数値として扱っていつつもドル・セント間の変換定義と、ドルが標準通貨単位で有ることの宣言を行ないますネ(´・ω・`)

object US extends  CurrencyZone {
  abstract class Dollar extends AbstractCurrency {
    def designation = "USD"
  }
  type Currency = Dollar
  def make(cents:Long) = new Dollar {
    val amount = cents
  }
  // 1セントは1セント
  val Cent = make(1)
  // 1ドルは100セント
  val Dollar = make(100)
  // ドルが標準通貨単位であることを宣言します
  val CurrencyUnit = Dollar
}

ついでに大元のCurrencyZoneにもCurrencyUnitの定義を加えつつ、toStringメソッドも補助通貨単位に対応できるように書き換えてやりますよ

// 通貨地域として大外クラスを定義しますよ
abstract class CurrencyZone {
  type Currency <: AbstractCurrency
  def make(x:Long):Currency
  abstract class AbstractCurrency {
    val amount:Long
    def designation:String
    // 補助通貨も含めて小数点表記してやります
    override def toString = {
      ((amount.toDouble / CurrencyUnit.amount.toDouble)
       formatted("%." + decimals(CurrencyUnit.amount) + "f")
       + " " + designation)
    }
    // 桁数-1を取得するためのメソッドです
    private def decimals(n:Long):Int = {
      if(n == 1) 0 else 1 + decimals(n/10)
    }
    def + (that:Currency):Currency = {
      make(this.amount + that.amount)
    }
    def * (x:Double):Currency = {
      make((this.amount * x).toLong)
    }
  }
  // 標準通貨定義も追加してやりますよ
  val CurrencyUnit:Currency
}

formattedメソッドはJavaのString.formattedみたいなものらしいんだけども、PHPとかでいうsprintfみたいな感じかしらね?あとdecimalsメソッドは桁数-1の値を返す再帰関数デス(´・ω・`)なにげにScalaって再帰処理大好きだよね

EUと円の通貨圏を追加しますよ
//// ユーロ関連の定義を行ないますよ
// まずはユーロ利用圏のヨーロッパを定義しますよ
object Europe extends CurrencyZone {
  // ヨーロッパ圏内定義でユーロを定義しますよ
  abstract class Euro extends AbstractCurrency {
    def designation = "EUR"
  }
  // 抽象型を具象実装します
  type Currency = Euro
  // 生成メソッドを定義しますね
  def make(cents:Long) = new Euro {
    val amount = cents
  }
  // 補助通貨関連の設定です
  val Cent = make(1)
  val Euro = make(100)
  val CurrencyUnit = Euro
}

//// 円関連の定義を行ないますよ
// まずは円利用圏の日本を定義しますよ
object Japan extends CurrencyZone {
  // 日本圏内定義で円を定義しますよ
  abstract class Yen extends AbstractCurrency {
    def designation = "JPY"
  }
  // 抽象型を具象実装します
  type Currency = Yen
  // 生成メソッドを定義しますね
  def make(yen:Long) = new Yen {
    val amount = yen
  }
  // 補助通貨関連の設定です
  // 円は円だけで運用するのです
  val Yen = make(1)
  val CurrencyUnit = Yen
}

んじゃ実行してみますかね

// Euroオブジェクトを生成しますよ
scala> val euro10 = Europe.make(10)
euro10: Europe.Euro = 0.10 EUR

scala> val euro100 = Europe.make(100)
euro100: Europe.Euro = 1.00 EUR

// 加法演算です
scala> euro10 + euro100
res1: Europe.Currency = 1.10 EUR
// 乗法演算です
scala> euro10 * 100    
res2: Europe.Currency = 10.00 EUR

// 円オブジェクトを生成してみますよ
scala> val yen10 = Japan.make(10)
yen10: Japan.Yen = 10 JPY

scala> val yen100 = Japan.make(100)
yen100: Japan.Yen = 100 JPY

// 加法演算です
scala> yen10 + yen100
res3: Japan.Currency = 110 JPY
// 乗法演算もOKデス
scala> yen10 * 3
res4: Japan.Currency = 30 JPY

OKデスネ(´・ω・`)

通貨換算機能の導入

ここまででドル、円、Euroの各オブジェクトを生成することが出来るようになったので相互変換の定義を行ってみますよ(´・ω・`)

まずは各通貨の変換用マップを定義します

object Converter {
  var exchangeRate = Map(
    "USD" -> Map("USD" -> 1.0, "EUR" -> 0.7596, "JPY" -> 1.211),
    "EUR" -> Map("USD" -> 1.316, "EUR" -> 1.0, "JPY" -> 1.594), 
    "JPY" -> Map("USD" -> 0.8257, "EUR" -> 0.6272, "JPY" -> 1.0),
  )
}

次に換算メソッドをCurrencyクラスに追加してやります

abstract class CurrencyZone {
  type Currency <: AbstractCurrency
  def make(x:Long):Currency
  abstract class AbstractCurrency {
    val amount:Long
    def designation:String
    override def toString = {
      ((amount.toDouble / CurrencyUnit.amount.toDouble)
       formatted("%." + decimals(CurrencyUnit.amount) + "f")
       + " " + designation)
    }
    // 通貨変換用のメソッドを追加してやりますよ(´・ω・`)
    // 具体的な計算としては金額に換算率をかけて計算します
    // なお、型パラメータはCurrencyZoneのAbstractCurrency型デス
    def from (other:CurrencyZone#AbstractCurrency):Currency = {
      make(Math.round(
        other.amount.toDouble * Converter.exchangeRate(other.designation)(this.designation))
      )
    }
    private def decimals(n:Long):Int = {
      if(n == 1) 0 else 1 + decimals(n/10)
    }
    def + (that:Currency):Currency = {
      make(this.amount + that.amount)
    }
    def * (x:Double):Currency = {
      make((this.amount * x).toLong)
    }
  }
  val CurrencyUnit:Currency
}

んじゃ、ちょっくら通貨変換してやりますかねー

// 円をドルに変換しますよ
scala> Japan.Yen.from(US.Dollar * 100)  
res10: Japan.Currency = 12110 JPY
// Scala的省略演算でもう一度
scala> Japan.Yen from US.Dollar * 100   
res11: Japan.Currency = 12110 JPY

//円からユーロに変換しますよ
scala> Japan.Yen.from(Europe.Euro * 100)
res12: Japan.Currency = 15940 JPY

// 演算と組み合わせてみますねー
scala> yen100 + (Japan.Yen from euro50)
res21: Japan.Currency = 180 JPY

// 通貨変換を伴わない演算はエラーです
scala> yen100 + euro50
<console>:11: error: type mismatch;
 found   : Europe.Euro
 required: Japan.Currency
       yen100 + euro50
                ^

型の抽象化を行うことで異なる通貨(サブ型)の演算を禁止しておりますねー。この異なる単位間の変換を変換忘れを防止しつつもうまい具合に扱えるようになりましたー(`・ω・´)

演算メソッドを追加して完成させますよ

最後に減算と除算のメソッドを追加して完成ですよー

abstract class CurrencyZone {
  type Currency <: AbstractCurrency
  def make(x:Long):Currency
  abstract class AbstractCurrency {
    val amount:Long
    def designation:String
    override def toString = {
      ((amount.toDouble / CurrencyUnit.amount.toDouble)
       formatted("%." + decimals(CurrencyUnit.amount) + "f")
       + " " + designation)
    }
    def from (other:CurrencyZone#AbstractCurrency):Currency = {
      make(Math.round(
        other.amount.toDouble * Converter.exchangeRate(other.designation)(this.designation))
      )
    }
    private def decimals(n:Long):Int = {
      if(n == 1) 0 else 1 + decimals(n/10)
    }
    def + (that:Currency):Currency = {
      make(this.amount + that.amount)
    }
    def * (x:Double):Currency = {
      make((this.amount * x).toLong)
    }
    // 減算メソッドを追加してやりますよ
    def - (that:Currency):Currency = {
      make(this.amount - that.amount)
    }
   // Doubleによる除算メソッドを追加です
    def / (that:Double) = {
      make((this.amount / that).toLong)
    }
     // Currency同士の除算メソッドを追加です
    def / (that:Currency) = {
      this.amount.toDouble / that.amount
    }
  }
  val CurrencyUnit:Currency
}

最終実行ですよー

// オブジェクトを生成します
scala> val yen100 = Japan.make(100)
yen100: Japan.Yen = 100 JPY

scala> val yen50 = Japan.make(50)  
yen50: Japan.Yen = 50 JPY

// 減法を試しますよー
scala> yen100 - yen50
res13: Japan.Currency = 50 JPY

// 除法を試しますねー
scala> yen100 / yen50
res14: Double = 2.0
// Doubleによる除法はこんな感じです
scala> yen100 / 20   
res15: Japan.Currency = 5 JPY

おおー出来ました(`・ω・´)抽象型を使っていろいろ遊んでみましたねー

いじょー

Scalaに関する抽象メンバーについての20章はこれで終わりですー。次回は暗黙の型変換とかそのあたりの21章をやりたいと思いますよー。が、頑張ります(´・ω・`)