Wolfgang
Wolfgang

Reputation: 155

Scala Mutually Convertible Generic Types

I'm very new to Scala programming, and I really like the degree to which code is composable. I wanted to write some traits that deal with two related objects that are convertible to each other, and build more functionality by continuing to extend that trait so that when I create objects I can specify the related types for my generics. Here is a working toy example of the type of code I'm talking about:

trait FirstConverter[First] {
  def toFirst: First
}

trait SecondConverter[Second] {
  def toSecond: Second
}

trait TwoWayConverter[First <: SecondConverter[Second], Second <: FirstConverter[First]] {
  def firstToSecond(x: First) = x.toSecond
  def secondToFirst(x: Second) = x.toFirst
}

trait RoundTripConverter[First <: SecondConverter[Second], Second <: FirstConverter[First]] extends TwoWayConverter[First, Second] {
  def firstToFirst(x: First) = secondToFirst(firstToSecond(x))
  def secondToSecond(x: Second) = firstToSecond(secondToFirst(x))
}

case class A(s: String) extends SecondConverter[B] {
  def toSecond: B = B((s.toInt) + 1)
}

case class B(i: Int) extends FirstConverter[A] {
  def toFirst: A = A((i * 2).toString)
}

object ABConverter extends RoundTripConverter[A, B]

object Main {
  def main(args: Array[String]): Unit = {
    println(ABConverter firstToSecond A("10")) // 11
    println(ABConverter secondToFirst B(42)) // 84
    println(ABConverter firstToFirst A("1")) // 4
    println(ABConverter secondToSecond B(2)) // 5
  }
}

While this works, I'm not sure if it's idiomatic Scala. I'm asking if there are any tricks to make the type definitions more concise and if I can somehow define the type restrictions only once and have them used by multiple traits which extend other traits.

Thanks in advance!

Upvotes: 2

Views: 367

Answers (1)

Peter Neyens
Peter Neyens

Reputation: 9820

One way to improve your design would be to use a type class instead of inheriting from FirstConverter and SecondConverter. That way you could use multiple conversion functions for the same types and convert between classes you don't control yourself.

One way would be to create a type class which can convert an A into a B :

trait Converter[A, B] {
  def convert(a: A): B
}

trait TwoWayConverter[A, B] {
  def firstToSecond(a: A)(implicit conv: Converter[A, B]): B = conv.convert(a)
  def secondToFirst(b: B)(implicit conv: Converter[B, A]): A = conv.convert(b)
}

trait RoundTripConverter[A, B] extends TwoWayConverter[A, B] {
  def firstToFirst(a: A)(implicit convAB: Converter[A, B], convBA: Converter[B, A]) =
    secondToFirst(firstToSecond(a))
  def secondToSecond(b: B)(implicit convAB: Converter[A, B], convBA: Converter[B, A]) =
    firstToSecond(secondToFirst(b))
}

We could create type class instances for the following two classes Foo and Bar similar to your A and B

case class Foo(s: String)
case class Bar(i: Int)

implicit val convFooBarFoor = new Converter[Foo, Bar] {
  def convert(foo: Foo) = Bar((foo.s toInt) + 1)
}

implicit val convBarFoo = new Converter[Bar, Foo] {
  def convert(bar: Bar) = Foo((bar.i * 2) toString)
}

We then could create a FooBarConverter :

object FooBarConverter extends RoundTripConverter[Foo, Bar]

FooBarConverter firstToSecond Foo("10")  // Bar(11)
FooBarConverter secondToFirst Bar(42)    // Foo(84)
FooBarConverter firstToFirst Foo("1")    // Foo(4)
FooBarConverter secondToSecond Bar(2)    // Bar(5)

The only problem is because we can not pass parameters to a trait, we can not limit the types to types with a Converter type class instance. So you can create the StringIntConverter below even if no Converter[String, Int] and/or Convert[Int, String] instances exist.

object StringIntConverter extends TwoWayConverter[String, Int]

You cannot call StringIntConverter.firstToSecond("a") because the firstToSecond method needs the implicit evidence of the two mentioned type class instances.

Upvotes: 3

Related Questions