scaladslmodule-patternzio

How to interpret a final tagless DSL with ZIO?


I have a final tagless DSL to build simple math expressions:

trait Entity[F[_]] {
  def empty: F[Int]
  def int(value: Int): F[Int]
}

trait Operation[F[_]] {
  def add(a: F[Int], b: F[Int]): F[Int]
}

I wanted to implement a ZIO interpreter. Based on the module-pattern guide, a possible implementation could look as follows:

type Entity = Has[Entity[UIO]]

object Entity {
  val test: ULayer[Entity] =
    ZLayer.succeed {
      new Entity[UIO] {
        override def empty: UIO[Int] =
          ZIO.succeed(0)

        override def int(value: Int): UIO[Int] =
          ZIO.succeed(value)
      }
    }

  def empty: URIO[Entity, Int] =
    ZIO.accessM(_.get.empty)

  def int(value: Int): URIO[Entity, Int] =
    ZIO.accessM(_.get.int(value))
}

type Operation = Has[Operation[UIO]]

object Operation {
  val test: ULayer[Operation] =
    ZLayer.succeed {
      new Operation[UIO] {
        override def add(a: UIO[Int], b: UIO[Int]): UIO[Int] =
          ZIO.tupled(a, b).map { case (x, y) => x + y }
      }
    }

  def add(a: UIO[Int], b: UIO[Int]): URIO[Operation, Int] =
    ZIO.accessM(_.get.add(a, b))
}

When building expressions with this implementation, one has to call provideLayer repeatedly like this:

Operation.subtract(
  Entity.empty.provideLayer(Entity.test),
  Entity.int(10).provideLayer(Entity.test)
).provideLayer(Operation.test)

That looks more like an anti-pattern. What would be the most idiomatic or the most ZIO way to interpret DSLs?


Solution

  • Getting back to this question with a better understanding of ZIO I have found a solution. It is a workaround and not in the spirit of ZIO, nevertheless, I thought it might be worth sharing.

    I updated the Operation implementation of ZIO:

    type Operation = Has[Service[URIO[Entity, *]]]
    
    object Operation {
      val live: ULayer[Operation] =
        ZLayer.succeed {
          new Service[URIO[Entity, *]] {
            override def add(a: URIO[Entity, Int])(b: URIO[Entity, Int]): URIO[Entity, Int] =
              a.zip(b).map { case (x, y) => x + y }
          }
        }
    }
    
    def add(a: URIO[Entity, Int])(b: URIO[Entity, Int]): URIO[Entity with Operation, Int] =
      ZIO.accessM(_.get[Service[URIO[Entity, *]]].add(a)(b))
    

    This way Entity and Operation can be combined like this:

    operation.add(entity.int(5))(entity.int(37))