I want to look into the transformation of optionally-executed blocks (esp. if-else
and switch
) a bit more. I looked into the examples in the Awesome GitHub, and categorized them into three flavours β :
- Heterogeneous: where each block produces different types, or the information about which branch produces the result is preserved,
- Homogeneous: where each block produces the same type, and the branch information is discarded,
- Incongruent: where the optionally-executed block is disallowed.
For type-erasing builders, such as ArrayBuilder
, the homogeneous transformation is the most common, followed by the incongruent case. For type-preserving builders (mostly SwiftUI-inspired and/or UI-related), the common case is heterogeneous transformation.
The incongruent case is achieved simply by not defining buildOptional
, and heterogeneous transformation by defining both buildOptional
and buildEither
. The homogeneous transformation, however, is very ceremonious, requiring the builder to define three functions:
func buildOptional(...) -> Component { ... }
func buildEither(first: Component) -> Component { first }
func buildEither(second: Component) -> Component { second }
At the same time, when only buildOptional
is defined, the partial results (for the lack of a better naming) transformation is used β‘. The problem is that the partial result case is not being used by any of the exemplars.
I propose that for the case with only buildOptional
, we use a slightly different transformations, which I call homogeneous injection. The homogeneous injection is akin to when you define buildEither
that simply passes the values. More precisely, the homogeneous transformation is as follows:
- Declare
vMerged
at the beginning of the branching blocks.
- For each result producing block, assign
buildOptional(block)
to vMerged
.
- For non-result producing block, assign
buildOptional(nil)
to vMerged
.
- Type-check all
vMerged
assignment together.
So the code
if condition1 { a }
else if condition2 { b }
is transformed into
let vMerged
if condition1 {
let block = ...
vMerged = Builder.buildOptional(block)
} else if condition2 {
let block = ...
vMerged = Builder.buildOptional(block)
} else {
vMerged = Builder.buildOptional(nil)
}
And of course, we can also skip buildOptional
if there are only result-producing cases, like in switch
. So now the API authors can:
- Use injection tree for heterogenous transformation by defining
buildOptional
and buildEither
,
- Use homogeneous injection for homogeneous transformations by defining only
buildOptional
, and
- Disable optionally-executed blocks by not defining
buildOptional
.
It also seems to simplify the explanation, since this case can be seen as skipping buildEither
, similar to how buildExpression
and buildFinalResult
are skipped when not defined.
Footnotes:
β From the Awesome GitHub:
The rest are incongruent cases. I also saw some builders define: buildIf(_: Component?) -> Component
or even buildIf(_: Components?...) -> Components
. From what I can grok, they implement it as a homogeneous transformation.
β‘ In my testing with Xcode 12.0 Beta 2, if you define only buildOptional
, you can only use if
without else
or else if
. The proposal seems to suggest that you can still use else
and else if
, which will produce more variables. Could someone confirm which of the two behaviours is the intended one? It doesn't affect my suggestion here, but it's always good to clarify cases like this.