Accessor Methods (deprecated)
Accessor methods are little helper methods that lookup a service from the environment, and then forward your call to that service.
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
Rchannel of their method's return types.
The recommended service pattern injects service dependencies directly, and therefore has none of these problems.
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
}