While it can seem awkward, considering how rare it is to explicitly pass implicit parameters, I think the cost is low. And in my opinion, the case is quite strong for allowing normal parameter blocks to follow implicit parameter blocks. It aids usability in generic programming. Martin’s example shows the issue I’m talking about:
This kind of type dependency is extremely common in generic programming, where implicits are responsible for computing the some of the types needed in the parameter list of a method. As it stands, these types need to be added as type parameters, which can lead to a blowup in type parameters and reduced readability/writability of generic functions. It forces you to reason about type inference and implicit resolution at the same time, which gets very confusing. I have run into this issue a fair bit when doing generic programming.
Interspersing implicit and explicit parameters would help a lot here, as you can make the implicit parameter precede any explicit parameters whose types depend on it. That way you know exactly when/how those types are computed. I can give you a concrete example from shapeless, with a method that updates a value in a generic record. We can start with a naive-ish implementation:
final class RecordOps[L <: HList](val l : L) extends AnyVal with Serializable {
// ...
def updateWith[W, V](k: Witness)(f: V => W)
(implicit modifier: Modifier[L, k.T, V, W]): modifier.Out = modifier(l, f)
// ...
}
Here the resolution of the implicits depends on the modifier function’s parameter type V
. As a result, V
must be annotated at the use-site (I’ve just verified this), which makes this method totally unusable for nested records. In the actual implementation in shapeless, the issue is solved in a somewhat roundabout way:
final class RecordOps[L <: HList](val l : L) extends AnyVal with Serializable {
// ...
def updateWith[W](k: WitnessWith[FSL])(f: k.instance.Out => W)
(implicit modifier: Modifier[L, k.T, k.instance.Out, W]): modifier.Out = modifier(l, f)
type FSL[K] = Selector[L, K]
// ...
}
Here WitnessWith
, in combination with a Selector
that identifies the type of value associated with a record key, is used to compute the type of f
’s parameter so it doesn’t have to be annotated. Essentially, WitnessWith
is used to hack implicit resolution earlier into the parameter list. It uses machinery that honestly I don’t fully understand, but it looks like it involves macros and implicit conversions and depends on having a singleton Witness
to anchor onto. It seems rather complex and brittle. However, if explicit parameter lists could follow implicit ones, the solution would be clear:
final class RecordOps[L <: HList](val l : L) extends AnyVal with Serializable {
// ...
def updateWith[W](k: Witness) with (selector: Selector[L, k.T]) (f: selector.Out => W)
with (modifier: Modifier[L, k.T, selector.Out, W]): modifier.Out = modifier(l, f)
// ...
}
This is simpler than the current implementation, and I hate to imagine how the issue would have to be resolved in cases where WitnessWith
doesn’t cut it.