Search code examples
scalafunctional-programmingscala-catsfree-monad

Is it possible to compose multiple interpreters for my Free Monad scala app


So i started to learn free monadic approach in my code style recently, and i hope i get the main idea of it. We have our own dsl to build a program, then we pass a compiler to a real world effect such as cats.IO and it looks good in simple programs such as calculator or smth but when i tried to build a real world app, such as http server with multiple domains (Postgress adt: get, post, delete, update. Config adt: read. HttpServer adt: start) and many more, i can't understand, how to compose a program with multiple dsls.

cats off doc says to try (type CatsApp[A] = EitherK[DataOp, Interact, A]) which is kind of a thing i've been looking for, but so far it only says how to compose 2 compilers into 1.

i either need a solution to compose any amount compilers or i have to build a huge ass adt which can do anything

object ConfigADT {
  type Config[A] = Free[ConfigA, A]
  sealed trait ConfigA[A]
  case class Read[T]() extends ConfigA[T]
  def read[A]: Config[A] = liftF[ConfigA, A](Read[A]())
}

object ServerADT {
  type Server[A] = Free[ServerA, A]
  sealed trait ServerA[A]
  case class Start[T, C](config: C) extends ServerA[T]
  def start[A, C](config: C): Server[A] = liftF[ServerA, A](Start[A, C](config))
}

object VictoriaMetricsADT {
  type VictoriaMetrics[T] = Free[VictoriaMetricsA, T]
  sealed trait VictoriaMetricsA[T]
  case class Query[T, C](query: String)(config: C) extends VictoriaMetricsA[T]
  case class Put[T, C](value: T)(config: C) extends VictoriaMetricsA[Unit]
  def query[T, C](query: String)(config: C): VictoriaMetrics[T] = liftF[VictoriaMetricsA, T](Query[T, C](query)(config))
  def put[T, C](value: T)(config: C): VictoriaMetrics[Unit] = liftF[VictoriaMetricsA, Unit](Put[T, C](value)(config))
}

i want to make a program which will read a config then start a server and server will use victoria dsl

for {
 config <- ConfigADT.read[ConfigData]
 dataFromVictoria <- VictoriaADT.query("my query")(config)
 server <- ServerADT.start(config)
} yield ()

i understand, code is very abstract, but i hope i showed you the main idea what i'm trying to do.

i might not fully understood the concept of multiple programs combining into big one. I'm still thinking to compile some parts of it in the middle, so i can compose it using IO. Please help my stupid head


Solution

  • Yes, surely it's possible to compose multiple interpreters.

    Your code corresponds to the section Free your ADT. But you should use written in the next section Composing Free monads ADTs, just not for EitherK[Algebra1, Algebra2, A] but for EitherK[Algebra1, EitherK[Algebra2, Algebra3, *], A]

    import cats.{Id, InjectK, ~>}
    import cats.data.EitherK
    import cats.free.Free
    
    // the 1st ADT
    sealed trait Config[A]
    case class Read[T]() extends Config[T]
    
    class Configs[F[_]](implicit I: InjectK[Config, F]) {
      def read[T](): Free[F, T] = Free.liftInject[F][Config, T](Read[T]())
    }
    object Configs {
      implicit def configs[F[_]](implicit I: InjectK[Config, F]): Configs[F] = new Configs[F]
    }
    
    // the 2nd ADT
    sealed trait Server[A]
    case class Start[T, C](config: C) extends Server[T]
    
    class Servers[F[_]](implicit I: InjectK[Server, F]) {
      def start[T, C](config: C): Free[F, T] = Free.liftInject[F][Server, T](Start[T, C](config))
    }
    object Servers {
      implicit def servers[F[_]](implicit I: InjectK[Server, F]): Servers[F] = new Servers[F]
    }
    
    // the 3rd ADT
    sealed trait VictoriaMetric[T]
    case class Query[T, C](query: String, config: C) extends VictoriaMetric[T]
    case class Put[T, C](value: T, config: C) extends VictoriaMetric[Unit]
    
    class VictoriaMetrics[F[_]](implicit I: InjectK[VictoriaMetric, F]) {
      def query[T, C](query: String, config: C): Free[F, T] = Free.liftInject[F][VictoriaMetric, T](Query[T, C](query, config))
      def put[T, C](value: T, config: C): Free[F, Unit] = Free.liftInject[F][VictoriaMetric, Unit](Put[T, C](value, config))
    }
    object VictoriaMetrics {
      implicit def victoriaMetrics[F[_]](implicit I: InjectK[VictoriaMetric, F]): VictoriaMetrics[F] = new VictoriaMetrics[F]
    }
    
    //interpreters
    object ConfigInterpreter extends (Config ~> Id) {
      override def apply[A](fa: Config[A]): A = fa match {
        case Read() => ???
      }
    }
    
    object ServerInterpreter extends (Server ~> Id) {
      override def apply[A](fa: Server[A]): A = fa match {
        case Start(config) => ???
      }
    } 
    
    object VictoriaMetricInterpreter extends (VictoriaMetric ~> Id) {
      override def apply[A](fa: VictoriaMetric[A]): A = fa match {
        case Query(query, config) => ???
        case Put(value, config)   => ()
      }
    }
    
    case class ConfigData()
    case class FromVictoriaData()
    
    def program[F[_]](implicit
      C: Configs[F],
      S: Servers[F],
      V: VictoriaMetrics[F]
    ): Free[F, Unit] = for {
      config           <- C.read[ConfigData]()
      dataFromVictoria <- V.query[FromVictoriaData, ConfigData]("my query", config)
      server           <- S.start[Unit, ConfigData](config)
    } yield ()
    
    type CatsApp[A] = EitherK[Config, EitherK[Server, VictoriaMetric, *], A]
    
    val interpreter: CatsApp ~> Id = ConfigInterpreter or (ServerInterpreter or VictoriaMetricInterpreter)
    
    val evaled: Unit = program[CatsApp].foldMap(interpreter)
    

    https://medium.com/@olxc/the-evolution-of-a-scala-programmer-1b7a709fb71f#2de0