Skip to content

Instantly share code, notes, and snippets.

@johnynek
Last active March 31, 2020 20:49
Show Gist options
  • Save johnynek/2432e2e61221cbf9e56fc4d784c3fac7 to your computer and use it in GitHub Desktop.
Save johnynek/2432e2e61221cbf9e56fc4d784c3fac7 to your computer and use it in GitHub Desktop.
using literal types in scala 2.13 to track the half-life of an exponential decay
case class Timestamp(epochMillis: Long)
case class Decay[H <: Double](scaledTime: Double, value: Double) {
def timestampDouble(implicit v: ValueOf[H]): Double =
scaledTime * v.value
def timestamp(implicit v: ValueOf[H]): Timestamp =
Timestamp(timestampDouble.toLong)
def combine(that: Decay[H]): Decay[H] =
if (scaledTime > that.scaledTime) that.combine(this)
else {
// we know scaledTime <= that.scaledTime
val decayThis = math.exp(scaledTime - that.scaledTime) * value
if (decayThis == 0.0) that
else Decay[H](that.scaledTime, that.value + decayThis)
}
}
object Decay {
def build[H <: Double: ValueOf, N: Numeric](time: Timestamp, n: N): Decay[H] =
Decay(time.epochMillis.toDouble / valueOf[H], implicitly[Numeric[N]].toDouble(n))
implicit def monoidForDecay[H <: Double]: Monoid[Decay[H]] =
new Monoid[Decay[H]] {
val empty = Decay[H](Double.NegativeInfinity, 0.0)
def combine(l: Decay[H], r: Decay[H]): Decay[H] = l.combine(r)
}
}
@deanwampler
Copy link

Interesting. I haven't played with this construct before.

I had to make a few modifications to get it to compile (Scala 2.13.1):

import scala.language.implicitConversions

trait Monoid[M] {
  val empty: M
  def combine(l: M, r: M): M
}

case class Timestamp(epochMillis: Long) extends AnyVal
object Timestamp {
  implicit def doubleToTimestamp(d: Double):Timestamp = new Timestamp(d.toLong)
}

case class Decay[H <: Double](scaledTime: Double, value: Double) {
  def timestampDouble(implicit v: ValueOf[H]): Double =
    scaledTime * v.value

  def timestamp(implicit v: ValueOf[H]): Timestamp =
    Timestamp(timestampDouble.toLong)

  def combine(that: Decay[H]): Decay[H] =
    if (scaledTime > that.scaledTime) that.combine(this)
    else {
      // we know scaledTime <= that.scaledTime
      val decayThis = math.exp(scaledTime - that.scaledTime) * value
      if (decayThis == 0.0) that
      else Decay[H](that.scaledTime, that.value + decayThis)
    }
}
object Decay {
  import Timestamp._

  def apply[H <: Double: ValueOf, N: Numeric](time: Timestamp, n: N): Decay[H] =
    Decay(time.epochMillis.toDouble / valueOf[H], implicitly[Numeric[N]].toDouble(n))

  implicit def monoidForDecay[H <: Double]: Monoid[Decay[H]] =
    new Monoid[Decay[H]] {
      val empty = new Decay(Double.NegativeInfinity, 0.0)
      def combine(l: Decay[H], r: Decay[H]): Decay[H] = l.combine(r)
    }
}

Even then, I'm not sure how to instantiate a Decay:

scala> import Decay._, Timestamp._
...
scala> Decay(10000.0, 100)
res0: Decay[Nothing] = Decay(10000.0,100.0)

scala> val d2 = Decay[Double,Int](10000.0, 100)
                                 ^
       error: No singleton value available for Double.

The Decay[Nothing] inferred type seems wrong (?). I got the same behavior in dotr, too.

@johnynek
Copy link
Author

yeah, unfortunately, I posted before I ironed out usability.

Change apply to def build because currently you are getting confused with the apply method. (also I wouldn't use the implicit conversion).

@deanwampler
Copy link

That works better (and makes more sense ;) Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment