2

Let's say I have a class:

abstract class NumericCombine[A:Numeric,B:Numeric]{
        type AB <: AnyVal
    }

I want to define a function that returns a value of type NumericCombine[A,B].AB. for instance:

def plus[A: Numeric,B:Numeric](x: A, y: B): NumericCombine[A,B].AB

but the compiler doesn't let me reference .AB in plus.

FYI, this is the context of this question.

I want to provide:

implicit object IntFloat extends NumericCombine[Int,Float]{override type AB = Float}
implicit object FloatInt extends NumericCombine[Float,Int]{override type AB = Float}

and its other 44 friends (7*6-2) so that I can define my plus as below:

def plus[A: Numeric,B:Numeric](x: A, y: B): NumericCombine[A,B].AB =
{
type AB = Numeric[NumericCombine[A,B].AB]
implicitly[AB].plus(x.asInstanceOf[AB],y.asInstanceOf[AB])
}

plus(1f,2)//=3f
plus(1,2f)//=3f

I am aware of the fact that value conversions in Scala allows me to define

def plus[T](a: T, b: T)(implicit ev:Numeric[T]): T = ev.plus(a,b)

and achieve the behaviour above as suggested here, but since I want to use this function as part of a bigger function (which is described in the link mentioned as the context of this question), I need to parametrize the function with both A and B.

Update:

I made some good progress with this.

My NumericCombine now looks like this:

abstract class NumericCombine[A: Numeric, B: Numeric] {
        type AB <: AnyVal

        def fromA(x: A): AB
        def fromB(y: B): AB

        val numeric: Numeric[AB]

        def plus(x: A, y: B): AB = numeric.plus(fromA(x), fromB(y))
        def minus(x: A, y: B): AB = numeric.minus(fromA(x), fromB(y))
        def times(x: A, y: B): AB = numeric.times(fromA(x), fromB(y))
    }

and My plus function looks like:

def plus[A: Numeric, B: Numeric](x: A, y: B)(implicit ev:NumericCombine[A,B])
        : ev.AB = ev.plus(x, y)

The weighted average function requiring plus ended up becoming a bit more complicated:

def accumulateWeightedValue[A: Numeric,B: Numeric]
            (accum: (A, NumericCombine[A, B]#AB), ValueWithWeight: (A, B))
            (implicit combine: NumericCombine[A, B], timesNumeric: Numeric[NumericCombine[A, B]#AB])
            :(A,NumericCombine[A, B]#AB)=

this is a function that takes (A,AB),(A,B) and returns (A,AB). I use it internally inside weightedSum which just aggregates over this:

def weightedSum[A: Numeric,B: Numeric](weightedValues: GenTraversable[(A, B)])
(implicit numericCombine: NumericCombine[A, B], plusNumeric: Numeric[NumericCombine[A, B]#AB])
: (A, NumericCombine[A, B]#AB)

Now, this compiles fine. It does seem to have a problem with the second implicit parameter. ie Numeric[AB] when I run it with implicit values for say NumericCombine[Int,Float] present. It gives me:

could not find implicit value for parameter plusNumeric: Numeric[NumericCombine[Int,Float]#AB]

note that in NumericCombine, I have a Numeric[AB] which should be available for implicit look-up. storing it locally, in the case of [Int,Float]:

val lst: Seq[(Int, Float)] =List((1,3f),(1,4f))
implicit val num: Numeric[Float] = IntFloat.numeric //IntFloat extends NumericCombine[Int,Float]
weightedSum(lst)

in a local variable before invoking the function needing it doesn't seem to have any impact. So why is it being picked up by the implicit system.

Community
  • 1
  • 1
Maths noob
  • 1,684
  • 20
  • 42
  • 1
    Think of `NumericCombine[A,B]#AB` as a common type for `AB` of all instances of `NumericCombine[A,B]` which _could_ exist. So `accum` has the wrong type. – Alexey Romanov Apr 13 '17 at 21:52
  • I see. So is there any way of abstracting over these ABs? to allow other functions to use this generic plus? – Maths noob Apr 14 '17 at 14:19
  • You say `accumulateWeightedValue` is used inside `weightedSum`. So just make it a local method, and you can use `numericCombine.AB`. You shouldn't need `plusNumeric` or `Numeric` constraints on `A` and `B`. – Alexey Romanov Apr 14 '17 at 18:19
  • @AlexeyRomanov Are you saying that using implicits as parameters to a function will stop your functions from being "first class functions" and I should resort to using them locally only? – Maths noob Apr 16 '17 at 13:04
  • No, I am saying you should do it in this specific case because type of `accum` should depend on `combine`. – Alexey Romanov Apr 16 '17 at 17:59
  • @AlexeyRomanov well yeah but i'm extrapolating here... Let's say I was going to write a statistics library based on `NumericCombine`. There is going to be a lot of duplicated code if I am not going to be able to take `accumulateWeightedValue` out of the likes of `weightedSum`. – Maths noob Apr 17 '17 at 18:55
  • Ok. In this case I think it's possible, but quite tricky. And ultimately, I suspect, less usable than a library which works just with `A` and `Numeric[A]`. – Alexey Romanov Apr 17 '17 at 19:39
  • @AlexeyRomanov Well I would have been happy with just working with `A`, but if I write a `weightedSum` with only `A` and feed it a `Seq[(Int,Float)]` the compiler will tell me: **could not find implicit value for evidence parameter of type `Numeric[AnyVal]`.** So yeah I'd really be interested to know if this be done. – Maths noob Apr 17 '17 at 21:10
  • @AlexeyRomanov btw, your suggestion won't work. The minute I use a function like `plus` or `times` that takes an implicit and returns `combine.AB` , the return type of the function externally will be `NumericCombine[A, B]#AB` not `combine.AB` so you will hit the problem earlier. ultimately you'd have to bring `times`, `plus` inside the calling function which just becomes silly. This question seems to have the same problem as me: http://stackoverflow.com/questions/20473641/how-do-i-use-a-generic-type-projection-in-a-method-parameter but I am not sure what to make of the answer. – Maths noob Apr 17 '17 at 22:06

3 Answers3

3

Just use

def plus[A: Numeric,B:Numeric](x: A, y: B): NumericCombine[A,B]#AB

Note the # (hash) instead of . (dot). This is called "type projection". Dot notation is called "path dependent type". I'm telling you these names so that you can google for more info easily. Simply put, # is used for accessing types from classes/traits, and . is used for accessing types from objects/values.

Example:

trait Foo {
    type T   
}

val fooObj: Foo = new Foo {
    type T = Int
}

type t1 = fooObj.T
type t2 = Foo#T
slouc
  • 9,508
  • 3
  • 16
  • 41
  • 1
    Unfortunately, I don't think you can "just use" it for this specific question: return type for `plus[Int, Float]` will be `NumericCombine[Int, Float]#AB` and not `Float`. – Alexey Romanov Apr 13 '17 at 14:41
  • This is puzzling me. I have now defined def `plusAB[A: Numeric, B: Numeric](x: A, y: B) (implicit ev: NumericCombine[A, B]): ev.AB = ev.plus(x, y)` which works perfectly. Now if I create another function with exactly the same signature, but redirecting to the this function, ie: `plusAB2[..](...)=plusAB(x,y)` I get: **expression of type NumericCombine[A,B]#AB does not conform to expected type: ev.AB** am I being silly for expecting this to work? – Maths noob Apr 13 '17 at 20:16
  • hmmm actually reading the text of your answer is starting to give me an idea of what the problem is. I have now updated my question with some progress and a problem which is I think another manifestation of what I described above. Googling "Scala implicit type projection" is bringing some up some stuff that I don't quite understand. – Maths noob Apr 13 '17 at 20:52
1

An alternative to @slouc's answer is

def plus[A, B](x: A, y: B)(implicit ev: NumericCombine[A, B]): ev.AB

I'd also enhance NumericCombine:

trait NumericCombine[A, B] {
  type AB <: AnyVal
  def fromA(a: A): AB
  def fromB(b: B): AB
  val num: Numeric[AB]
}

abstract class NumericCombineImpl[A, B, R](implicit val num: Numeric[R], f1: A => R, f2: B => R) {
  type AB = R
  def fromA(a: A) = f1(a)
  def fromB(b: B) = f2(b)
}

implicit object IntFloat extends NumericCombineImpl[Int,Float,Float]
...

This would allow to actually implement plus, no casts required:

def plus[A, B](x: A, y: B)(implicit ev: NumericCombine[A, B]): ev.AB = 
  ev.num.plus(ev.fromA(x), ev.fromB(y))
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • ha I am actually working on this exact thing as the original plan just becomes tedious. with the addition that I have decided to move the plus times functions etc in the NumericCombine. it's just less classes and less implicit arguments. – Maths noob Apr 13 '17 at 14:55
  • This is how I ended up implementing the plus. https://pastebin.com/ne0KwfnK I would love to have your feedback. I am slightly unsure about asking R to be subtype of AnyVal. – Maths noob Apr 13 '17 at 15:57
  • also see: http://stackoverflow.com/questions/43392597/scala-accessing-class-type-alias-externally/43395151#comment73862648_43393363 I basically can't seem to use your suggested `num` varriable in dependent types, so I have been adding a second implicit for my functions whereas I would have hoped to somehow access num from combine. – Maths noob Apr 13 '17 at 20:56
1

* 18 Apr 2017: updated base on the latest code from author *

* 19 Apr 2017 *

  • Add NumericCombine#Implicits for convinence
  • Remove AnyVal constraints to support any Numeric types e.g. BigInt
  • Refactor NumericCombine

You need Aux pattern:

import scala.collection.GenSeq

trait NumericCombine[A, B] {
  type AB

  def fromA(x: A): AB

  def fromB(y: B): AB

  val numericA: Numeric[A]
  val numericB: Numeric[B]
  val numericAB: Numeric[AB]

  // For convenience, caller can 'import combine.Implicits._'
  // to bring the Numeric's into the current scope
  object Implicits {
    implicit def implicitNumericA = numericA

    implicit def implicitNumericB = numericB

    implicit def implicitNumericAB = numericAB
  }

  def plus(x: A, y: B): AB = numericAB.plus(fromA(x), fromB(y))

  def minus(x: A, y: B): AB = numericAB.minus(fromA(x), fromB(y))

  def times(x: A, y: B): AB = numericAB.times(fromA(x), fromB(y))
}

object NumericCombine {
  type Aux[A, B, _AB] = NumericCombine[A, B] {
    type AB = _AB
  }

  private def combine[A, B, _AB](fa: A => _AB, fb: B => _AB)
                               (implicit
                                _numericA: Numeric[A],
                                _numericB: Numeric[B],
                                _numericAB: Numeric[_AB]
                               ): NumericCombine[A, B] = new NumericCombine[A, B] {
      override type AB = _AB

      override def fromA(x: A): AB = fa(x)

      override def fromB(y: B): AB = fb(y)

      override val numericA: Numeric[A] = _numericA
      override val numericB: Numeric[B] = _numericB
      override val numericAB: Numeric[AB] = _numericAB
    }

  implicit lazy val IntFloat  = combine[Int, Float, Float](_.toFloat, identity)
  implicit lazy val BigIntBigDecimal  = combine[BigInt, BigDecimal, BigDecimal](i => BigDecimal(i), identity)

}

implicit class ValuesWithWeight[A, B](val weightedValue: (A, B)) {
  def weight: A = weightedValue._1

  def value: B = weightedValue._2
}

def weightedSum[A, B, AB]
(valuesWithWeight: GenSeq[(A, B)])
(implicit combine: NumericCombine.Aux[A, B, AB]):
(A, AB) = {

  import combine.Implicits._

  val z: (A, AB) =
    (combine.numericA.zero, combine.numericAB.zero)

  def accumulateWeightedValue(accum: (A, AB), valueWithWeight: (A, B)): (A, AB) = {
    val weightedValue = combine.times(valueWithWeight.weight, valueWithWeight.value)
    (
      combine.numericA.plus(accum.weight, valueWithWeight.weight),
      combine.numericAB.plus(accum.value, weightedValue)
    )
  }

  valuesWithWeight.aggregate(z)(
    accumulateWeightedValue,
    // dataOps.tuple2.plus[A,AB]
    {
      case ((a1, ab1), (a2, ab2)) =>
        (combine.numericA.plus(a1, a2) ->
          combine.numericAB.plus(ab1, ab2))
    }
  )
}

weightedSum(Seq(1 -> 1.5f, 2 -> 1f, 3 -> 1.7f))
weightedSum(Seq(BigInt(1) -> BigDecimal("1.5"), BigInt(2) -> BigDecimal("1"), BigInt(3) -> BigDecimal("1.7")))
PH88
  • 1,796
  • 12
  • 12
  • Interesting! So from what I understand, you're suggesting: 1) parameterizing the "private" functions with an additional `AB` type parameter and constructing the right `Numeric[A,B]` based on the `Aux` pattern. I am still not sure how to use a function utilising the `Aux` parameter inside my "public" functions taking only `A` and `B`. ie I want to use `accumulateWeightedValue` inside `weightedSum`. passing `accumulateWeightedValue[A,B,combine.AB]` to aggregate gives me: **scala method with dependent type cannot be converted to function value** – Maths noob Apr 18 '17 at 13:52
  • Hi @ShS, the key is you need to declare the AB type param for the type inference and in turn the implicit lookup to work correctly. You can visit the link on 'Aux pattern' for more details. – PH88 Apr 18 '17 at 15:19
  • I've updated my answer with a working version of your code. Also simplified the code a bit. See if it's what you need. – PH88 Apr 18 '17 at 15:20
  • Great. This works. I have modified your solution and refactored it slightly. see here: http://pastebin.com/69YCusc8 There is only one problem with it. I have to provide explicit implicit params if I am to pass the numeric params to other functions. Is there any way of doing some magic in the NumericCombine object like the AUX technique to bring the numericA, numericB, numericAB variables in "implicit scope" for the times,plus or addTuples function calls? [here is a complete working demo: https://gist.github.com/ShahOdin/9b3a5326c7ee355c1cbe76e8bfb2a97c ] – Maths noob Apr 18 '17 at 23:26
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/142063/discussion-between-shs-and-ph88). – Maths noob Apr 19 '17 at 10:11
  • duh! why did I not think of that! :) great this is all working now. Thanks. – Maths noob Apr 19 '17 at 10:37