2

I'm trying to build a coroutine framework to enable batch data fetching by stepping through each data-dependent function in parallel. Here is what I have so far: http://pastie.org/7147798

  1. This doesn't work

    def get(id: Long) = reset {
      // Is it not already cached?
      if (!cached.isDefinedAt(id)) {
        // Store the ID we want to fetch.
        queued += id
        // Come back later...
        shift { fetch[Object]() } : Seq[Any] @cps[ExecState[Object]]
      }
      // We should have the ID fetched now.
      Result(cached(id))
    }
    

    I get the following error

    ashoat@ashoatmbp [~/project]# scala -P:continuations:enable Loader.scala
    /Users/ashoat/project/Loader.scala:134: error: type mismatch;
     found   : Unit
     required: Any @util.continuations.package.cps[Main.$anon.Loader.ExecState[Main.$anon.Loader.Object]]
          if (!cached.isDefinedAt(id)) {
          ^
    one error found
    

    This works

    def get(id: Long) = reset {
      // Is it not already cached?
      if (!cached.isDefinedAt(id)) {
        // Store the ID we want to fetch.
        queued += id
        // Come back later...
        shift { fetch[Object]() } : Seq[Any] @cps[ExecState[Object]]
        // We should have the ID fetched now.
        Result(cached(id))
      } else {
        // We should have the ID fetched now.
        Result(cached(id))
      }
    }
    
  2. This doesn't work

    val getFive = reset {
      if (true) {
        Result(5)
      } else {
        val seq: Seq[Any] = shift { fetch[Int](Object.get(15181990251L)) }
        val Seq(obj: Object) = seq
        Result(obj.fields("test").toInt)
      }
    }
    

    I get the following error

    ashoat@ashoatmbp [~/project]# scala -P:continuations:enable Loader.scala
    /Users/ashoat/project/Loader.scala:170: error: cannot cps-transform expression new this.Loader.Result[Int](5): type arguments [this.Loader.Result[Int],this.Loader.Result[Int],Nothing] do not conform to method shiftUnit's type parameter bounds [A,B,C >: B]
        Result(5)// : Result[Int] @cps[Result[Int]]
              ^
    one error found
    

    This works

    val getFive = reset {
      if (true) {
        Result(5) : Result[Int] @cps[Result[Int]]
      } else {
        val seq: Seq[Any] = shift { fetch[Int](Object.get(15181990251L)) }
        val Seq(obj: Object) = seq
        Result(obj.fields("test").toInt)
      }
    }
    

    But I get the following warning

    ashoat@ashoatmbp [~/project]# scala -P:continuations:enable Loader.scala
    /Users/ashoat/project/Loader.scala:170: warning: expression (new this.Loader.Result[Int](5): this.Loader.Result[Int]) is cps-transformed unexpectedly
        Result(5) : Result[Int] @cps[Result[Int]]
                  ^
    one warning found
    8
    
hammar
  • 138,522
  • 17
  • 304
  • 385
Ashoat
  • 529
  • 1
  • 7
  • 20
  • Why do you expect "to have the ID fetched now"? The way I read your code, I see that `Result(...)` is after the *shift* block and is also the last expression inside your *reset*. So I would expect `shift` to take a `Result[Int]` and compute another expression with it, that would be the value returned by the whole *reset* block. – huynhjl Mar 28 '13 at 07:25
  • @Ashoat Please, it would greatly help if you try to explain what you want to achieve. What does the fetch function do? It looks like you use shift as a call to an external procedure instead of a context shift. Don't want to sound rude, but I must ask if you are confident with the use of shift/reset. – pagoda_5b Mar 28 '13 at 08:17
  • @pagoda_5b: I've edited the question to add some more details including a link to the full source I have so far. My use of shift just passes the current state of the delimited reset block along with some specified dependencies into a function that transforms them into a single continuation, which is then returned to the caller of the reset block. – Ashoat Mar 28 '13 at 09:17
  • @huynhjl: The ID will be "fetched now" because between each shift "step" in the function a data fetch is executed by the function's caller. – Ashoat Mar 28 '13 at 09:17
  • 1
    @Ashoat link to the source improves the question greatly. – huynhjl Mar 28 '13 at 14:22
  • @Ashoat no clear solution now, but wondering how do the `shift` combines with being inside an `if` block... I know that other *flow control* stuff like `for` and `while` doesn't play friends with *delimited conts*. Q: what happens when you capture the `cont`? does it run up to the `else` block? goes over it? mess it up? Could you refactor to put the `reset` block inside the `if` and would it work? Just brainstorming here... – pagoda_5b Mar 29 '13 at 09:17

1 Answers1

1

Although I still don't quite understand continuations myself, as best as I can tell, the key issue in your example is that your code does not always supply a shift to the reset.

The compiler expects to find some shift nested inside the reset. It will then CPS transform the shift into a ControlContext][A, B, C] and the code that happens after the shift into a ControlContext.map call.

Because you have an if statement, in the case where the else branch is taken, there is no nested shift:

reset {
  if (false) {
    shift { ... }
  } 
  Result(cached(id)) // no shift
}

Same with

reset {
  if (false) {
    shift { ... }
  } else {
    Result(cached(id)) // no shift
  }
}

That cannot be transformed into valid CPS code.

It seems you could have the reset inside the if branch or supply a trivial shift statement to the else branch:

if (!cached.isDefinedAt(id)) reset {
   shift { ... }
   Result(cached(id))
} else {
   Result(cached(id))
}

// or

reset {
  if (!cached.isDefinedAt(id)) {
    shift { ... }
    Result(cached(id))
  } else {
    shift[Result[Object], ExecState[Object], ExecState[Object]] { k => 
      Result(cached(id))
    }
  }
}    

Edit: It does seems there is some inconsistencies on how the cps plugin infers the types. For example:

var b = false
def test[A](a: A) = reset {
  if (b) {
    a
  } else {
    shift{ (k: Unit => A) => k() }
    a
  }
}

Running compilation with the -Xprint:selectivecps options shows that the compiler infers the type as Reset[A, Nothing] then running the code will produce an error at runtime. If the if is reversed as:

var b = false
def test[A](a: A) = reset {
  if (b) {
    shift{ (k: Unit => A) => k() }
    a
  } else {
    a
  }
}

Then the compiler correctly infers reset[A, A]. If I provide the type parameters to reset like test[A](a: A) = reset[A, A] { then it works in both cases.

Maybe specifying the type parameters to reset and shift and also instead of using Result(5), using the shiftUnit[A, B, C] method will help with reducing inconsistencies.

huynhjl
  • 41,520
  • 14
  • 105
  • 158
  • Thanks for the advice. It does seem like these issues only occur when there is a return path in the `reset` that doesn't pass through a `shift` block. However, this still doesn't explain why I'm getting the behavior that I am: computationally equivalent statements are compiling when written in one way and erroring when written in others. My suspicion is this this has to do with how type inference works with the continuations plugin, but I would like to understand exactly what's going wrong. – Ashoat Mar 29 '13 at 22:03
  • I think the compiler cps plugin fails to report code that is incorrect. When you says "it works", do you mean the code actually perform what it's supposed to do? Or it just compiles? My hunch is that it compiles but you will get a runtime error or incorrect behavior. – huynhjl Mar 30 '13 at 01:51
  • The code performs what it should... unless I reverse the if-else blocks, in which case it gives a runtime error. Very inconsistent behavior. – Ashoat Mar 30 '13 at 04:27