2

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!

Wolfgang
  • 155
  • 9
  • A stylistic pet peeve only perhaps: I do not like postfix operations for `toInt` or `toString` in this code - I do not think they add anything here, to me they only harm readability a bit. In particular I prefer `foo.s.toInt + 1` over `(s toInt) + 1`. – Suma Oct 16 '15 at 08:01
  • @Suma - true, I'm still learning when to use the period and when not. Added those back in. – Wolfgang Oct 16 '15 at 08:20
  • @Wolfgang - I've searched in the same direction one time. See, what I got: http://stackoverflow.com/questions/1154571/scala-abstract-types-vs-generics/10891994#10891994 – ayvango Oct 16 '15 at 08:32

1 Answers1

3

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.

Peter Neyens
  • 9,770
  • 27
  • 33
  • "You cannot call `StringIntConverter.firstToSecond("a")`" - fortunately this is still detected compile time, therefore it does not seem like serious limitation. – Suma Oct 16 '15 at 08:05
  • I would prefer `implicit object convBarFoo extends Converter[Bar, Foo]` over `implicit val convBarFoo = new Converter[Bar, Foo]` to avoid anonymous class, but your taste may vary. – Suma Oct 16 '15 at 08:15
  • @Peter Neyens - thanks for the new approach. I like how this keeps the types "clean" and how my functionality could be added on to types I don't control. I was wondering where is the best place to define the `implicit Converter`s, inside the object that `extends RoundTripConverter`? This was the only way I could get your code to compile, and unfortunately I seem to have to use `implicit` parameters to any functions that use this instead of having `implicit val`s in my traits. Any suggestions how to avoid that verbosity? – Wolfgang Oct 18 '15 at 12:55