The trick is to cheat: come up with the answer I want first, and then work backwards to fill in the rest!
What I do is to add a new step at the beginning of composition. In this step, we start out with a "composition target." We then fold the composition operands' components in two phases: first the ones that are part of the target, and then the rest.
What's the target type? At compile time, it's empty; at run time, it's the compile-time type.
As always, I'll illustrate with an example. In fact, I'll use the same example as before. Here's the background stuff:
type List[A] = ... (put/get) size -> Int type Box[A] = ... (put/get) type Container: isEmpty -> Bool -- abstract method type Container Box[A]: @override isEmpty -> Bool = TODO intsList : List[Int] = list(1, 2, 3) intsBox : Box[Int] = box(4) tmp = intsBox <?> Container intsContainer : Container = tmp
... and here's the composition:
composed = intsContainer <?> intsList composedSize = composed2.size
The problem we had before was that
intsContaineris only a
Containerat compile-time, but it's a
Box[Int] <?> Containerat runtime. When
Containeris composed with
List[Int]at compile time, no conflicts are found, so the resulting type is
Container <?> List[Int]. But at run time,
List[Int]collide and cancel each other out, and the resulting object is only a
Container, which doesn't have a
The composition target saves the day. Everything works the same at compile time, leading to a type of
Container <?> List[Int]. At run time, we take the following steps:
- Decompose both operands, so that the composition is
(Box[Int] <?> Container) <?> List[Int].
- The target type is
Container <?> List[Int], so take those components out of the composition and fold them into the resulting object. We now have:
- A folded object of
Container <?> List[Int]
- Remaining object of
(Box[Int] <?> ∅) <?> ∅, which simplifies to just
- A folded object of
- Fold the rest of the components (just
List[Int], so discard it — and there's nothing else to fold.
So the resulting object is a
Container <?> List[Int], which is exactly what we expected at compile time!
I wrote above that the target type at compile time is empty, but even that can be improved upon: it's the closest available explicit type declaration, if any. So, if you had:
boxContainer = intsBox <?> Container composed2 = boxContainer <?> intsList
... then it would result in a compile-time error, since
intsBox : Box[Int]collides with
intsList : Box[Int](they both define
getmethods). But if you had:
composed3 : List[Int] = boxContainer <?> intsList
... then the target type is
List[Int], meaning that this gets folded in first. When the conflict with
Box[Int]is detected in the second folding stage,
Box[Int]is discarded. The resulting type of the composition (this is all at compile time, remember) is
List[Int] <?> Container, which is then "upcast" to
List[Int]when it's assigned to