How to interpret a final tagless DSL with ZIO?

291 Views Asked by At

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?

2

There are 2 best solutions below

0
Lando-L On BEST ANSWER

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))
2
simpadjo On

It not really clear from the question what you are trying to achieve but let me try to answer.

  1. ZIO's R parameter is not directly related to building DSLs. Once you build your DSL, R can potentially help you pass it to your computation ergonomically (but probably not).

  2. DSL is not a precise term but still it's unlikely that ZIO would help you building it. DSLs are usually based on plain introspectable datatypes (so-called initial encoding) or abstract datatypes with lots of Fs (final encoding). ZIO datatype is neither abstract nor introspectable.