5

Is there a way to use Scala's type-system to concisely specify the context-relevant subgraph of a complete object graph?

DCI argues that you often have a fairly complex object graph but in any one use-case you often only want to work with a sub-graph. You have a Foo that has a Bar and a Bat, but when you're in use-case 1, you only care about the Bar and when in use-case 2, only about the Bat.

For instance, let's say that you have this structure, and the Role1 use-case requires Foo->Bar->Baz->Bin and Role2 use-case requires Foo->Bat->Baz->Buz:

class Foo{
   val bar = new Bar() //Only relevant to Role 1
   val bat = new Bat() //Only relevant to Role 2 
}

class Bar {
   val baz = new Baz() 
}

class Bat {
   val baz = new Baz()
}

//Relevant to both Role 1 and 2 (via Bar or Bat)
class Baz {
  val bin = new Bin() //Only relevant to Role 1
  val buz = new Buz() //Only relevant to Role 2
}

class Bin{}
class Buz{}

It's easy to see how you can constrain access in a single class by using traits:

trait FooInRole1 { def bar : Bar }  //Define accessor in trait
s/Foo/Foo extends FooInRole1/       //Change Foo's declaration to implement trait
val f : FooInRole1 = new Foo        //LHS is i'face, RHS is implementation
//f.bat <--Compile error              Irrelevant field is not available. \o/ 

But you have to repeat this pattern for every object relevant to the use-case. (For instance, you need a BazInRole1 to access bin and a BazInRole2 to access biz)

My question is whether there's some way to avoid writing all these easy-to-get-wrong, namespace-crowding traits. For instance, I could imagine something like this code (that doesn't compile):

class Foo[T] {
  T match { 
    case r1 : Role1 => def bar : Bar[T]
    case r2 : Role2 => def bat : Bat[T]
    case _ => //Nothing
  }
}

val fInRole1 = new Foo[Role1] //Provides Foo->Bar->Baz->Bin
val fInRole2 = new Foo[Role2] //Provides Foo->Bat->Baz->Buz

It seems like Scala's type-system is expressive enough to do something like this, but I cannot figure it out.

Larry OBrien
  • 8,484
  • 1
  • 41
  • 75
  • I think something like this can be achieved with type-classes. Just make the type-class the view on the object graph and access and manipulate its contents only through the type-class. – ziggystar Feb 23 '12 at 09:54

3 Answers3

1

Not extremely concise, and the members are there, just impossible to use, but maybe going in this direction would be acceptable?

class Foo[R] {
  def bar(implicit ev: R <:< Role1) = new Bar[R] //Only relevant to Role 1
  def bat(implicit ev: R <:< Role2) = new Bat[R] //Only relevant to Role 2
}
Michał Politowski
  • 4,288
  • 3
  • 30
  • 41
0

If I understood your question correctly (which I'm not sure of) you want Foo to provide one of either bar or bat depending on the type parameter of Foo.

My first shot would be:

class Bar
class Bat

trait BarExt { def bar = new Bar }
trait BatExt { def bat = new Bat }

trait Role
case object Role1 extends Role
case object Role2 extends Role

trait RoleProvider[From <: Role, To] {
  def apply(): To
}

object RoleProvider {
  implicit val r1 = new RoleProvider[Role1.type, Foo[Role1.type] with BarExt] {
    def apply() = new Foo[Role1.type] with BarExt
  }

  implicit val r2 = new RoleProvider[Role2.type, Foo[Role2.type] with BatExt] {
    def apply() = new Foo[Role2.type] with BatExt
  }
}

class Foo[T <: Role]

object Foo {
  def create[T <: Role, To](f: T)(implicit rp: RoleProvider[T,To]): To = rp()
}

so that

scala> Foo.create(Role1)
res1: Foo[Role1.type] with BarExt = RoleProvider$$anon$3$$anon$1@187b2d93    scala> Foo.create(Role1).bar

scala> Foo.create(Role1).bar
res2: Bar = Bar@7ea4b9da

scala> Foo.create(Role1).bat
<console>:12: error: value bat is not a member of Foo[Role1.type] with BarExt
              Foo.create(Role1).bat

and

scala> Foo.create(Role2).bat
res3: Bat = Bat@56352b57

scala> Foo.create(Role2).bar
<console>:12: error: value bar is not a member of Foo[Role2.type] with BatExt
              Foo.create(Role2).bar

One could get rid of BarExt and BatExt by pulling the corresponding declarations into the definitions of r1 and r2, however I find it "harder" to work with that:

implicit val r1 = new RoleProvider[Role1.type, Foo[Role1.type] { val bar: Bar }] {
  def apply() = new Foo[Role1.type] { val bar = new Bar }
}

implicit val r2 = new RoleProvider[Role2.type, Foo[Role2.type] { val bat: Bat }] {
  def apply() = new Foo[Role2.type] { val bat = new Bat }
}

At the bottom line, I'm still not convinced that this is exactly what you've been asking for, or is it?

fotNelton
  • 3,844
  • 2
  • 24
  • 35
  • I don't think it's exactly what I'm looking for. Think about Baz (or, worse, a graph of 20 classes with cross-references). To ensure that Baz.bin is only available in Role1, don't I have to define a trait (xExt) per object per role and write a conversion fn per edge? Maybe that's the best that can be done, but I was *hoping* to find a way to "cascade" the type-parameter from Foo to Baz concisely. Does that make sense? – Larry OBrien Feb 23 '12 at 19:31
  • So *all* methods are supposed to be contained in the original graph but when "viewing" the graph from a certain role, only a subset of those methods are exposed? I'm assuming that by "subgraph" you're referring to the same set of nodes but with different, type-dependent roles? – fotNelton Feb 23 '12 at 20:51
  • Yes, that's exactly what I'm hoping for. – Larry OBrien Feb 23 '12 at 21:12
  • 1
    Would it make sense to define a `toRole[T]` method for each node that returns a reference of the desired type? You could probably use the same implicit mechanism as in the example above. – fotNelton Feb 23 '12 at 21:20
  • Yes, *something* like toRole or RoleAdapter[T <: Role, U <: TypeThatCanAssumeRole] : U[T] would be nice and concise. You certainly cannot avoid writing *somewhere* that "Role1 means Foo with Bar, Baz with Bin, etc." It's just that you want that definition to be explicit and in a single place. And then if you have, as you say, some kind of toRole / inRole factory that takes care of the wiring in a single place. – Larry OBrien Feb 24 '12 at 18:18
0

In this artima article on DCI, the author presents a way to get a DCI architecture in Scala, which looks like what you are aiming for.

The basic idea is to define the methods that are relevant to your use-case in a trait, but instead of your approach it uses a self-type annotation to ensure it is an object of a certain base class.

So to make this a bit more approachable: you have a Data-class Data, which contains the elementary components of your data objects. When you want to realize a certain use-case, which likes to consider a Data object in a certain role Role you can prepare the role like this:

trait Role { self : Data => 
  def methodForOnlyThisUseCase = {...}
}

For execution of the use-case, you then create an object specific to this role via:

val myUseCaseObject = new Data with Role

Like this, the object myUseCaseObject is limited to exactly its Data constituents and the methods required for its role in the given use-case.

If it gets more complex, you may have to create something like a pseudo role trait, which defines the methods common to multiple use-cases. The self-type annotation of the use-case role would then point back to this pseudo trait, whereas the pseudo traits self-type annotation points to the corresponding data class.

Frank
  • 10,461
  • 2
  • 31
  • 46
  • Yes, that's an important article about DCI, and my question starts from this type of design, but the issue is that you have to explicitly write a trait for every class-in-role. In my example, you end up having to create a trait FooInRole1, FooInRole2 and BazInRole1, BazInRole2, and you even end up having to create a BarInRole1 in order to "pass through" the Role type to Baz. It *may be* that this is as good as you can do, but it seems like there ought to be a way to avoid so much namespace clutter. – Larry OBrien Feb 24 '12 at 18:09