3

Hello: I've been learning Scala recently (my related background is mostly in C++ templates), and I've run into something I currently don't understand about Scala, and it is driving me insane. :(

(Also, this is my first post to StackOverflow, where I've noticed most of the really awesome Scala people seem to hang out, so I'm really sorry if I do something horrendously stupid with the mechanism.)

My specific confusion relates to implicit argument binding: I have come up with a specific case where the implicit argument refuses to bind, but a function with seemingly identical semantics does.

Now, it of course could be a compiler bug, but given that I just started working with Scala, the probability of me having already run into some kind of serious bug are sufficiently small that I'm expecting someone to explain what I did wrong. ;P

I have gone through the code and whittled it quite a bit in order to come up with the single example that doesn't work. Unfortunately, that example is still reasonably complex, as the problem seems to only occur in the generalization. :(

1) simplified code that does not work in the way I expected

import HList.::

trait HApplyOps {
    implicit def runNil
        (input :HNil)
        (context :Object)
        :HNil
    = {
        HNil()
    }

    implicit def runAll[Input <:HList, Output <:HList]
        (input :Int::Input)
        (context :Object)
        (implicit run :Input=>Object=>Output)
        :Int::Output
    = {
        HCons(0, run(input.tail)(context))
    }

    def runAny[Input <:HList, Output <:HList]
        (input :Input)
        (context :Object)
        (implicit run :Input=>Object=>Output)
        :Output
    = {
        run(input)(context)
    }
}

sealed trait HList

final case class HCons[Head, Tail <:HList]
    (head :Head, tail :Tail)
    extends HList
{
    def ::[Value](value :Value) = HCons(value, this)
}

final case class HNil()
    extends HList
{
    def ::[Value](value :Value) = HCons(value, this)
}

object HList extends HApplyOps {
    type ::[Head, Tail <:HList] = HCons[Head, Tail]
}

class Test {
    def main(args :Array[String]) {
        HList.runAny(   HNil())(null) // yay! ;P
        HList.runAny(0::HNil())(null) // fail :(
    }
}

This code, compiled with Scala 2.9.0.1, returns the following error:

broken1.scala:53: error: No implicit view available from HCons[Int,HNil] => (java.lang.Object) => Output.
        HList.runAny(0::HNil())(null)

My expectation in this case is that runAll would be bound to the implicit run argument to runAny.

Now, if I modify runAll so that, instead of taking its two arguments directly, it instead returns a function that in turn takes those two arguments (a trick I thought to try as I saw it in someone else's code), it works:

2) modified code that has the same runtime behavior and actually works

    implicit def runAll[Input <:HList, Output <:HList]
        (implicit run :Input=>Object=>Output)
        :Int::Input=>Object=>Int::Output
    = {
        input =>
        context =>
        HCons(0, run(input.tail)(context))
    }

In essence, my question is: why does this work? ;( I would expect that these two functions have the same overall type signature:

1: [Input <:HList, Output <:HList] (Int::Input)(Object):Int::Output
2: [Input <:Hlist, Output <:HList] :Int::Input=>Object=>Int::Output

If it helps understand the problem, some other changes also "work" (although these change the semantics of the function, and therefore are not usable solutions):

3) hard-coding runAll for only a second level by replacing Output with HNil

    implicit def runAll[Input <:HList, Output <:HList]
        (input :Int::Input)
        (context :Object)
        (implicit run :Input=>Object=>HNil)
        :Int::HNil
    = {
        HCons(0, run(input.tail)(context))
    }

4) removing the context argument from the implicit functions

trait HApplyOps {
    implicit def runNil
        (input :HNil)
        :HNil   
    = {
        HNil()  
    }

    implicit def runAll[Input <:HList, Output <:HList]
        (input :Int::Input)
        (implicit run :Input=>Output)
        :Int::Output
    = {
        HCons(0, run(input.tail))
    }

    def runAny[Input <:HList, Output <:HList]
        (input :Input) 
        (context :Object)
        (implicit run :Input=>Output)
        :Output 
    = {
        run(input)
    }
}

Any explanation anyone may have for this would be much appreciated. :(

(Currently, my best guess is that the order of the implicit argument with respect to the other arguments is the key factor that I'm missing, but one that I'm confused by: runAny has an implicit argument at the end as well, so the obvious "implicit def doesn't work well with trailing implicit" doesn't make sense to me.)

Jay Freeman -saurik-
  • 1,759
  • 1
  • 13
  • 13
  • 1
    can't you make that a little shorter? – Kim Stebel May 30 '11 at 08:32
  • `runNil` and `runAll` are both needed (in order to have the repeating and terminating cases: `runNil` works fine, and as demonstrated by #3 hardcoding it to two levels then works); meanwhile, `context` is also needed (as demonstrated by #4, where I remove it and it then works); I already simplified the HList down as much as I could (which is why I have to use `HNil()` instead of the `HNil` that most similar code actually uses); finally, I need `runAny`, as that's the function that actually takes the `implicit` parameter; so no: as stated, I already tried and can't simplify it more than this :( – Jay Freeman -saurik- May 30 '11 at 08:51

2 Answers2

6

When you declare an implicit def like this:

implicit def makeStr(i: Int): String = i.toString

then the compiler can automatically create an implicit Function object from this definition for you, and will insert it where an implicit of type Int => String is expected. This is what happens in your line HList.runAny(HNil())(null).

But when you define implicit defs which themselves accept implicit parameters (like your runAll method), it doesn't work any more, as the compiler cannot create a Function object whose apply method would require an implicit — much less guarantee that such an implicit would be available at the call site.

The solution to this is to define something like this instead of runAll:

implicit def runAllFct[Input <: HList, Output <: HList]
    (implicit run: Input => Object => Output):
        Int :: Input => Object => Int :: Output =
  { input: Int :: Input =>
    context: Object =>
      HCons(0, run(input.tail)(context))
  }

This definition is a bit more explicit, as the compiler now won't need to try to create a Function object from your def, but will instead call your def directly to get the needed function object. And, while calling it, will automatically insert the needed implicit parameter, which it can resolve right away.

In my opinion, whenever you expect implicit functions of this type, you should provide an implicit def that does indeed return a Function object. (Other users may disagree… anyone?) The fact that the compiler is able to create Function wrappers around an implicit def is there mainly, I suppose, to support implicit conversions with a more natural syntax, e.g. using view bounds together with simple implicit defs like my first Int to String conversion.

Community
  • 1
  • 1
Jean-Philippe Pellet
  • 59,296
  • 21
  • 173
  • 234
  • First off, thank you so much for taking the time to help me! So, your answer fully/directly explains why my original attempt (#1) didn't work, and why hoisting the implicit argument to the front did (#2). However, the other two implementations, #3 and #4, while not being direct replacements for the original code, should have this same problem. Why do these work, given this explanation? In #4, I have removed the `context` argument, but otherwise have the same situation: an `implicit def` with an `implicit` parameter; the removed argument was of a boring type (`Object`)... why does this help? – Jay Freeman -saurik- May 30 '11 at 21:09
  • @Jay I couldn't exactly tell; maybe you're pushing Scala's type inference capabilities to its limits in these examples. You're better off translating each `implicit def` to return a `Function` object instead. – Jean-Philippe Pellet May 30 '11 at 21:32
  • Yes: I could tell that specifying my `implicit def`s in that manner worked better, but I was hoping to actually learn how Scala's implicit parameter binding mechanism worked, so I'd be able to better predict its abilities in future situations that might not quite look like this one (the same as I've come to understand, and even internalize, Koenig lookup from C++). So far, it seems we are still left with "seems like a complex example that stresses the black magic we don't understand well", and have not actually ruled out "this could be a compiler bug demonstrated by these complex examples" :(. – Jay Freeman -saurik- May 30 '11 at 22:33
  • Ok, I've been sitting around comparing horrendous compiler output traces dumped with -Ytyper-debug, and this explanation is incorrect: the compiler, in fact, is able to (and does) bind `implicit` arguments, even for this specific non-working case, on an `implicit def` that occur after the other arguments. What isn't working, however, is that the type of `Output` is being detected as `Nothing` in this case, so when it attempts to bind the implicit argument for `runAll` itself, it does not believe that `runNil` is a compatible match. I still haven't determined exactly why this happens, though. – Jay Freeman -saurik- May 31 '11 at 01:06
  • So, based on the output from -Ytyper-debug I thought I might try adding a slightly more specific type bound on `runAll` (adding `>:HList`), and I managed to "fix" what was being detected by the type inferencer: `implicit def runAll[Input <:HList, Output >:HList <:HList]`. However, this now seems entirely backwards to me as `Output` in this context is supposed to be `HNil`, which is a subtype of `HList`, but here I've gone ahead and asserted that `Output` is a _supertype_ (in addition to being a subtype, so really: equivalent) to `HList`, which should exclude `HNil` from being accepted here. :( – Jay Freeman -saurik- May 31 '11 at 02:03
  • Ok... but that didn't actually do the same thing anymore, so I feel somewhat better: now, the return type of `runAny` is apparently `Int::HList` (which makes sense), so if you try to do `.tail.head` on the result of the `runAny` call you get this: `working5.scala:48: error: value head is not a member of HList println(HList.runAny(0::0::HNil())(null).tail.head)` So, I'll continue staring at the output of -Ytyper-debug to see if I can figure out why the `Nothing` is being inferred for `Output` (in addition to what is going on to allow the code for #4 to work: it seems to be "adapted"). – Jay Freeman -saurik- May 31 '11 at 02:05
  • @Jay Good investigations, I'll update my answer accordingly. You may want to explain it more in your own answer and accept it; it reads probably better than in comments. – Jean-Philippe Pellet May 31 '11 at 07:19
  • Uhhh... so it turns out that the #4 case _compiles_, but it doesn't actually _work_. It turns out the implicit function that is being bound to the `run` argument, with type `Input=>Output`, where `Input` happens to always be the same as `Output`, is simply `conforms:<:<[Input,Output]`, which is defined in Predef.scala: `implicit def conforms[A]: A <:< A = new (A <:< A) { def apply(x: A) = x }`... the fact that this function exists and can randomly get bound to my `implicit` parameters is scary, to be honest. _So_, the result is that _deleting_ `runNil` and `runAll` in #4 has the same behavior. – Jay Freeman -saurik- May 31 '11 at 08:09
  • @Jay Yes, it is true that normally, an implicit param of type `A => B` should be seen as a request from the compiler to provide a function that converts from `A` to `B`. In trivial cases, `conforms` does the job. Maybe you'd be better off working only with your own class for implicit parameters, e.g., `HlistProcessor`, which has a method with the appropriate signature. Then you are sure that no one else can provide such implicits. *(With these new comments, I've [temporarily?] removed my edit which said that my explanation was wrong.)* – Jean-Philippe Pellet May 31 '11 at 08:30
  • Yeah: I certainly am learning the lesson here that the type-based mechanism for binding `implicit` parameters means that you need to be very careful to not set yourself up for fail by having too-generic types: I am going to make certain to use something like the `HlistProcessor` you suggest in the future (which is closer in semantics to, for example, `Manifest` or `CanBuildFrom`). Regardless, the comments I made regarding how the type adapter is trying to handle the original code stands: it is mapping `Output` to `Nothing` and _is_ _trying_ to resolve `implicit`, but failing due to `Nothing`. – Jay Freeman -saurik- May 31 '11 at 08:54
  • @Jay Maybe it's worth extracting a minimal example showing that behavior clearly and asking another question (which may draw more people into the conversion, too). – Jean-Philippe Pellet May 31 '11 at 08:58
  • OK, I understand exactly what is happening now (I finally broke down and _read the source code for the type inferencer_): while `runAny` _is_ considered an `implicit def` available for this usage, the problem _is_ that its type is not being inferred correctly. Specifically, there is a mechanism in the compiler for allowing "undetermined type params" to be specified indirectly, by way of an `implicit` parameter; this is required in this case, as `Output` is not directly bound to anything, or even used in any of its arguments: it is indirectly determined by the return type of the `run` argument. – Jay Freeman -saurik- May 31 '11 at 10:51
  • The same thing actually happens in #3, but it isn't fatal there, as the type `Output` being bound to `Nothing` is irrelevant: I could have left `Output` out entirely. In the case of #3, then, the implicit binding mechanism succeeds not only in checking `runAny` and then additionally calling it but also binding its implicit argument and making the whole program work. And, finally: the way the "undetermined type params" mechanism works requires that the `MethodType` itself be `.isImplicit`, which `runAny` in #1 technically isn't: it takes the argument `input` and returns a more complex function. – Jay Freeman -saurik- May 31 '11 at 10:55
3

(Note: this is a summary of the discussion that took place in possibly more detail in the comments section of another answer on this question.)

It turns out that the problem here is that the implicit parameter is not first in runAny, but not because the implicit binding mechanism is ignoring it: instead, the issue is that the type parameter Output is not bound to anything, and needs to be indirectly inferred from the type of the run implicit parameter, which is happening "too late".

In essence, the code for "undetermined type parameters" (which is what Output is in this circumstance) only gets used in situations where the method in question is considered to be "implicit", which is determined by its direct parameter list: in this case, runAny's parameter list is actually just (input :Input), and isn't "implicit".

So, the type parameter for Input manages to work (getting set to Int::HNil), but Output is simply set to Nothing, which "sticks" and causes the type of the run argument to be Int::HNil=>Object=>Nothing, which is not satisfiable by runNil, causing runAny's type inferencing to fail, disqualifying it for usage as an implicit argument to runAll.

By reorganizing the parameters as done in my modified code sample #2, we make runAny itself be "implicit", allowing it to first get its type parameters fully determined before applying its remaining arguments: this happens because its implicit argument will first get bound to runNil (or runAny again for more than two levels), whose return type will get taken/bound.

To tie up the loose ends: the reason that the code sample #3 worked in this situation is that the Output parameter wasn't even required: the fact that it was bound to Nothing didn't affect any subsequent attempts to bind it to anything or use it for anything, and runNil was easily chosen to bind to its version of the run implicit parameter.

Finally, code sample #4 was actually degenerate, and shouldn't even have been considered to "work" (I had only verified that it compiled, not that it generated the appropriate output): the data types of its implicit parameters were so simplistic (Input=>Output, where Input and Output were actually intended to be the same type) that it would simply get bound to conforms:<:<[Input,Output]: a function that in turn acted as the identity in this circumstance.

(For more information on the #4 case, see this apparently dead-on related question: problem with implicit ambiguity between my method and conforms in Predef.)

Community
  • 1
  • 1
Jay Freeman -saurik-
  • 1,759
  • 1
  • 13
  • 13