Reputation: 1124
I am tinkling with Scala and would like to produce some generic code. I would like to have two classes, one "outer" class and one "inner" class. The outer class should be generic and accept any kind of inner class which follow a few constraints. Here is the kind of architecture I would want to have, in uncompilable code. Outer
is a generic type, and Inner
is an example of type that could be used in Outer
, among others.
class Outer[InType](val in: InType) {
def update: Outer[InType] = new Outer[InType](in.update)
def export: String = in.export
}
object Outer {
def init[InType]: Outer[InType] = new Outer[InType](InType.empty)
}
class Inner(val n: Int) {
def update: Inner = new Inner(n + 1)
def export: String = n.toString
}
object Inner {
def empty: Inner = new Inner(0)
}
object Main {
def main(args: Array[String]): Unit = {
val outerIn: Outer[Inner] = Outer.empty[Inner]
println(outerIn.update.export) // expected to print 1
}
}
The important point is that, whatever InType
is, in.update
must return an "updated" InType
object. I would also like the companion methods to be callable, like InType.empty
. This way both Outer[InType]
and InType
are immutable types, and methods defined in companion objects are callable.
The previous code does not compile, as it is written like a C++ generic type (my background). What is the simplest way to correct this code according to the constraints I mentionned ? Am I completely wrong and should I use another approach ?
Upvotes: 1
Views: 1449
Reputation: 149538
One approach I could think of would require us to use F-Bounded Polymorphism along with Type Classes.
First, we'd create a trait which requires an update
method to be available:
trait AbstractInner[T <: AbstractInner[T]] {
def update: T
def export: String
}
Create a concrete implementation for Inner
:
class Inner(val n: Int) extends AbstractInner[Inner] {
def update: Inner = new Inner(n + 1)
def export: String = n.toString
}
Require that Outer
only take input types that extend AbstractInner[InType]
:
class Outer[InType <: AbstractInner[InType]](val in: InType) {
def update: Outer[InType] = new Outer[InType](in.update)
}
We got the types working for creating an updated version of in
and we need somehow to create a new instance with empty
. The Typeclass Pattern is classic for that. We create a trait which builds an Inner
type:
trait InnerBuilder[T <: AbstractInner[T]] {
def empty: T
}
We require Outer.empty
to only take types which extend AbstractInner[InType]
and have an implicit InnerBuilder[InType]
in scope:
object Outer {
def empty[InType <: AbstractInner[InType] : InnerBuilder] =
new Outer(implicitly[InnerBuilder[InType]].empty)
}
And provide a concrete implementation for Inner
:
object AbstractInnerImplicits {
implicit def innerBuilder: InnerBuilder[Inner] = new InnerBuilder[Inner] {
override def empty = new Inner(0)
}
}
Invoking inside main:
object Experiment {
import AbstractInnerImplicits._
def main(args: Array[String]): Unit = {
val outerIn: Outer[Inner] = Outer.empty[Inner]
println(outerIn.update.in.export)
}
}
Yields:
1
And there we have it. I know this may be a little overwhelming to grasp at first. Feel free to ask more questions as you read this.
Upvotes: 2
Reputation: 27535
I can think of 2 ways of doing it without referring to black magic:
with trait:
trait Updatable[T] { self: T =>
def update: T
}
class Outer[InType <: Updatable[InType]](val in: InType) {
def update = new Outer[InType](in.update)
}
class Inner(val n: Int) extends Updatable[Inner] {
def update = new Inner(n + 1)
}
first we use trait, to tell type system that update
method is available, then we put restrains on the type to make sure that Updatable
is used correctly (self: T =>
will make sure it is used as T extends Updatable[T]
- as F-bounded type), then we also make sure that InType will implement it (InType <: Updatable[InType]
).
with type class:
trait Updatable[F] {
def update(value: F): F
}
class Outer[InType](val in: InType)(implicit updatable: Updatable[InType]) {
def update: Outer[InType] = new Outer[InType](updatable.update(in))
}
class Inner(val n: Int) {
def update: Inner = new Inner(n + 1)
}
implicit val updatableInner = new Updatable[Inner] {
def update(value: Inner): Inner = value.update
}
First we define type class, then we are implicitly requiring its implementation for our type, and finally we are providing and using it. Putting whole theoretical stuff aside, the practical difference is that this interface is that you are not forcing InType
to extend some Updatable[InType]
, but instead require presence of some Updatable[InType]
implementation to be available in your scope - so you can provide the functionality not by modifying InType
, but by providing some additional class which would fulfill your constrains or InType
.
As such type classes are much more extensible, you just need to provide implicit for each supported type.
Among other methods available to you are e.g. reflection (however that might kind of break type safety and your abilities to refactor).
Upvotes: 1