Skip to main content
Version: 2.x

Accessor Methods (deprecated)

Accessor methods are little helper methods that lookup a service from the environment, and then forward your call to that service.

info

The service pattern provides a better way to structure programs, and it does not need accessor methods. Therefore, accessor methods are now deprecated.

What is an Accessor Method?​

Imagine a service defined as:

import zio._

trait BlobStorage {
def get(id: String): ZIO[Any, Throwable, Array[Byte]]

def put(content: Array[Byte]): ZIO[Any, Throwable, String]
}

The accessor methods are then defined as:

import zio._

object BlobStorage {
// Accessor method for BlobStorage.get
def get(id: String): ZIO[BlobStorage, Throwable, Array[Byte]] =
ZIO.serviceWithZIO[BlobStorage](_.get(id))

// Accessor method for BlobStorage.put
def put(content: Array[Byte]): ZIO[BlobStorage, Throwable, String] =
ZIO.serviceWithZIO[BlobStorage](_.put(content))
}

Each accessor method fetches the service from the environment, and then immediately forwards the method call.

The service can now be used as:

  BlobStorage.get("blob-id")  // returns a ZIO[BlobStorage, Throwable, Array[Byte]]

Notice how the BlobStorage trait is in the environment (the R channel) of the returned ZIO.

Why are accessor methods deprecated?​

Accessor methods have some drawbacks:

  • You must write more code.
  • The extra code must stay in sync with the service's trait.
  • The service is looked up in the environment each time it is used. This incurs (a small) performance penalty.
  • The ZIO environment permeates deeper into your code than strictly necessary. This problem is exacerbated when services start exposing the services they depend on in the R channel of their method's return types.

The recommended service pattern injects service dependencies directly, and therefore has none of these problems.

Generating Accessor Methods with Macros​

info

Accessor Methods macros are only available for Scala versions 2.x. They will not be made available for scala 3.x.

Writing accessor methods is a repetitive task and is tedious for services with many methods. We can automate the generation of accessor methods using the zio-macro module.

To install the zio-macro add the following line in the build.sbt file:

libraryDependencies += "dev.zio" %% "zio-macros" % "<zio-version>"

In addition, enable macro expansion with:

  • for Scala 2.13 add the compiler option:

    scalacOptions += "-Ymacro-annotations"
  • for Scala < 2.13 add the 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.

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

}