1
votes

I'm writing a simple Scala application using the Cake Pattern, but I'm running into trouble with a particular use case. Usually, I define a component with some existential type (MyType) that is retricted by a trait (MyTypeLike). This allows to "inject" a particular implementation that explicits that type (class MyType extends MyTypeLike). However, in some cases, I would need to define an existential type with several subtypes like in the example below. This is when I run into some trouble.

trait ApiComponent {

    type Api <: ApiLike

    trait ApiLike

    type RestApi <: RestApiLike

    trait RestApiLike extends ApiLike {
        /* omitted for brevity */
    }

    type SoapApi <: SoapApiLike

    trait SoapApiLike extends ApiLike {
        /* omitted for brevity */
    }

    type WebsocketApi <: WebsocketApiLike

    trait WebsocketApiLike extends ApiLike {
        /* omitted for brevity */
    }

    def apiForName: PartialFunction[String, Api]

}

trait ApiComponentImpl extends ApiComponent {

    class TwitterApi extends RestApiLike {
        /* omitted for brevity */
    }

    class SalesforceApi extends SoapApi {
        /* omitted for brevity */
    }

    override def apiForName: PartialFunction[String, Api] = {
        case "twitter" => new TwitterApi // Compiler error: TwitterApi is not a subtype of Api
        case "salesforce" => new SalesforceApi // Compiler error: SalesforceApi is not a subtype of Api
    }

}

trait SomeOtherComponent {
    self: ApiComponent => 

    def doSomethingWithTwitter = apiForName lift "twitter" map { twitterApi => 
        /* Do something with Twitter API */
    }

}

It's easy to see why that doesn't work. RestApi is not a subtype of Api, but RestApiLike is a subtype of ApiLike, so TwitterApi is only a subtype of RestApiLike and ApiLike. Oops! Of course, a simple solution would be to change everything to *Like. However, that kind of shoots the whole idea of existential types in the head. Also, by varying where there is a *Like where there's not, I can arbitrarily make the compiler complain at different places, but I can't make the compiler happy :-(

How can fix this? I'm happy to take a quick fix, but I would also like to get some more detailed design feedback and other suggestions.

2
type Api <: ApiLike etc. introduces an abstract type. Existential types are something different, which your code isn't using (which is a good thing). As for a quick fix, putting type Api = ApiLike into ApiComponentImpl would make it compile. You could also make apiForName return an ApiLike. I don't really understand why you have separate * and *Like traits, so I can't recommend something better.wingedsubmariner
I don't get why you are declaring all those abstract types, what do you gain by this? IMHO it's a lot of boilerplate and complexity, maybe it's a good idea to make more simple and straight forward?regexp
@wingedsubmariner: you're technically quite right, but the distinction between existentials and abstract types is redundant, and the work on dotty is showing that Scala can do without this distinction.Blaisorblade
@regexp I agree that's a lot of boilerplate and complexity. The point with these abstract types was to have client components use them without knowing their instantiation. I've been using this pattern ever since I read an article from Precog (by Daniel Spiewak) describing the benefits. I'm quite happy to get your opinion on whether that's a good idea or not in this particular situation and how you would do it differently :-)user510159
@wingedsubmariner I'm not sure I understand the difference between the two. The article I was referring to in my previous comment from Precog specially calls these 'existential'. Unfortunately, that article is no longer online. Do you have any good reference for me to understand these concepts better?user510159

2 Answers

1
votes

@wingedsubmariner is right that you can add type Api = ApiLike in the implementation. But that's too restrictive. I'll explain what else works and how you can figure it out.

  1. Since ApiComponent only declares that Api <: ApiLike — that is, Api >: Nothing <: ApiLike, Api could well be Nothing. So, the typechecker will accept expression e when you need to return Api only if e has type Nothing — that is, if e fails. In other words, you cannot return a value of type Api.
  2. Conversely, to return a value of type Api, you need to give a more specific lower bound. Saying that Api = ApiLike is one way, but you can just fix Api to be a more specific type, still inheriting from ApiLike: This allows Api to have more methods (hidden from clients) in implementations like ApiComponentImpl. See Sol.1 / 2 / 3 below.
  3. In all implementations of ApiComponent, all implementations of RestApi and RestApiLike need to descend from Api. This does not show up in the code you have, but
    • clients might want to know that, so you should say that RestApi <: Api — you can give two upper bounds by using their intersection, created with with.
    • you might want to force instances of RestApiLike (or *Like) to also inherit from Api. Since Api is an abstract type, you cannot say that a trait extends Api. Instead, you can use a self-type annotation to specify that: this adds a constraint for implementors.

For example:

  trait ApiComponent {

    type Api <: ApiLike // with Api //EDIT, Api <: Api makes no sense.

    trait ApiLike

    type RestApi <: RestApiLike with Api

    trait RestApiLike extends ApiLike {
      this: Api =>
      /* omitted for brevity */
    }

    type SoapApi <: SoapApiLike

    trait SoapApiLike extends ApiLike {
      this: Api =>
      /* omitted for brevity */
    }

    type WebsocketApi <: WebsocketApiLike

    trait WebsocketApiLike extends ApiLike {
      this: Api =>
      /* omitted for brevity */
    }

    def apiForName: PartialFunction[String, Api]

  }

  trait ApiComponentImpl extends ApiComponent {
    //Sol. 1:
    //type Api = ApiLike
    //Sol. 2:
    //trait Api extends ApiLike {
      //decls
    //}
    //Sol. 3, equivalent to 2:
    trait Api2 extends ApiLike {
      //decls
    }
    type Api = Api2

    class TwitterApi extends RestApiLike with Api {
      /* omitted for brevity */
    }

    class SalesforceApi extends SoapApiLike with Api {
      /* omitted for brevity */
    }

    override def apiForName: PartialFunction[String, Api] = {
      case "twitter" => new TwitterApi // Compiler error: TwitterApi is not a subtype of Api
      case "salesforce" => new SalesforceApi // Compiler error: SalesforceApi is not a subtype of Api
    }

  }

  trait SomeOtherComponent {
    self: ApiComponent =>

    def doSomethingWithTwitter = apiForName lift "twitter" map { twitterApi =>
      /* Do something with Twitter API */
    }

  }

EDIT:

  • I can't say whether the cake pattern is too complicated (that's for you to decide), but to learn it you need to acquire a solid understanding of the involved concepts, and it will take some time. I don't think Spiewak's post gives enough background if you're not already familiar with the concepts. My answer is not really sufficient for that, so I'll add some pointers.
    • However, in the general case the problem is not much simpler than this.
    • You might not be in the general case, so you might not be needing the full power of the cake pattern. I know your snippet is simplified, but for the code you show, Java interfaces would be powerful enough.
  • A nice reference: http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di/
  • My favorite reference is in fact this paper: http://lampwww.epfl.ch/~odersky/papers/ScalableComponent.pdf. Good papers are great at motivating the features they describe, though they talk to a different audience; feel free to ignore sentences you don't understand (and probably skip "Related work").
0
votes

For the code you are showing, do you need the full power of the cake pattern? Not really. In this answer, I provide a simplification of this design, and I discuss for what you might need the code you show. I imagine your actual code might require some intermediate form.

How do we simplify this code:

  1. The only client you show simply accesses the API in Api, and the other classes look like simple implementations of this API, with more methods which are not accessible to the client. In this case, you don't need all those interfaces in ApiComponent.
  2. By declaring type Api <: ApiLike; trait ApiLike {...} instead of having simply trait Api {...}, you allow ApiComponentImpl to refine Api to a more specific interface, so that clients of Api within ApiComponentImpl can use the more specific interface. Do you actually need that?
  3. In fact, you also have a single implementation of ApiComponent. As long as this is not not going to change you could simplify this code further by conflating ApiComponent and ApiComponentImpl, but I guess you do want to keep this separation because you want to hide part of ApiComponentImpl and allow other implementations.

So, here's an (over)simplified version of your code:

trait ApiComponent { trait Api { ... } def apiForName: PartialFunction[String, Api] } trait ApiComponentImpl { //Many implementations of Api //... def apiForName: PartialFunction[String, Api] = //pick which. }