Generating Accessor Methods Using Macros
Writing accessor methods is a repetitive task and would be cumbersome in services with many methods. We can automate the generation of accessor methods using zio-macro
module.
To install the zio-macro
we should add the following line in our build.sbt
file:
libraryDependencies += "dev.zio" %% "zio-macros" % "<zio-version>"
Also, to enable macro expansion we need to setup our project:
for Scala
>= 2.13
add compiler option:scalacOptions += "-Ymacro-annotations"
for Scala
< 2.13
add macro paradise compiler plugin:compilerPlugin(("org.scalamacros" % "paradise" % "2.1.1") cross CrossVersion.full)
If you are using IntelliJ, macro generated accessors will not be available in IDE hints without ZIO plugin.
At the moment these are only available for Scala versions 2.x
, however their equivalents for Scala 3 are on our roadmap.
Monomorphic Services
We can use the @accessible
macro to generate service member accessors:
import zio._
import zio.macros.accessible
@accessible
trait ServiceA {
def method(input: Something): UIO[Unit]
}
// below will be autogenerated
object ServiceA {
def method(input: Something) =
ZIO.serviceWithZIO[ServiceA](_.method(input))
}
For normal values, a ZIO
with Nothing
on error channel is generated:
import zio._
import zio.macros.accessible
@accessible
trait ServiceB {
def pureMethod(input: Something): SomethingElse
}
// below will be autogenerated
object ServiceB {
def pureMethod(input: Something): ZIO[ServiceB, Nothing, SomethingElse] =
ZIO.serviceWith[ServiceB](_.pureMethod(input))
}
The @throwing
annotation will mark impure methods. Using this annotation will request ZIO to push the error on the error channel:
import zio._
import zio.macros.accessible
import zio.macros.throwing
@accessible
trait ServiceC {
@throwing
def impureMethod(input: Something): SomethingElse
}
// below will be autogenerated
object ServiceC {
def impureMethod(input: Something): ZIO[ServiceC, Throwable, SomethingElse] =
ZIO.serviceWithZIO[ServiceC](s => ZIO(s.impureMethod(input)))
}
Below is a fully working example:
import zio._
import zio.macros.accessible
@accessible
trait KeyValueStore {
def set(key: String, value: Int): Task[Int]
def get(key: String): Task[Int]
}
case class InmemoryKeyValueStore(map: Ref[Map[String, Int]])
extends KeyValueStore {
override def set(key: String, value: Int): Task[Int] =
map.update(_.updated(key, value)).map(_ => value)
override def get(key: String): Task[Int] =
map.get.map(_.get(key)).someOrFailException
}
object InmemoryKeyValueStore {
val layer: ULayer[KeyValueStore] =
ZLayer {
for {
map <- Ref.make(Map[String, Int]())
} yield InmemoryKeyValueStore(map)
}
}
object MainApp extends ZIOAppDefault {
val myApp =
for {
_ <- KeyValueStore.set("key", 5)
key <- KeyValueStore.get("key")
} yield key
def run = myApp.provide(InmemoryKeyValueStore.layer).debug
}
Writing Polymorphic Services
With Proper Type Parameters
If the service is polymorphic for some proper types, we can use the @accessible
macro like previous examples.
Assume we have a KeyValueStore
like below, as we will see using @accessible
will generate us the accessor methods:
import zio._
import zio.macros.accessible
@accessible
trait KeyValueStore[K, V] {
def set(key: K, value: V): Task[V]
def get(key: K): Task[V]
}
case class InmemoryKeyValueStore(map: Ref[Map[String, Int]])
extends KeyValueStore[String, Int] {
override def set(key: String, value: Int): Task[Int] =
map.update(_.updated(key, value)).map(_ => value)
override def get(key: String): Task[Int] =
map.get.map(_.get(key)).someOrFailException
}
object InmemoryKeyValueStore {
val layer: ULayer[KeyValueStore[String, Int]] =
ZLayer {
for {
map <- Ref.make(Map[String, Int]())
} yield InmemoryKeyValueStore(map)
}
}
object MainApp extends ZIOAppDefault {
val myApp =
for {
_ <- KeyValueStore.set("key", 5)
key <- KeyValueStore.get[String, Int]("key")
} yield key
def run = myApp.provide(InmemoryKeyValueStore.layer).debug
}
With Higher-Kinded Type Parameters (F[_]
)
If a service has a higher-kinded type parameter like F[_]
we should use the accessibleM
macro. Here is an example of such a service:
import zio._
import zio.macros.accessibleM
@accessibleM[Task]
trait KeyValueStore[K, V, F[_]] {
def set(key: K, value: V): F[V]
def get(key: K): F[V]
}
case class InmemoryKeyValueStore(map: Ref[Map[String, Int]])
extends KeyValueStore[String, Int, Task] {
override def set(key: String, value: Int): Task[Int] =
map.update(_.updated(key, value)).map(_ => value)
override def get(key: String): Task[Int] =
map.get.map(_.get(key)).someOrFailException
}
object InmemoryKeyValueStore {
val layer: ULayer[KeyValueStore[String, Int, Task]] =
ZLayer {
for {
map <- Ref.make(Map[String, Int]())
} yield InmemoryKeyValueStore(map)
}
}
object MainApp extends ZIOAppDefault {
val myApp =
for {
key <- KeyValueStore.set[String, Int]("key", 5)
_ <- KeyValueStore.get[String, Int]("key")
} yield key
def run = myApp.provide(InmemoryKeyValueStore.layer).debug
}
With Higher-Kinded Type Parameters (F[_, _]
)
If the service has a higher-kinded type parameter like F[_, _]
we should use the accessibleMM
macro. Let's see an example:
import zio._
import zio.macros.accessibleMM
@accessibleMM[IO]
trait KeyValueStore[K, V, E, F[_, _]] {
def set(key: K, value: V): F[E, V]
def get(key: K): F[E, V]
}
case class InmemoryKeyValueStore(map: Ref[Map[String, Int]])
extends KeyValueStore[String, Int, String, IO] {
override def set(key: String, value: Int): IO[String, Int] =
map.update(_.updated(key, value)).map(_ => value)
override def get(key: String): IO[String, Int] =
map.get.map(_.get(key)).someOrFail(s"key not found: $key")
}
object InmemoryKeyValueStore {
val layer: ULayer[KeyValueStore[String, Int, String, IO]] =
ZLayer {
for {
map <- Ref.make(Map[String, Int]())
} yield InmemoryKeyValueStore(map)
}
}
object MainApp extends ZIOAppDefault {
val myApp =
for {
_ <- KeyValueStore.set[String, Int, String]("key", 5)
key <- KeyValueStore.get[String, Int, String]("key")
} yield key
def run = myApp.provide(InmemoryKeyValueStore.layer).debug
}