Exploring Kyo - idiomatic usage

I’m mucking around with Kyo with a view to perhaps cutting over some code using Cats into Kyo, just exploring the landscape right now.

One thing I intended to do is cut over the uses of Cats’ traverse, these usually being used to flip around a collection of monad instances of type Workflow[X] to being a Workflow[Vector[X]] or Workflow[Seq[X]] or whatever.

A workflow is an EitherT stacked on a WriterT stacked on an IO. Quite the fancy layer cake.

Once the traversal is done I’m happy forgetting that collections are monadic in their own right, so there is no need to integrate them into an even more complex monad stack.

So, after belatedly realising that I need Scala 3.8.1 for the RC1 build of Kryo :cat_with_wry_smile: (I’m sticking with Scala 3.3.* for as long as possible, not sure about libraries using non-LTS builds, but understand that this is a release candidate after all), I cook up:

import kyo.*

object Application extends KyoApp:
  run {
    val monad: String < (Sync & Choice & Abort[Absent]) = for
      x <- Console.printLine("Starting...")
      y <- Choice.evalSeq(1 to 10)
      z <- Abort.get(Maybe.when(0 == y % 2)(s"Even: $y").toResult)
    yield z

    // Failure is outermost....
    val oneWay = monad.handle(Choice.run, Abort.run).map(Console.printLine)

    // Failures are nested in the choices...
    val anotherWay = monad.handle(Abort.run(_), Choice.run).map(Console.printLine)

    oneWay `andThen` anotherWay
  }
end Application

This produces the output:

Starting...
Failure(failure = Absent)
Starting...
Seq(
  Failure(failure = Absent),
  "Even: 2",
  Failure(failure = Absent),
  "Even: 4",
  Failure(failure = Absent),
  "Even: 6",
  Failure(failure = Absent),
  "Even: 8",
  Failure(failure = Absent),
  "Even: 10"
)

That makes sense, and I note the way permuting the effect handlers in the < monad achieves a similar effect to doing a traverse or sequence for nested monads in Cats.

(I’m not sure if this is an expected idiom to mix-and-match handler orders prior to evaluation, but my assumption is that in the wild, distinct areas of code might want their own handler orders, but would end up feeding into some giant final application monad value to be evaluated.)

Anyway, some questions:

I’m using the Choice effect to represent layering a collection over some abortable computation, rather than packing the abortable computations into a collection, which is what the Cats-style code does.

Now I know that the direct-style support in Kyo allows lifting of effects through ordinary Scala collections via dotty-cps-async, but for now I don’t want to cut straight over to direct-style. Is there some equivalent of traverse that would allow a collection of X < Abort[Absent] & Sync things to be flipped inside out to Vector[X] < Abort[Absent] & Sync or whatever?

Or is the use of Choice (or for that matter Stream) the idiomatic way of doing this in Kyo?

I also was caught out in an earlier version of the code above where the third bind was written as:

      <- Maybe.when(0 == y % 2)(s"Even: $y") // Not converted to a `Result` to work with `Abort`

That compiles, but has a surprising outcome - the Maybe aspect is not incorporated into the < monad as an effect, rather as the type of the underlying values. It just happens to compile because of the magic lifting of plain values into a < that Kyo does.

Once again, is the preferred idiom to stick with Maybe as being in a separate world from <, so there is no effect-style interpretation of Maybe, other than using either Abort or Choice to simulate it?

1 Like

Figured something out - use Kyo.foreach, that is more or less the same as traverse insofar as a collection is treated as a monad:

import kyo.*

object Application extends KyoApp:
  run {
    val monad: String < (Sync & Choice & Abort[Absent]) = for
      x <- Console.printLine("Starting...")
      y <- Choice.evalSeq(1 to 10)
      z <- Abort.get(Maybe.when(0 == y % 2)(s"Even: $y").toResult)
    yield z

    // Failure is outermost....
    val oneWay = monad.handle(Choice.run, Abort.run).map(Console.printLine)

    // Failures are nested in the choices...
    val anotherWay =
      monad.handle(Abort.run(_), Choice.run).map(Console.printLine)

    val justPlainOldSequenceThankYou: Seq[String] < (Sync & Abort[Absent]) = for
      x <- Console.printLine("Starting...")
      z <- Kyo.foreach(1 to 10) { y =>
        Kyo.fromMaybe(Maybe.when(0 == y % 2)(s"Even: $y"))
      }
    yield z

    oneWay `andThen` anotherWay `andThen` justPlainOldSequenceThankYou.handle(
      Abort.run
    )
  }
end Application

Also discovered the combinators library, so it’s slightly less messy getting Maybe values into the < monad.

Still interested to hear others’ opinions, though…

1 Like