はじめに
- 某所Scalaミートアップの発表資料です。
- 用途に適した単位型を、自力で小さく実装することが目的です。
- 実装した言語はScalaですが、各テクニックは他の言語でも応用が利くと思います。
バグを探せ
val imp: Int = 400
val yen: Int = 30
val cpm: Double = yen / imp
広告が400回閲覧されたので、30円支払いました。
単価のCPMはいくらでしょう?
単位演算は罠だらけ
val imp: Int = 500
val yen: Int = 30
val cpm: Double = yen.toDouble / imp.toDouble * 1000
- 閲覧(viewable impression)数は、掲載(impression)数と異なる。
-
Int
の割り算は切り捨てなので、割る前にDouble
に変換する。 - CPMは、1000掲載(impression)単位の価格です。
バグを防ぐために
- テストすれば充分……?
- テストも同じ勘違いをしちゃう問題。。。
- 単位演算をモジュール化すれば、そこだけ気をつければいい……?
def calcCpm(imp: Int, yen: Int): Double
- うっかり異なる単位の値を、引数に渡してしまう。。。
- せっかく作った関数を使いわすれてしまう。。。
- 型に単位情報を埋めこめば、\型システム/が解決してくれるのでは!
値クラス&幽霊型
case class UnitBase[A, Tag](value: A) extends AnyVal
- 型
A
のvalue
をcase class UnitBase
でラップ。- 実行時のオーバーヘッドは、値クラス化
extends AnyVal
で回避。
- 実行時のオーバーヘッドは、値クラス化
- 実際には使わない幽霊型
Tag
により、同じ型A
に対して異なるラッパークラスを定義できる。
詳しくは、「Refactoring in Scala」お勧め。
値クラス&幽霊型
def calcCpm(imp: Imp, yen: Yen): Cpm =
Cpm(yen.value.toDouble / imp.value.toDouble * 1000)
- 単位ごとに次のような定義をしていく。
trait CpmTag
type Cpm = UnitBase[Double, CpmTag]
object Cpm {
def apply(value: Double) = new Cpm(value)
}
- 単位を型で明記できるようになった。
- うっかり異なる単位の変数を引数に渡すと、コンパイルエラー。
- 実際の演算は面倒になった。。。
-
case class UnitBase
にメソッドを実装すればいいのでは!
-
欲しい演算とは
たとえば広告系(アドテク)だと、次のような感じ。
- 同じ型の足し算&引き算&大小比較は、元の数値型そのままでいい。
- 掛け算&割り算は、単位の組み合わせを制限したい。
- クリック率にコンバージョン数を掛けているコードは、おそらくバグ。
- 掛け算&割り算は、固定の係数を適用したい。
- CPMの
1000
とか。
- CPMの
数値型を統べるもの
Q. Int
やDouble
の演算って、どうやって共通化すればいいの? スーパークラス無いよね?
A. Numeric
型クラスとかあるよ!
trait Numeric[T] extends Ordering[T] {
def plus(x: T, y: T): T
def minus(x: T, y: T): T
def times(x: T, y: T): T
trait Integral[T] extends Numeric[T] {
trait IntIsIntegral extends Integral[Int] {
def plus(x: Int, y: Int): Int = x + y
def minus(x: Int, y: Int): Int = x - y
def times(x: Int, y: Int): Int = x * y
implicit object IntIsIntegral extends IntIsIntegral with Ordering.IntOrdering
型クラスがいっぱい
-
Ordering
:大小比較ができる。 -
Numeric
:足し算&引き算&掛け算ができる。 -
Fractional
:割り算もできる。 - 掛け算&割り算で、単位の組み合わせを制限し係数を適用する型クラスは、自力で実装する。
型クラスについては、「主要な型クラスの紹介」お勧め。
実装してみた
- 50行程度の小さな実装です。
- コードの大部分が型の制御。
case class UnitNum[A, Tag](value: A) extends AnyVal {
def +(x: UnitNum[A, Tag])(implicit op: Numeric[A]) =
UnitNum[A, Tag](op.plus(value, x.value))
def -(x: UnitNum[A, Tag])(implicit op: Numeric[A]) =
UnitNum[A, Tag](op.minus(value, x.value))
def *[Tag1, Tag2](x: UnitNum[A, Tag1])(implicit op: Numeric[A], mul: UnitNum.Mul[A, Tag, Tag1, Tag2]) =
UnitNum[A, Tag2](op.times(op.times(value, x.value), mul.factor))
def /[Tag1, Tag2](x: UnitNum[A, Tag1])(implicit op: Fractional[A], mul: UnitNum.Mul[A, Tag1, Tag2, Tag]) =
UnitNum[A, Tag2](op.div(op.div(value, x.value), mul.factor))
def map[B](f: A => B) =
UnitNum[B, Tag](f(value))
def unary_-(implicit op: Numeric[A]) =
map(op.negate)
def abs(implicit op: Numeric[A]) =
map(op.abs)
def mapInt(implicit op: Numeric[A]) =
map(op.toInt)
def mapLong(implicit op: Numeric[A]) =
map(op.toLong)
def mapFloat(implicit op: Numeric[A]) =
map(op.toFloat)
def mapDouble(implicit op: Numeric[A]) =
map(op.toDouble)
}
object UnitNum {
import scala.language.implicitConversions
implicit def ordering[A: Ordering, Tag]: Ordering[UnitNum[A, Tag]] =
Ordering.by(_.value)
implicit def ordered[A: Ordering, Tag](x: UnitNum[A, Tag]): Ordered[UnitNum[A, Tag]] =
Ordered.orderingToOrdered(x)(ordering[A, Tag])
case class Mul[A, Tag1, Tag2, Tag3](factor: A) extends AnyVal
object Mul {
implicit def commutative[A, Tag1, Tag2, Tag3](implicit mul: Mul[A, Tag1, Tag2, Tag3]) =
Mul[A, Tag2, Tag1, Tag3](mul.factor)
}
}
使ってみた
val imp: Vimp = Vimp(400)
val yen: Yen = Yen(30)
val cpm: Cpm = yen / imp // コンパイルエラー!!!
- 単位の扱い間違えているとコンパイル通らない。
-
Yen
をVimp
で割る演算の結果は、Cpm
でない。 -
Yen
をImp
で割る時は、実体がDouble
でないといけない。
-
使ってみた
val imp: Imp = Imp(500)
val yen: Yen = Yen(30)
val cpm: Cpm = yen.mapDouble / imp.mapDouble
assert(cpm == Cpm(60.0))
- バグ潰せばコンパイル通る。
- 係数
1000
は自動で掛けてくれる。
ちょいテク
implicit val mulImpCpmYen = UnitNum.Mul[Double, ImpTag, CpmTag, YenTag](0.001)
この定義一つで、実体がDoubleならば、次の四つの演算ができるようにしています。
-
Imp
*Cpm
/ 1000 =Yen
-
Cpm
*Imp
/ 1000 =Yen
-
Yen
/Imp
* 1000 =Cpm
-
Yen
/Cpm
* 1000 =Imp
def *[Tag1, Tag2](x: UnitNum[A, Tag1])(implicit op: Numeric[A], mul: UnitNum.Mul[A, Tag, Tag1, Tag2]) =
UnitNum[A, Tag2](op.times(op.times(value, x.value), mul.factor))
def /[Tag1, Tag2](x: UnitNum[A, Tag1])(implicit op: Fractional[A], mul: UnitNum.Mul[A, Tag1, Tag2, Tag]) =
UnitNum[A, Tag2](op.div(op.div(value, x.value), mul.factor))
object Mul {
implicit def commutative[A, Tag1, Tag2, Tag3](implicit mul: Mul[A, Tag1, Tag2, Tag3]) =
Mul[A, Tag2, Tag1, Tag3](mul.factor)
}
}
まとめ
- いくつかの型テクニックを使って、良い感じの単位型を実装することができたよ。
- この実装は、単位演算の要件に応じて細かくカスタマイズすることができるよ。
\さすがScala/
他の単位型パターン:物理
- メートル毎秒(m/s)を秒(second)で割ったら、メートル毎秒毎秒(m/s^2)になってほしい。
-
Map("meter" -> 1, "second" -> -2)
という情報を、型に埋めこめればよい。 - 「コンパイル時に型レベルで整数を四則演算してみた」みたいな依存型の実現が必要……。
-
- 「ミリ」や「キロ」などの接頭辞もサポートしたい。
- Scalaのライブラリでは、KarolS / units が個人的な理想に近い。
- C++のライブラリでは、Boost.Unitsが色々と凄い。
他の単位型パターン:通貨
- 円とか、ドルとか。
- 為替レートが市場依存&動的なので、為替処理を型で解決するのは危なそう。
実行環境
$ scala -version
Scala code runner version 2.11.8 -- Copyright 2002-2016, LAMP/EPFL