Skip to content

Instantly share code, notes, and snippets.

@mucaho
Last active May 1, 2025 08:28
Show Gist options
  • Save mucaho/d80551dd0b62c59ce0e2186608482577 to your computer and use it in GitHub Desktop.
Save mucaho/d80551dd0b62c59ce0e2186608482577 to your computer and use it in GitHub Desktop.
YAES: Thoughts on context-based capability passing style for state threading and integration into tagless-final application

First creation date: 2025-04-20
Latest update date: 2025-04-24

Changelog

  • 2025-04-24: Updated solution for state-threading

Table of Contents

Introduction

If you have been following recent developments in the Scala ecosystem, you might have heard about yaes (Yet Another Effect System): https://github.com/rcardin/yaes.
First of all, big thanks to its author Riccardo Cardin and others who have helped out here!

It leverages implicits (context parameters) to thread effects seamlessly throughout the call stack. You can have e.g. an effect for logging some data, without having to pass this effect around directly.

import in.rcard.yaes.Output.*

def program(name: String)(using Output): String = {
  val result = s"Hello, $name!"
  Output.printLn(result)
  result
}

val program: Output ?=> Unit = program("Jane") // This does not print anything actually!

Output.run { program } // This does: Hello, Jane!

However, any call to such an effect, e.g. to log some data on the logger effect, is not actually executed immediately. The execution of such code is deferred, until an effect handler is provided that takes care of running the effectful computation.

Thus, it results in code that is written in direct style without the need for for-comprehensions, while also offering staged execution of effectful program descriptions. Sounds great, but what's the catch?

State threading

If you look at the currently available effects https://github.com/rcardin/yaes?tab=readme-ov-file#effects-or-capabilities, and also at the plans for future effects https://github.com/rcardin/yaes/issues, you will notice the lack of a State effect. A State effect allows you to read values from the context, and more importantly, also write values to for future consumption by downstream code.

A very straightforward implementation for a plain (non-monadic) State effect would be a case class that contains a mutable current state value, and some additional helper code under the hood. Then, such capability could be passed around using implicits like the rest of these effects:

case class MutState[S](var value: S) {
  def run[A](block: MutState[S] ?=> A) = block(using this)
}
object MutState {
  def apply[S](using MutState[S]): MutState[S] = implicitly
}

def program(name: String)(using MutState[Int], Output): String = {
  MutState[Int].value += 1
  val time = MutState[Int].value
  val result = s"Hello, $name, for the ${time}th time!"
  Output.printLn(result)
  result
}

val program: MutState[Int] ?=> Output ?=> Unit = program("Jane")

MutState(0).run { Output.run { program } } // prints "Hello, Jane, for the 1th time!"

However, we lose referential transparency when updating an existing MutState instance:

val initalState = MutState(0)
initalState.run { Output.run { program } } // prints "Hello, Jane, for the 1th time!"
initalState.run { Output.run { program } } // prints "Hello, Jane, for the 2th time!"

Let's convert it to an immutable state then, and create fresh instances when we need to update it, instead:

case class MutState[S](val value: S) {  
  def modify[SB](f: S => SB): MutState[SB] = copy(value = f(value))
  
  def update[SB](newValue: SB): MutState[SB] = modify(_ => newValue)
}
object MutState {
  def apply[S](using MutState[S]): MutState[S] = implicitly

  def run[S, A](initial: S)(block: MutState[S] ?=> A) = block(using MutState(initial))
}

def program(name: String)(using MutState[Int], Output): String = {
  given MutState[Int] = MutState[Int].modify(_ + 1)
  val time = MutState[Int].value
  val result = s"Hello, $name, for the ${time}th time!"
  Output.printLn(result)
  result
}

MutState.run(0) { Output.run { program("Jane") } }

Great! However, if you try to update the state again and thus try to redeclare given MutState[Int] once more, the compiler will complain about Ambiguous given instance. There's a limitation in place that you can have only one given instance per scope.

We can work around this by assigning each MutState instance to its own variable:

def program3(name: String)(using m0: MutState[Int], O: Output): String = {
  val m1 = m0.modify(_ + 1)
  val result = s"Hello, $name, for the ${m1.value}th time!"
  Output.printLn(result)

  val m2 = MutState[Int](using m1).modify(_ - 1)
  result
}

This is quite error-prone, though. We can easily reference the wrong variable while updating state, or forget about passing the freshest state variable as the proper implicit parameter to other functions explicitly.

By sacrificing a bit of referential transparency, which affects the current scope only and does not spill over to the rest of the call stack (!), this can be cleaned up significantly though:

def program(name: String)(using M0: MutState[Int], O: Output): String = {
  implicit var M: MutState[Int] = M0 // interestingly, we cannot use "given" syntax here for var
  
  M = M.modify(_ + 1)
  Output.printLn(s"Greeted for the ${M.value}th time")

  M = M.modify(_ - 1)
  Output.printLn(s"Processed index: ${M.value}")

  s"Hello, $name!"
}

MutState.run(0) { Output.run { program("Jane") } }
// prints "Greeted for the 1th time"
// prints "Processed index: 0"

We still might forget to re-assign the new state to the existing variable. Let's try to encapsulate this, to reduce the surface for human error:

case class MutStateVar[S](var state: MutState[S]) {
  def value: S = state.value
  def value_=(newValue: S): Unit = state = state.update(newValue)
}
object MutStateVar {
  def apply[S](using MutState[S]): MutStateVar[S] = new MutStateVar[S](implicitly[MutState[S]])
}

def program(name: String)(using M0: MutState[Int], O: Output): String = {
  val MutState = MutStateVar[Int](using M0)
  implicit def mutState: MutState[Int] = MutState.state 
  
  MutState.value += 1
  Output.printLn(s"Greeted for the ${MutState.value}th time")

  val implicitMutState = implicitly[MutState[Int]]
  val result = s"Hello, $name, greeting you for the ${implicitMutState.value}th time!"

  MutState.value -= 1
  Output.printLn(s"Processed index: ${MutState.value}")

  result
}

MutState.run(0) { Output.run { program("Jane") } } // results in "Hello, Jane, greeting you for the 1th time!"
// prints "Greeted for the 1th time"
// prints "Processed index: 0"

The state update looks much cleaner and safer now, and the implicit resolution works as expected, without having to pass the correct implicit instance manually! We still need these two lines of "preamble" code at the start of the program method though, which could maybe somehow be made even more concise.

Unfortunately, there's another issue with this approach we haven't examined yet - how do we return the updated MutState to the caller? We would like to know the final value for MutState at the top of the call stack, thus we would like MutState.run to return a tuple (finalStateValue: Int, programResult: String).

If we make program return its final MutState as part of its return value def program(name: String)(using M0: MutState[Int], O: Output): (MutState[Int, String) we lose all the ergonomics and are missing the point of using effects seamlessly from the function's implicit environment.

Just like we can have implicit (context) arguments for a function, the idea crossed my mind to have implicit (context) return values for functions. Not sure whether that would be feasible or still result in a sound type system in Scala, but, anyways, that's not an option right now.

Update:
Previously, I thought the only way to deal with this was to thread a single MutStateVar all the way from the beginning to the end of the call stack where the MutState effect is needed. This would have resulted in losing referential transparency across the whole call stack.

However, with a new approach we can achieve partial referential transparency though, after all. Namely, we have to adapt the implicit resolution via MutState.apply to return the most up-to-date MutState at all times. The MutState instances themselves can remain immutable, though. By having the MutState.run handler keep track of the most up-to-date state S, the whole mechanism works even across different call stacks!

trait MutState[S] {
  def value: S
  def value_=(s: S): Unit
  
  private[MutState] def implicitValue: S
}

object MutState {
  def run[S, A](initialValue: S)(block: MutState[S] ?=> A): (S, A) = {
    var internalValue: S = initialValue

    val mutState = new MutState[S] {
      val value = initialValue
      def value_=(s: S) = internalValue = s
      def implicitValue = internalValue
    }
    
    given MutState[S] = mutState
    val a = block
    val s = internalValue
    (s, a)
  }

  def apply[S](using M: MutState[S]): MutState[S] = new MutState[S] {
    val value = M.implicitValue
    def value_=(s: S) = M.value = s
    def implicitValue = M.implicitValue
  }
}

def program(name: String)(using MutState[(Int, Int)]): String = {
  var result = s"Processing $name:\n"
  val initialState = MutState[(Int, Int)]
  
  result += getGreeting(name)
  result += getFarewell(name)
  result += getGreeting(name)
  result += getFarewell(name)
  
  val finalState = MutState[(Int, Int)]
  val diff = (finalState.value._1 + finalState.value._2) - (initialState.value._1 + initialState.value._2)
  result += s"Processed statements for $name: $diff\n"

  result
}

def getGreeting(name: String)(using MutState[(Int, Int)]): String = {
  val (greetings, farewells) = MutState[(Int, Int)].value
  MutState[(Int, Int)].value = (greetings + 1, farewells)
  s"Greetings, $name!\n"
}

def getFarewell(name: String)(using MutState[(Int, Int)]): String = {
  val (greetings, farewells) = MutState[(Int, Int)].value
  MutState[(Int, Int)].value = (greetings, farewells + 1)
  s"Farewell, $name!\n"
}

MutState.run((0,0)) { program("Jane") }
/* returns ( (2,2), "
  Processing Jane:
  Greetings, Jane!
  Farewell, Jane!
  Greetings, Jane!
  Farewell, Jane!
  Processed statements for Jane: 4
") */

Integrating with existing tagless final codebase

As the library's name clearly states, it's yet another effect system. Even if this effect system's approach was the ultimate solution to all our problems and wishes for safe yet lean FP programming in Scala, no application already in production will be migrating its whole codebase to this new approach. You can have a state-of-the-art solution for a problem, if it's hard to apply and integrate in practice it won't be used unfortunately.

Supermonad effect systems may tell you otherwise, but tagless final is a powerful abstraction that allows you to be more flexible in what actual effect system implementation you use under the hood, at least in theory. In practice, compared to Typelevel (cats & co), Monix, Izumi (BIO) and tofu-tf, I have yet to see ZIO and kyo ecosystem offer typeclass instances for common effect types, if that even is feasible and makes sense on a technical level.

Prior to putting the cart before the horse though, what is Tagless Final (TF) and how do you structure your application with it? I'd like to thank and refer to existing resources on the Web here, that explain that better than I ever could, for example:

How does an example TF application look like then? Let's use the following contrived example, which uses a couple of TF typeclass abstractions:

//> using scala 3.3.5
//> using dep org.typelevel::cats-mtl:1.5.0
//> using dep org.typelevel::cats-effect:3.6.1
//> using options -deprecation -encoding UTF-8 -feature -unchecked

import cats.Monad
import cats.mtl.Stateful
import cats.mtl.Raise
import cats.effect.Unique

type Cache = Map[String, String]

case class Error(message: String)

def statement[F[_]](id: String)(using M: Monad[F], U: Unique[F], S: Stateful[F, Cache]): F[String] = {
  import cats.syntax.all._
  
  for {
    cache  <- S.get
    result <- cache.get(id) match {
                case Some(result) => M.pure(result)
                case None => U.unique.map(t => t.hash.toString())
              }
    _      <- S.set(cache.updated(id, result))
  } yield result
}

def program[F[_]](using Monad[F], Unique[F], Raise[F, Error], Stateful[F, Cache]): F[String] = {
  import cats.syntax.all._
  import cats.mtl.syntax.all._

  for {
    res1 <- statement("123")
    res2 <- statement("456")
    _    <- if (res2.toInt % 2 != 0) then
              Error("Cannot use odd res2 for some reason!").raise
            else
              Error("").pure
    res3 <- statement("123")
    res   = s"[$res1, $res2, $res3]"
  } yield res
}

To make the integration between TF and YAES work and to have YAES thus drive the execution of this code, we need to provide typeclass instances for these various effects that are used:

  • cats.effect.Unique
  • cats.mtl.Raise
  • cats.mtl.Stateful

Plus we need a cats.Monad instance for our effect container F[_], so that we can construct a monadic for-comprehension, as seen above.

What could be our effect container F[_]?

  • we need to be able to access our capability implementations from it
  • we need the effect container to be a monad
  • we need to be able to make it a type constructor of kind * -> *

The cats.data.Reader monad perfectly fits the above requirements, so let's use that!

Thus, we can "substitute" F[_] with [A] =>> Reader[EF, A]. A is the type of the effectful computation, the result of our above program (in our case String), wrapped in our effect container Reader. EF is the type of some read-only value we can get from the environment, from our effect container Reader. In our case, ef: EF needs to have some methods like ef.unique, ef.raise and ef.stateful defined.

With a general plan laid out, let's start by providing the typeclass instance for (arguably) the most straightforward effect:

trait Unique[F[_]] {
  def unique: F[Token]
}

We need to create a new YAES capability that will match the cats effect for generating an unique token, since YAES capabilities perform their effect directly, "outside" the effect container. YAES capabilities are non-monadic, their computation does not depend on previous operations from the effect container, nor do they alter the behavior of the effect container for follow-up computations. To avoid name-clashes with the existing cats trait Unique, we'll call our YAES trait TokenGen.

Putting above points into action, creating a effect typeclass instance for Unique by delegating the work to a TokenGen capability, which is part of environment context EF via HasTokenGen:

trait TokenGen {
  def generate: Token
}

trait HasTokenGen {
 def tokenGen: TokenGen
}

implicit def contextUnique[EF <: HasTokenGen]: Unique[[A] =>> Reader[EF, A]] =
  new Unique[[A] =>> Reader[EF, A]] {
  
    def unique: Reader[EF, Token] =
      Ask.ask[[A] =>> Reader[EF, A], EF].map(_.tokenGen.generate)

    def applicative: Applicative[[A] =>> Reader[EF, A]] = implicitly
  }

Let's construct a simple program, substituting F[_] with our Reader monad. This is just a description of a program, it's not run until we give it the required capabilities:

def tokenGenTest[F[_]](using M: Monad[F], U: Unique[F]): F[String] = {
  import cats.syntax.all._
  
  U.unique.map(t => t.hash.toString())
}

val monad /*: Reader[HasTokenGen, String]*/ = tokenGenTest[[A] =>> Reader[HasTokenGen, A]]

We now need to give it the implementation of the capability TokenGen, wrapped in our environment context HasTokenGen, in order to run this program description:

val hasTokenGen = new HasTokenGen {
  def tokenGen = new TokenGen {
    def generate = new Token
  }
}

val result: String = program.run(hasTokenGen)

result // returns some random hash, e.g. "974592379"

However, this is not entirely in the spirit of YAES, and its idiomatic way to run capabilities by providing context parameters. Let's try to rewrite this in YAES's spirit then:

object TokenGen {
  def run[A](block: TokenGen ?=> A): A = {
    given TokenGen = new TokenGen {
      def generate = new Token
    }
    block
  }
}

val result: String = TokenGen.run {
  val hasTokenGen = new HasTokenGen {
    def tokenGen = implicitly
  }
  program.run(hasTokenGen)
}

result // returns some random hash, e.g. "1049174687"

We have successfully crossed the bridge between YAES-like capabilities and TF-style applications, at least for this simple program, yay!!

Let's attempt a similar approach for the capability MutState[S] that will be used as the counterpart for the effect Stateful[F[_], S]:

  • Instead of Reader[EF, A] we will be using State[SF, A] as our effect container for F[_]
    • sf: SF stays the same as ef: EF before, bar the name change
  • This allows us to make MutState immutable, and update it using the State monad instead
  • We need this in order to satisfy the laws for the Stateful effect
  • We'll use a local variable inside MutState.run, in order to keep track of the final MutState[S].value
    • refer to the end of the [#state-threading](state-threading section) for more details on this approach
    • we are adding an additional private method MutState.withValue, to make the creation of a new MutState atomic
trait MutState[S] {
  def value: S
  def value_=(s: S): Unit
  
  private[MutState] def implicitValue: S
  private[MutState] def withValue(s: S): MutState[S]
}

object MutState {
  def run[S, A](initialValue: S)(block: MutState[S] ?=> A): (S, A) = {
    var internalValue: S = initialValue

    case class MutStateImpl(value: S) extends MutState[S] {
      def value_=(s: S) = internalValue = s
      def implicitValue = internalValue
      def withValue(s: S) = {
        value = s
        copy(value = s)
      }
    }
    
    given MutState[S] = MutStateImpl(initialValue)
    val a = block
    val s = internalValue
    (s, a)
  }

  def apply[S](using M: MutState[S]): MutState[S] = new MutState[S] {
    val value = M.implicitValue
    def value_=(s: S) = M.value = s
    def implicitValue = M.implicitValue
    def withValue(s: S) = M.withValue(s)
  }

  def updated[S](s: S)(using M: MutState[S]) = M.withValue(s)
}

trait HasMutState[SF, S] {
 def state: MutState[S]
 def withState(newState: MutState[S]): SF
}

implicit def contextStateful[S, SF <: HasMutState[SF, S]]: Stateful[[A] =>> State[SF, A], S] =
  new Stateful[[A] =>> State[SF, A], S] {

    def get: State[SF, S] =
      Stateful.get[[A] =>> State[SF, A], SF].map(_.state.value)

    def set(s: S): State[SF, Unit] =
      Stateful.modify[[A] =>> State[SF, A], SF](sf => sf.withState(
        MutState.updated(s)(using sf.state)
      ))

    def monad: cats.Monad[[A] =>> State[SF, A]] = implicitly
}

Testing the new MutState capability:

def mutStateTest[F[_]](using M: Monad[F], S: Stateful[F, Int]): F[String] = {
  import cats.syntax.all._
  
  S.set(1).map(_ => "Hello!")
}

class SF(using val state: MutState[Int]) extends HasMutState[SF, Int] {
  def withState(newState: MutState[Int]): SF = new SF(using newState)
}

val program = mutStateTest[[A] =>> State[SF, A]]

val result = MutState.run(0) {
  program.runA(SF()).value
}

result // returns (1, "Hello")

After migrating TokenGen to the State monad, and following this pattern going forward, we can create similar bridges between other capabilities and effects. You can find the whole runnable code here in the other file of this gist.
Scastie: https://scastie.scala-lang.org/uDlRcQOSS2SZOa06RrnGyg

One more interesting thing is how you can build and run the program from the start of this chapter, after putting everything together:

TokenGen.run {
  RaiseError.run[Error] {
    MutState.run(Map.empty) {
    
      class SF(using val state: MutState[Cache], val raiseError: RaiseError[Error], val tokenGen: TokenGen)
        extends HasMutState[SF, Cache] with HasTokenGen with HasRaiseError[Error] {
          def withState(newState: MutState[Cache]): SF = new SF(using newState, implicitly, implicitly)
        }
  
      val p = program[[A] =>> State[SF, A]]

      val result = p.runA(SF()).value
      
      result
    }
  }
}
// sometimes prints e.g. "(Map(123 -> 833957828, 456 -> 559994164),[833957828, 559994164, 833957828])"
// sometimes prints "Error(Cannot use odd res2 for some reason!)" instead

Closing thoughts

I hope my thoughts and examples here have given at least some food for thought on using and integrating YAES into existing applications. It seems like a lightweight alternative to monadic effect system, and a step towards more direct-style effect handling.

One of the advantages here of using YAES instead of cats for the program above is that we have gotten rid of the whole monad transfomer stack that would have been used for the effect container F[_]! We are using a simple State monad for F[_] instead. There have been reports of performance degradation due to deeply nested monad stacks. Furthermore, the compiler's type inference is getting more tricky, and may need additional type hints the more complex the stack gets.

Moreover, Monads to not compose in all scenarios, you cannot make a monad transformer for all combinations of all kinds of monads. However, I'm also quite not sure whether YAES capabilities can compose properly either, in all scenarios. This would be an awesome boon if true.

Furthermore, as we have seen above, some compromises have to be made in terms of referential transparency when trying to thread state context throughout the call stack.

Do note that I may have missed a critical aspect for state threading or while integrating YAES into TF-style code. I would greatly appreciate any further input and discussion on this topic!

//> using scala 3.3.5
//> using dep org.typelevel::cats-mtl:1.5.0
//> using dep org.typelevel::cats-effect:3.6.1
//> using options -deprecation -encoding UTF-8 -feature -unchecked
import scala.util.boundary
import scala.util.boundary.break
import scala.util.boundary.Label
import cats.Applicative
import cats.Functor
import cats.Monad
import cats.data.State
import cats.data.Reader
import cats.mtl.Stateful
import cats.mtl.Ask
import cats.mtl.Raise
import cats.effect.Unique
import cats.effect.kernel.Unique.Token
trait MutState[S] {
def value: S
def value_=(s: S): Unit
private[MutState] def implicitValue: S
private[MutState] def withValue(s: S): MutState[S]
}
object MutState {
def run[S, A](initialValue: S)(block: MutState[S] ?=> A): (S, A) = {
var internalValue: S = initialValue
case class MutStateImpl(value: S) extends MutState[S] {
def value_=(s: S) = internalValue = s
def implicitValue = internalValue
def withValue(s: S) = {
value = s
copy(value = s)
}
}
given MutState[S] = MutStateImpl(initialValue)
val a = block
val s = internalValue
(s, a)
}
def apply[S](using M: MutState[S]): MutState[S] = new MutState[S] {
val value = M.implicitValue
def value_=(s: S) = M.value = s
def implicitValue = M.implicitValue
def withValue(s: S) = M.withValue(s)
}
def updated[S](s: S)(using M: MutState[S]) = M.withValue(s)
}
trait HasMutState[SF, S] {
def state: MutState[S]
def withState(newState: MutState[S]): SF
}
implicit def contextStateful[S, SF <: HasMutState[SF, S]]: Stateful[[A] =>> State[SF, A], S] =
new Stateful[[A] =>> State[SF, A], S] {
def get: State[SF, S] =
Stateful.get[[A] =>> State[SF, A], SF].map(_.state.value)
def set(s: S): State[SF, Unit] =
Stateful.modify[[A] =>> State[SF, A], SF](sf => sf.withState(
MutState.updated(s)(using sf.state)
))
def monad: cats.Monad[[A] =>> State[SF, A]] = implicitly
}
trait TokenGen {
def generate: Token
}
object TokenGen {
def run[A](block: TokenGen ?=> A): A = {
given TokenGen = new TokenGen {
def generate = new Token
}
block
}
}
trait HasTokenGen {
def tokenGen: TokenGen
}
implicit def contextUnique[SF <: HasTokenGen]: Unique[[A] =>> State[SF, A]] =
new Unique[[A] =>> State[SF, A]] {
def unique: State[SF, Token] =
Stateful.get[[A] =>> State[SF, A], SF].map(_.tokenGen.generate)
def applicative: Applicative[[A] =>> State[SF, A]] = implicitly
}
implicit def contextUnique2[EF <: HasTokenGen]: Unique[[A] =>> Reader[EF, A]] =
new Unique[[A] =>> Reader[EF, A]] {
def unique: Reader[EF, Token] =
Ask.ask[[A] =>> Reader[EF, A], EF].map(_.tokenGen.generate)
def applicative: Applicative[[A] =>> Reader[EF, A]] = implicitly
}
trait RaiseError[E] {
def raise[E2 <: E](e: E2): Nothing
}
object RaiseError {
final class RunHelper[E] {
def apply[A](block: RaiseError[E] ?=> A): E | A = {
boundary[E | A] {
given RaiseError[E] = new RaiseError[E] {
def raise[E2 <: E](e: E2): Nothing = {
break(e)
}
}
block
}
}
}
def run[E] = new RunHelper[E]
}
trait HasRaiseError[E] {
def raiseError: RaiseError[E]
}
implicit def contextRaise[E, SF <: HasRaiseError[E]]: Raise[[A] =>> State[SF, A], E] =
new Raise[[A] =>> State[SF, A], E] {
def raise[E2 <: E, Nothing](e: E2) =
Stateful.get[[A] =>> State[SF, A], SF].map(_.raiseError.raise(e))
def functor: Functor[[A] =>> State[SF, A]] = implicitly
}
def program0(name: String)(using MutState[(Int, Int)]): String = {
var result = s"Processing $name:\n"
val initialState = MutState[(Int, Int)]
result += getGreeting(name)
result += getFarewell(name)
result += getGreeting(name)
result += getFarewell(name)
val finalState = MutState[(Int, Int)]
val diff = (finalState.value._1 + finalState.value._2) - (initialState.value._1 + initialState.value._2)
result += s"Processed statements for $name: $diff\n"
result
}
def getGreeting(name: String)(using MutState[(Int, Int)]): String = {
val (greetings, farewells) = MutState[(Int, Int)].value
MutState[(Int, Int)].value = (greetings + 1, farewells)
s"Greetings, $name!\n"
}
def getFarewell(name: String)(using MutState[(Int, Int)]): String = {
val (greetings, farewells) = MutState[(Int, Int)].value
MutState[(Int, Int)].value = (greetings, farewells + 1)
s"Farewell, $name!\n"
}
MutState.run((0,0)) { program0("Jane") }
/* returns ( (2,2), "
Processing Jane:
Greetings, Jane!
Farewell, Jane!
Greetings, Jane!
Farewell, Jane!
Processed statements for Jane: 4
") */
def rt[F[_]](using M: Monad[F], U: Unique[F]): F[Boolean] = {
import cats.syntax.all._
for {
_ <- U.unique
ref = U.unique
token1 <- ref
token2 <- ref
} yield token1 == token2
}
def tokenGenTest[F[_]](using M: Monad[F], U: Unique[F]): F[String] = {
import cats.syntax.all._
U.unique.map(t => t.hash.toString())
}
val program1: Reader[HasTokenGen, String] = tokenGenTest[[A] =>> Reader[HasTokenGen, A]]
val hasTokenGen = new HasTokenGen {
def tokenGen = new TokenGen {
def generate = new Token
}
}
val result1: String = program1.run(hasTokenGen)
result1
val result2: String = TokenGen.run {
val hasTokenGen = new HasTokenGen {
def tokenGen = implicitly
}
program1.run(hasTokenGen)
}
result2
def mutStateTest[F[_]](using M: Monad[F], S: Stateful[F, Int]): F[String] = {
import cats.syntax.all._
S.set(1).map(_ => "Hello!")
}
class SF3(using val state: MutState[Int]) extends HasMutState[SF3, Int] {
def withState(newState: MutState[Int]): SF3 = new SF3(using newState)
}
val program3 = mutStateTest[[A] =>> State[SF3, A]]
val result3 = MutState.run(0) {
program3.runA(SF3()).value
}
result3 // returns (1, "Hello!")
type Cache = Map[String, String]
case class Error(message: String)
def statement[F[_]](id: String)(using M: Monad[F], U: Unique[F], S: Stateful[F, Cache]): F[String] = {
import cats.syntax.all._
for {
cache <- S.get
result <- cache.get(id) match {
case Some(result) => M.pure(result)
case None => U.unique.map(t => t.hash.toString())
}
_ <- S.set(cache.updated(id, result))
} yield result
}
def program[F[_]](using Monad[F], Unique[F], Raise[F, Error], Stateful[F, Cache]): F[String] = {
import cats.syntax.all._
import cats.mtl.syntax.all._
for {
res1 <- statement("123")
res2 <- statement("456")
_ <- if (res2.toInt % 2 != 0) then
Error("Cannot use odd res2 for some reason!").raise
else
Error("").pure
res3 <- statement("123")
res = s"[$res1, $res2, $res3]"
} yield res
}
val result = TokenGen.run {
RaiseError.run[Error] {
MutState.run(Map.empty) {
class SF(using val state: MutState[Cache], val raiseError: RaiseError[Error], val tokenGen: TokenGen)
extends HasMutState[SF, Cache] with HasTokenGen with HasRaiseError[Error] {
def withState(newState: MutState[Cache]): SF = new SF(using newState, implicitly, implicitly)
}
val p = program[[A] =>> State[SF, A]]
val result = p.runA(SF()).value
result
}
}
}
println(result)
@rcardin
Copy link

rcardin commented Apr 23, 2025

Sorry, @mucaho. Discussions on the yaes project should be opened now 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment