Skip to main content
Version: 2.x

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.

info

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

}