0
votes

Firstly, some background:

I'm currently writing a generic solver for a certain class of problems (specifically, a structural SVM solver) in Scala. In order to use this, the user has to implement an interface.

Consider a simplified version of the interface:

trait HelperFunctions[X, Y] {
    def func1(x: X, y: Y): Y
    def func2(y1: Y, y2: Y): Int
}

and a simple implementation:

object ImplFunctions extends HelperFunctions[Vector[Double], Double] {
    def func1(x: Vector[Double], y: Double): Double  = { ... }
    def func2(y1: Double, y2: Double): Int = { .. }
}

(Note that the implementation has to be provided in form of an object.)

Now, the problem: I need to write a diagnostic suite which aids the user to verify the sanity of his functions. An example sanity test would involve something like func1(..) being positive for all x and y. For this, I set out by writing a BDD-style unit-tests using ScalaTest.

Note that the diagnostic suite should be generic, to work with any object which extends HelperFunctions[X, Y]. A high-level picture of how I planned to go about this: first providing all the sanity tests designed for generic X and Y. Then, the user merely replaces, say a ???, with a ImplFunctions and runs the suite.

But, turns out I didn't find an elegant approach to treat ImplFunctions as a generic HelperFunctions[X, Y]. Here's a taste of what I've tried so far:

import org.scalatest._

class ApplicationDiag extends FlatSpec {

  // Option 1:
  // Cast the ImplFunctions into a generic type
  // ERROR: type mismatch; found : ImplFunctions.type, required : HelperFunctions[X, Y]
  val helpers: HelperFunctions[X, Y] = ImplFunctions
  // OR
  def helpers[X, Y](): HelperFunctions[X, Y] = ImplFunctions


  // Option 2: User fills up the right side, no type specified for the value
  // WORKS. But, cannot explicitly state types
  val helpers = ImplFunctions

  "func1" should "be positive" in {
    // ... the check as described previously
   }

}

Due to the covariance nature, I imagine it should be possible to cast an instance of a type to its supertype. But, it seems to be tougher since this involves generics. Is there a clean way to do this?

Note: 1. I'm totally open to alternate designs for the diagnostic suite. 2. The real-world interface: HelperFunctions and a real-world application: ImplFunctions

1
If you want your types to be co or contra-variant you should declare them as such using the +X/-X syntax. You shouldn't need to cast.lmm

1 Answers

0
votes

I'm not sure how you write your generic tests, but maybe you should consider using type classes.

I'm going to use slightly simpler interfaces to demonstrate the idea but it should be easily applicable to your problem. For example, you have an interface

trait Helpers[A] {
  def f(a: A): A
}

And an implementation

object IntImpl extends Helpers[Int] {
  def f(a: Int) = -a
}

Unfortunately, Helpers[A] does not require any constraints on A which would be nice. But you probably do not want do modify the library.

It is still possible to write a generic test suite for some As:

class Diag[A : Numeric](h: Helpers[A]) {
  val fromInt = implicitly[Numeric[A]].fromInt _

  // just some assertions, using scalatests here should also work
  assert(h.f(fromInt(0)) == fromInt(0))
  assert(h.f(fromInt(1)) == fromInt(1)) // will fail with IntImpl
}

This suite can be used with every A for which we have an implementation of the Numeric type class (e.g. Int).

The concrete suite can now be instantiated with:

new Diag(IntImpl)

or if the test framework requires a class

class ImplDiag extends Diag(IntImpl)

This approach has the advantage that it is completely type-safe. You do not have to cast anything and the signature of Diag clearly specifies what is required to run the suite.