1

I am playing with generics and upper bounds in scala and I am facing the following problem:

Let's say I have the following function (where I only expect either Int or Char):

def foo[T](tuple: Tuple2[T, T]) = tuple match {
  case Tuple2(x: Int, y: Int) => (x to y).toArray
  case Tuple2(x: Char, y: Char) => (x to y).toArray
}

I would expect to have a better and compressed form like:

def foo2[T >: Int with Char <: AnyVal](tuple: (T, T))  = (tuple._1 to tuple._2).toArray

But this is definitely not working.

Any ideas?

jalv1039
  • 1,663
  • 3
  • 17
  • 21

2 Answers2

2

Here's the relatively-horrifying signature you can use to get the (tup._1 to tup._2).toArray syntax on your implementation:

def foo[T 
  <% RangedProxy[T]{ type ResultWithoutStep <: TraversableOnce[T] } 
  : ClassTag
](tup: (T, T))

Breaking it down:
T is implicitly convertable to a RangedProxy[T], which is where the to method is defined. The result type of to depends on the type member ResultWithoutStep, which we must ensure is a subclass of TraversableOnce[T] so that we can call .toArray on it.

Note that neither the above signature or @MichaelZajac's answer will handle your requested functionality of "only work with Int and Char". Really, the best approach to satisfy that would be to use a typeclass.

sealed trait Fooable[T] extends ((T, T) => Array[T])
object Fooable {
  private def impl[T](f: (T, T) => Array[T]) = 
    new Fooable[T]{ def apply(min: T, max: T): Array[T] = f(min, max) }
  implicit val intFooable = impl[Int]{ (i,j) => (i to j).toArray }
  implicit val charFooable = impl[Char]{ (a,b) => (a to b).toArray }
}
def foo[T: Fooable](tup: (T, T)) = implicitly[Fooable[T]].apply(tup._1, tup._2)

Unfortunately, the typeclass approach is a bit cumbersome for this particular problem, but if you focus on the implicit val xFooable you'll find that it's kind of similar to the following:

// unfortunately this doesn't work due to type erasure on the JVM,
// which is why we've resorted to typeclasses as above
def foo(tup: (Int, Int)) = ...
def foo(tup: (Char, Char)) = ...
Dylan
  • 13,645
  • 3
  • 40
  • 67
  • Adding `>: Int with Char` to mine will limit it to `Int` and `Char`, though there's little practical reason to enforce it, as `Integral` prevents client code from using another type that would otherwise break it. – Michael Zajac Nov 08 '16 at 16:05
1

It would be easier to simply call NumericRange.inclusive yourself, rather than try to sort out the implicits and type constraints that would be needed to use the to syntax. If we require an implicit Integral, we'll know how to construct the range with a step of one (provided by the Integral instance). We'll also need a ClassTag[A] to create the Array generically:

import scala.reflect.ClassTag
import scala.collection.immutable.NumericRange

def foo2[A >: Int with Char : ClassTag](tuple: (A, A))(implicit int: Integral[A]) = 
   NumericRange.inclusive(tuple._1, tuple._2, int.one).toArray

scala> foo2(('a', 'z'))
res13: Array[Char] = Array(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z)

scala> foo2((1, 10))
res14: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Others fail (if you want them to with the type constraint):

scala> foo2((1L, 10L))
<console>:19: error: could not find implicit value for parameter int: Integral[AnyVal]
       foo2((1L, 10L))
           ^
Michael Zajac
  • 55,144
  • 7
  • 113
  • 138