Together with Ziverge Inc., we’re more than happy to invite you to the first ZIO Hackathon 2020 after the official launch of the 1.0 version that took place on August 4, 2020. The whole ZIO community was waiting for this moment for almost three years, but here we are!

During the ZIO Hackathon 2020, you will be able to attend talks on ZIO projects straight from ZIO open source contributors, as well as get advice on all your ZIO challenges like bugs and new feature ideas.

About ZIO Hackathon 2020

Date: 20 – 21 November 2020
Location: Online – Zoom/Discord/Github

Schedule

  • Friday, November 20st: Introductory talk – 8 pm (GMT+1)
  • Saturday, November 21st: ZIO Hackathon – Start: 10 am (GMT+1)

Hands-on open-source coding, guided by expert mentors – is, of course, the main idea of the ZIO Hackathon 2020. However, attendees can join breakout sessions to learn about specific aspects of ZIO (ZIO core, fibers, STM, execution traces, fiber dumps, ZIO test, ZIO config, etc.), and team leaders will help different groups work on core issues in ZIO, the ZIO ecosystem, and other related libraries in the functional Scala ecosystem.

Additionally, Anyone who attends the ZIO Hackathon and merges a contribution to any official ZIO project during the event will get a free ZIO T-shirt (no shipping limitations) and a 10% discount to the Functional Scala 2020 Conference.

About ZIO

ZIO is a zero-dependency Scala library for asynchronous and concurrent programming. Powered by scalable, non-blocking fibers that never waste or leak resources, ZIO lets you build scalable, resilient, and reactive applications that meet the needs of your business.

After the big launch, it already has more than half a million downloads in a month. ZIO is used by firms like Netflix, DHL, Evolution Gaming, and many others.

The event was brought to you by Ziverge Inc. – the commercial developer of the ZIO library and us, Scalac. Scalac is a development, and consulting company, and contributor in multiple projects in the ZIO ecosystem. Find out more about Scalac.

Other ZIO events and resources by Scalac

Functional World Meetup

Exploring ZIO Prelude: The game-changer for type classes in Scala

Join us on Thursday, August 20 at 6.00 PM, GMT+2

About the talk

On this talk, we are going to explore how ZIO Prelude provides us an accessible and fun way of writing pure, generic, and composable code in Scala, without needing to appeal to the traditional Functor hierarchy.

More specifically, we’ll explore these uses cases:

  • Combining data structures
  • Traversing data structures
  • Validating data structures
  • And… working with the brand-new ZPure!

About Jorge Vasquez

Software developer focused on the backend. He had the chance to work with several technologies and programming languages across different industries, such as Telco, AdTech, and Online Education. 

He’s always looking to improve his skills, finding new and better ways to solve business problems. He loves functional programming, and he’s convinced it can help to make better software. Also, he’s excited about new libraries like ZIO that are making Scala FP more accessible to developers. 

Author of the “Mastering modularity in ZIO with Zlayer” ebook mentioned below.

Mastering Modularity in ZIO with Zlayer Ebook

Writing modular applications is without a doubt very important in software engineering.  It allows us to build software no matter the complexity involved. And composability has been one of the core principles of ZIO from the very beginning.

In this e-book you will learn:

  • What is the new module structure suggested by ZIO
  • How to reorganize the ZIO application around ZLayers
  • ZIO new data types: ZLayer and Has
  • ZLayer type aliases
  • How to create and combine ZLayers, with a horizontal and vertical composition
  • How to reorganize ZIO tests and mocks around ZLayers 

Read also

a:7:{s:8:”location”;a:1:{i:0;a:1:{i:0;a:3:{s:5:”param”;s:9:”page_type”;s:8:”operator”;s:2:”==”;s:5:”value”;s:10:”posts_page”;}}}s:8:”position”;s:6:”normal”;s:5:”style”;s:7:”default”;s:15:”label_placement”;s:3:”top”;s:21:”instruction_placement”;s:5:”label”;s:14:”hide_on_screen”;s:0:””;s:11:”description”;s:0:””;}

Writing concurrent data structures using traditional tools – just like everything else under java.util.concurrent – is generally very complicated. You need to really think about what you are doing to avoid typical concurrency issues, such as deadlocks and race conditions.  And let’s be honest, thinking about all the possible scenarios that could arise is not just hard, but also sometimes infeasible.

So, in this article, we are going to see how ZIO STM can make our lives a lot easier when it comes to writing concurrent data structures – such as a concurrent LRU Cache – in a completely lock-free fashion that is a lot simpler to reason about.

Requirements of an LRU Cache

A cache is a structure that stores data (which might be the result of an earlier computation or obtained from external sources such as databases) so that future requests for this data can be served faster.

Now, a Least Recently Used (LRU) Cache must fulfill these requirements:

  • Fixed capacity: This is for limiting memory usage.
  • Fast access: Insert and lookup operations should be executed in O(1) time.
  • An efficient eviction algorithm: The idea is that, when the cache capacity is reached and a new entry needs to be inserted into the cache, the Least Recently Used entry gets replaced.

More concretely, an LRU cache should support these two operations:

  • get(key): Get the value of a given key if it exists in the cache, otherwise return an error.
  • put(key, value): Put the given (key, value) into the cache. When the cache reaches its capacity, it should evict the Least Recently Used entry before inserting a new one.

So, for implementing an efficient LRU Cache (meaning get and put operations are executed in O(1) time), we could use two data structures:

  • Hash Map: containing (key, value) pairs.
  • Doubly linked list: which will contain the history of referenced keys. The Most Recently Used key will be at the start of the list, and the Least Recently Used one will be at the end.

In the following image, we can see an example of the status of a LRU Cache (with capacity of 4) at a given moment:

Example of the status of a LRU Cache

So, the history of referenced keys (1, 3, 2 and 4) shows that Key 1 is the Most Recently Used one (because it’s at the start of the list), and that Key 4 is the Least Recently Used one (because it’s at the end of the list). So, if a new item needs to be stored into the cache, Item 4 would have to be replaced.

Quick introduction to ZIO

According to the ZIO documentation page, ZIO is a library for “Type-safe, composable asynchronous and concurrent programming for Scala”. This means ZIO allows us to build applications that are:

  • Highly composable: Because ZIO is based on functional programming principles, such as using pure functions and immutable values, it allows us to easily compose solutions to complex problems from simple building blocks.
  • 100% asynchronous and non-blocking.
  • Highly performant and concurrent: ZIO implements Fiber-based concurrency, and by the way, you can read more about ZIO Fibers in this really nice article written by Mateusz Sokół here.
  • Type-safe: ZIO leverages the Scala Type System so it can catch more bugs at compile time.

The most important data type in ZIO (and also the basic building block of ZIO applications) is also called ZIO:

ZIO[-R, +E, +A]

The ZIO data type is called a functional effect, which means it is a lazy, immutable value that contains a description of a series of interactions with the outside world (database interactions, calling external APIs, etc.). A nice mental model of the ZIO data type is the following:

R => Either[E, A]

This means that a ZIO effect needs an environment of type R to run (the environment could be anything: a database connection, a REST client, a configuration object, etc.), and it can either fail with an error of type E or succeed with a value of type A.

Finally, it’s worth mentioning that ZIO provides some type aliases for the ZIO effect type which are very useful in representing some common use cases:

  • Task[+A] = ZIO[Any, Throwable, A]: This means a Task[A]is a ZIO effect that:
    • Doesn’t require an environment to run (that’s why the R type is replaced by Any, meaning the effect will run no matter what we provide to it as environment)
    • Can fail with a Throwable
    • Can succeed with an A
  • UIO[+A] = ZIO[Any, Nothing, A]: This means a UIO[A] is a ZIO effect that:
    • Doesn’t require an environment to run.
    • Can’t fail
    • Can succeed with an A
  • RIO[-R, +A] = ZIO[R, Throwable, A]: This means a RIO[R, A] is a ZIO effect that:
    • Requires an environment R to run
    • Can fail with a Throwable
    • Can succeed with an A
  • IO[+E, +A] = ZIO[Any, E, A]: This means a IO[E, A] is a ZIO effect that:
    • Doesn’t require an environment to run.
    • Can fail with an E
    • Can succeed with an A
  • URIO[-R, +A] = ZIO[R, Nothing, A]: This means a URIO[R, A] is a ZIO effect that:
    • Requires an environment R to run
    • Can’t fail
    • Can succeed with an A

Implementing the LRU Cache with ZIO Ref

First, we need to add some ZIO dependencies to our build.sbt:

val zioVersion = "1.0.0-RC18-2"

lazy val compileDependencies = Seq(
  "dev.zio" %% "zio" % zioVersion
) map (_ % Compile)

lazy val testDependencies = Seq(
  "dev.zio" %% "zio-test"     % zioVersion,
  "dev.zio" %% "zio-test-sbt" % zioVersion
) map (_ % Test)
JavaScript

Now, we can begin with the implementation of the LRU Cache. An initial model could be:

final class LRUCache[K, V](
  private val capacity: Int,
  private var items: Map[K, CacheItem[K, V]],
  private var start: Option[K],
  private var end: Option[K]
)
JavaScript

So, the LRUCache should have:

  • A capacity (which should be a positive integer set on creation and shouldn’t change anymore, which is why it’s modeled as a val).
  • A Map containing items, this will change all the time, which is why we are modeling this as a var. By the way, the model of a CacheItem would be like this:

final case class CacheItem[K, V](value: V, left: Option[K], right: Option[K])

This means that each CacheItem should not just contain a value to be stored, but also references to the left and right keys in the history of referenced keys (remember we’ll use a doubly linked list for keeping a history of referenced keys). These are modeled as Options because, if an item is at the start of the history (meaning it’s the Most Recently Used item), there won’t be any item on its left. Something similar happens when an item is at the end of the history (meaning it’s the Least Recently Used item), there won’t be any item on its right.

  • References to the start and end keys, these will also change all the time, and that’s why they are vars.

There’s a problem with this implementation though: the fact we are resorting to vars. In functional programming, we should model everything as immutable values, and also using vars will make it harder to use the LRUCache in concurrent scenarios (using mutability in our applications instantly makes them prone to race conditions!).

So, what can we do? Well, ZIO has the answer! We can use its Ref[A]data type, which is a purely functional description of a mutable reference. The fundamental operations of a Ref are get and set, and both of them return ZIO effects which describe the operations of reading from and writing to the Ref.

Then, a better (and purely functional) version of our LRUCache would be:

final class LRUCache[K, V](

  private val capacity: Int,

  private val itemsRef: Ref[Map[K, CacheItem[K, V]]],

  private val startRef: Ref[Option[K]],

  private val endRef: Ref[Option[K]]

)
JavaScript

Now, we can make the constructor private, and create a smart constructor in the companion object:

final class LRUCache[K, V] private (

  private val capacity: Int,

  private val itemsRef: Ref[Map[K, CacheItem[K, V]]],

  private val startRef: Ref[Option[K]],

  private val endRef: Ref[Option[K]]

)

object LRUCache {

  def make[K, V](capacity: Int): IO[IllegalArgumentException, LRUCache[K, V]] =

    if (capacity > 0) {

      for {

        itemsRef <- Ref.make(Map.empty[K, CacheItem[K, V]])

        startRef <- Ref.make(Option.empty[K])

        endRef   <- Ref.make(Option.empty[K])

      } yield new LRUCache[K, V](capacity, itemsRef, startRef, endRef)

    } else {

      ZIO.fail(new IllegalArgumentException("Capacity must be a positive number!"))

    }

}
JavaScript

The make function is our smart constructor, and we can see it expects to receive a capacity, and it returns an effect which can fail with an IllegalArgumentException (when a non-positive capacity is provided) or can succeed with an LRUCache[K, V]. We also know that the LRUCache constructor expects to receive not just the capacity, but also the initial values for itemsRef, startRef and endRef. For creating these Refs, we can use the Ref.make function, which receives the initial value for the Ref and returns a UIO[Ref[A]]. And because ZIO effects are monads (meaning they have map and flatMap methods), we can combine the results of calling Ref.make using for-comprehension syntax, for yielding a new LRUCache.

Now, we can implement the get and put methods for the LRUCache. Let’s start with the get method first:

def get(key: K): IO[NoSuchElementException, V] =

    (for {

      items <- self.itemsRef.get

      item  <- ZIO.fromOption(items.get(key)).mapError(_ => new NoSuchElementException(s"Key does not exist: $key"))

      _     <- removeKeyFromList(key) *> addKeyToStartOfList(key)

    } yield item.value).refineToOrDie[NoSuchElementException]
JavaScript

As you can see, the method implementation looks really nice and simple: it’s practically just a description of what to do when getting an element from the cache:

  • Firstly, we need to get the items Map from the itemsRef
  • Next, we need to obtain the requested key from the items map. This key may or not exist, if it does exist the flow just continues and, if it doesn’t, the method fails with a NoSuchElementException and the flow execution stops.
  • After the item is obtained from the Map, we need to update the history of referenced keys, because the requested key becomes the Most Recently Used one. That’s why we need to call the auxiliary functions removeKeyFromList and addKeyToStartOfList.
  • Finally, the item value is returned, and the error type is refined to be just NoSuchElementException (this is the only error we are expecting to happen and that should be handled when calling get). Any other errors should make the fiber execution die because they are bugs that need to be exposed at execution time and fixed.

Now, let’s see the put method implementation. Again it’s really simple:

def put(key: K, value: V): UIO[Unit] =

    (for {

      optionStart <- self.startRef.get

      optionEnd   <- self.endRef.get

      _ <- ZIO.ifM(self.itemsRef.get.map(_.contains(key)))(

            updateItem(key, value),

            addNewItem(key, value, optionStart, optionEnd)

          )

    } yield ()).orDie
JavaScript

We can see that:

  • The method checks whether the provided key is already present in the items map (again, we are accessing the Map calling the get method on itemsRef):
    • If the key is already present, the updateItem auxiliary function is called.
    • Otherwise, a new item is added, by calling the addNewItem auxiliary function.
  • Finally, the method just yields a unit value and dies in the case of an error. This is because this method should never fail, otherwise there’s a bug that needs to be exposed at runtime and fixed.

Now we can take a look at some auxiliary functions (we won’t go into the details of every auxiliary function, for more details you can take a look at the complete source code in the jorge-vasquez-2301/zio-lru-cache repository). First up, we have the removeKeyFromList function:

private def removeKeyFromList(key: K): IO[Error, Unit] =

    for {

      cacheItem      <- getExistingCacheItem(key)

      optionLeftKey  = cacheItem.left

      optionRightKey = cacheItem.right

      _ <- (optionLeftKey, optionRightKey) match {

            case (Some(l), Some(r)) =>

              updateLeftAndRightCacheItems(l, r)

            case (Some(l), None) =>

              setNewEnd(l)

            case (None, Some(r)) =>

              setNewStart(r)

            case (None, None) =>

              clearStartAndEnd

          }

    } yield ()
JavaScript

As you can see, the implementation is pretty straightforward, and it considers all the possible cases when removing a key from the history of referenced keys:

  • When the key to be removed has other keys to its left and right, the corresponding cache items have to be updated so they point to each other.
  • When the key to be removed has another key to its left, but not to its right, it means the key to be removed is at the end of the list, so the end has to be updated.
  • When the key to be removed has another key to its right, but not to its left, it means the key to be removed is at the start of the list, so the start has to be updated.
  • When the key to be removed has no keys to left nor right, that means the key to be removed is the only one, so the start and end references have to be cleared.

And here is the getExistingCacheItemfunction implementation:

private def getExistingCacheItem(key: K): IO[Error, CacheItem[K, V]] =

    ZIO.require(new Error(s"Key does not exist: $key"))(self.itemsRef.get.map(_.get(key)))
JavaScript

This function is named this way because the idea is that when we use it, we expect that the cache item we want to get exists. If the item does not exist, it means there’s some kind of problem, and we are signaling that with an Error. (By the way, if you look again at the get and put methods of LRUCache, you can see the application will die if an Error is produced).

Another interesting function to look at is updateLeftAndRightCacheItems, because it shows a use case of ZIO Ref.update, which automically modifies an Ref with the specified function.

private def updateLeftAndRightCacheItems(l: K, r: K): IO[Error, Unit] =

    for {

      leftCacheItem  <- getExistingCacheItem(l)

      rightCacheItem <- getExistingCacheItem(r)

      _              <- self.itemsRef.update(_.updated(l, leftCacheItem.copy(right = Some(r))))

      _              <- self.itemsRef.update(_.updated(r, rightCacheItem.copy(left = Some(l))))

    } yield ()
JavaScript

Finally, let’s take a look at the addKeyToStartOfList function, which is also pretty straightforward. Something to notice is that we are using Ref.updateSome for updating the endRef value, when it’s empty.

private def addKeyToStartOfList(key: K): IO[Error, Unit] =

    for {

      oldOptionStart <- self.startRef.get

      _ <- getExistingCacheItem(key).flatMap { cacheItem =>

            self.itemsRef.update(_.updated(key, cacheItem.copy(left = None, right = oldOptionStart)))

          }

      _ <- oldOptionStart match {

            case Some(oldStart) =>

              getExistingCacheItem(oldStart).flatMap { oldStartCacheItem =>

                self.itemsRef.update(_.updated(oldStart, oldStartCacheItem.copy(left = Some(key))))

              }

            case None => ZIO.unit

          }

      _ <- self.startRef.set(Some(key))

      _ <- self.endRef.updateSome { case None => Some(key) }

    } yield ()
JavaScript

There is one more thing to do before testing: create an IntLRUCacheEnv module in the com.example.cache package object (this module will be used for testing, and for simplicity, it considers integer keys and values):

package object cache {

  type IntLRUCacheEnv = Has[IntLRUCacheEnv.Service]

  object IntLRUCacheEnv {

    trait Service {

      def getInt(key: Int): IO[NoSuchElementException, Int]

      def putInt(key: Int, value: Int): UIO[Unit]

      val getCacheStatus: UIO[(Map[Int, CacheItem[Int, Int]], Option[Int], Option[Int])]

    }

    object Service {

      val zioRefImpl: ZLayer[Has[Int], IllegalArgumentException, IntLRUCacheEnv] =

        ZLayer.fromFunctionM { hasInt: Has[Int] =>

          LRUCache.make[Int, Int](hasInt.get).map { lruCache =>

            new Service {

              override def getInt(key: Int): IO[NoSuchElementException, Int] = lruCache.get(key)

              override def putInt(key: Int, value: Int): UIO[Unit] = lruCache.put(key, value)

              override val getCacheStatus: UIO[(Map[Int, CacheItem[Int, Int]], Option[Int], Option[Int])] =

                lruCache.getStatus

            }

          }

        }

    }

  }

  def getInt(key: Int): ZIO[IntLRUCacheEnv, NoSuchElementException, Int] = ZIO.accessM(_.get.getInt(key))

  def putInt(key: Int, value: Int): ZIO[IntLRUCacheEnv, Nothing, Unit] = ZIO.accessM(_.get.putInt(key, value))

  val getCacheStatus: ZIO[IntLRUCacheEnv, Nothing, (Map[Int, CacheItem[Int, Int]], Option[Int], Option[Int])] =

    ZIO.accessM(_.get.getCacheStatus)

}
JavaScript

By the way, this module is defined based on the new ZLayer data type that comes with ZIO since 1.0.0-RC18, and you can see there’s a zioRefImpl that makes use of our LRUCache. I won’t go into the details of using ZLayer here, but you can read the ZIO documentation page to get more information, and you can also take a look at these really nice articles:

Testing implementation with a single fiber

It’s time to put our LRUCache to test! Firstly, we are going to test it under a single-fiber scenario, the testing code is the following (by the way, this testing code reflects the example shown on this link):

object UseLRUCacheWithOneFiber extends App {

  def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =

    (for {

      _ <- put(1, 1)

      _ <- put(2, 2)

      _ <- get(1)

      _ <- put(3, 3)

      _ <- get(2)

      _ <- put(4, 4)

      _ <- get(1)

      _ <- get(3)

      _ <- get(4)

    } yield 0)

      .provideCustomLayer(ZLayer.succeed(2) >>> IntLRUCacheEnv.Service.zioRefImpl)

      .catchAll(ex => putStrLn(ex.getMessage) *> ZIO.succeed(1))

  private def get(key: Int): URIO[Console with IntLRUCacheEnv, Unit] =

    (for {

      _ <- putStrLn(s"Getting key: $key")

      v <- getInt(key)

      _ <- putStrLn(s"Obtained value: $v")

    } yield ()).catchAll(ex => putStrLn(ex.getMessage))

  private def put(key: Int, value: Int): URIO[Console with IntLRUCacheEnv, Unit] =

    putStrLn(s"Putting ($key, $value)") *> putInt(key, value)

}
JavaScript

So, we are running the application with an IntLRUCacheEnv.Service.zioRefImpl, with a capacity of 2. After executing the above program, the following result is obtained:

Putting (1, 1)

Putting (2, 2)

Getting key: 1

Obtained value: 1

Putting (3, 3)

Getting key: 2

Key does not exist: 2

Putting (4, 4)

Getting key: 1

Key does not exist: 1

Getting key: 3

Obtained value: 3

Getting key: 4

Obtained value: 4

As we can see, the behavior is correct! So, our implementation looks good so far.

Testing implementation with multiple concurrent fibers

Now, let’s test the LRUCache again, but against multiple concurrent fibers this time. The testing code is the following:

object UseLRUCacheWithMultipleFibers extends App {

  def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =

    (for {

      fiberReporter  <- reporter.forever.fork

      fiberProducers <- ZIO.forkAll(ZIO.replicate(100)(producer.forever))

      fiberConsumers <- ZIO.forkAll(ZIO.replicate(100)(consumer.forever))

      _              <- getStrLn.orDie *> (fiberReporter <*> fiberProducers <*> fiberConsumers).interrupt

    } yield 0)

      .provideCustomLayer(ZLayer.succeed(3) >>> IntLRUCacheEnv.Service.zioRefImpl)

      .catchAll(ex => putStrLn(ex.getMessage) *> ZIO.succeed(1))

  val producer: URIO[Console with Random with IntLRUCacheEnv, Unit] =

    for {

      number <- nextInt(100)

      _      <- putStrLn(s"Producing ($number, $number)")

      _      <- putInt(number, number)

    } yield ()

  val consumer: URIO[Console with Random with IntLRUCacheEnv, Unit] =

    (for {

      key   <- nextInt(100)

      _     <- putStrLn(s"Consuming key: $key")

      value <- getInt(key)

      _     <- putStrLn(s"Consumed value: $value")

    } yield ()).catchAll(ex => putStrLn(ex.getMessage))

  val reporter: ZIO[Console with IntLRUCacheEnv, NoSuchElementException, Unit] =

    for {

      (items, optionStart, optionEnd) <- getCacheStatus

      _                               <- putStrLn(s"Items: $items, Start: $optionStart, End: $optionEnd")

    } yield ()

}
JavaScript

We can see that an IntLRUCacheEnv.Service.zioRefImpl with a  capacity of 3 is provided. Also, 100 producers and 100 consumers of random integers are started in different fibers, and we have a reporter that will just print to the console the cache current status (stored items, start and end keys of the recently used items history). When we execute this, some ugly stuff happens:

  • First, more items than the defined capacity (a lot more) are being stored! And also, stored items have a lot of inconsistencies: for example you can see below that, for a given moment, the end key (the Least Recently Used key) is 97, but looking at the corresponding CacheItem we see it has other keys to its left and right (58 and 9 respectively), but… if 97 is at the end of the list, it shouldn’t have an item to its right!,Besides this, there are a lot more discrepancies among CacheItems:

Items: HashMap(5 -> CacheItem(5,Some(45),Some(6)), 84 -> CacheItem(84,Some(51),Some(91)), 69 -> CacheItem(69,Some(83),Some(36)), 0 -> CacheItem(0,None,Some(37)), 88 -> CacheItem(88,Some(82),Some(94)), 10 -> CacheItem(10,Some(37),Some(45)), 56 -> CacheItem(56,Some(54),Some(42)), 42 -> CacheItem(42,Some(6),Some(60)), 24 -> CacheItem(24,Some(30),Some(18)), 37 -> CacheItem(37,Some(0),Some(10)), 52 -> CacheItem(52,Some(70),Some(91)), 14 -> CacheItem(14,Some(72),Some(1)), 20 -> CacheItem(20,None,Some(46)), 46 -> CacheItem(46,Some(28),Some(70)), 93 -> CacheItem(93,Some(40),Some(6)), 57 -> CacheItem(57,Some(12),Some(45)), 78 -> CacheItem(78,None,Some(41)), 61 -> CacheItem(61,None,Some(26)), 1 -> CacheItem(1,Some(14),Some(2)), 74 -> CacheItem(74,None,Some(33)), 6 -> CacheItem(6,Some(5),Some(42)), 60 -> CacheItem(60,Some(42),Some(80)), 85 -> CacheItem(85,None,Some(99)), 70 -> CacheItem(70,Some(46),Some(52)), 21 -> CacheItem(21,None,Some(65)), 33 -> CacheItem(33,Some(77),Some(32)), 28 -> CacheItem(28,None,Some(46)), 38 -> CacheItem(38,Some(98),Some(68)), 92 -> CacheItem(92,Some(63),Some(0)), 65 -> CacheItem(65,Some(21),Some(51)), 97 -> CacheItem(97,Some(58),Some(9)), 9 -> CacheItem(9,Some(97),Some(99)), 53 -> CacheItem(53,None,Some(91)), 77 -> CacheItem(77,Some(27),Some(33)), 96 -> CacheItem(96,Some(3),Some(58)), 13 -> CacheItem(13,Some(14),Some(28)), 41 -> CacheItem(41,Some(78),Some(90)), 73 -> CacheItem(73,None,Some(41)), 2 -> CacheItem(2,Some(1),Some(92)), 32 -> CacheItem(32,Some(33),Some(98)), 45 -> CacheItem(45,Some(10),Some(5)), 64 -> CacheItem(64,None,Some(34)), 17 -> CacheItem(17,None,Some(35)), 22 -> CacheItem(22,None,Some(7)), 44 -> CacheItem(44,Some(79),Some(92)), 59 -> CacheItem(59,Some(15),Some(68)), 27 -> CacheItem(27,Some(4),Some(77)), 71 -> CacheItem(71,Some(46),Some(19)), 12 -> CacheItem(12,Some(75),Some(57)), 54 -> CacheItem(54,None,Some(56)), 49 -> CacheItem(49,None,Some(63)), 86 -> CacheItem(86,None,Some(43)), 81 -> CacheItem(81,Some(98),Some(1)), 76 -> CacheItem(76,None,Some(35)), 7 -> CacheItem(7,Some(22),Some(33)), 39 -> CacheItem(39,None,Some(4)), 98 -> CacheItem(98,Some(32),Some(81)), 91 -> CacheItem(91,Some(52),Some(75)), 66 -> CacheItem(66,None,Some(27)), 3 -> CacheItem(3,Some(94),Some(96)), 80 -> CacheItem(80,Some(60),Some(84)), 48 -> CacheItem(48,None,Some(9)), 63 -> CacheItem(63,Some(49),Some(3)), 18 -> CacheItem(18,Some(24),Some(26)), 95 -> CacheItem(95,None,Some(65)), 50 -> CacheItem(50,Some(68),Some(58)), 67 -> CacheItem(67,None,Some(21)), 16 -> CacheItem(16,None,Some(82)), 11 -> CacheItem(11,Some(5),Some(73)), 72 -> CacheItem(72,Some(99),Some(14)), 43 -> CacheItem(43,Some(86),Some(3)), 99 -> CacheItem(99,Some(9),Some(72)), 87 -> CacheItem(87,Some(36),Some(46)), 40 -> CacheItem(40,Some(11),Some(93)), 26 -> CacheItem(26,Some(18),Some(16)), 8 -> CacheItem(8,Some(3),Some(0)), 75 -> CacheItem(75,Some(91),Some(12)), 58 -> CacheItem(58,Some(96),Some(97)), 82 -> CacheItem(82,Some(16),Some(88)), 36 -> CacheItem(36,Some(69),Some(87)), 30 -> CacheItem(30,Some(11),Some(24)), 51 -> CacheItem(51,Some(65),Some(84)), 19 -> CacheItem(19,None,Some(83)), 4 -> CacheItem(4,Some(62),Some(27)), 79 -> CacheItem(79,None,Some(44)), 94 -> CacheItem(94,Some(88),Some(3)), 47 -> CacheItem(47,Some(35),Some(37)), 15 -> CacheItem(15,Some(68),Some(59)), 68 -> CacheItem(68,Some(38),Some(50)), 62 -> CacheItem(62,None,Some(4)), 90 -> CacheItem(90,Some(41),Some(33)), 83 -> CacheItem(83,Some(19),Some(69))), Start: Some(16), End: Some(97)

  • And, because of the issues mentioned above, we see fibers dying because of unexpected errors like this:
Fiber failed.

An unchecked error was produced.

java.lang.Error: Key does not exist: 35

at io.scalac.LRUCache.$anonfun$getExistingCacheItem$1(LRUCache.scala:113)
JavaScript

And well, it seems our current LRUCache implementation just works correctly in a single-fiber scenario, but not in a concurrent scenario, that is really bad. So now, let’s reflect on what’s happening.

Why doesn’t our implementation work in a concurrent scenario?

It may seem weird that our current implementation does not work as expected when multiple fibers use it concurrently: We have used immutable values everywhere, pure functions, purely functional mutable references (Ref[A]) that provide atomic operations on them… but wait a moment, Ref[A] provides atomic operations on SINGLE VALUES, but what happens if we need to keep consistency across MULTIPLE VALUES? Remember that in our LRUCache implementation, we have three Refs: itemsRef, startRef and endRef. So, it seems using Ref[A] is not a powerful enough solution for our use case:

  • Refs allow atomic operations on single values only.
  • Refs don’t compose! So you can’t compose two Refs to get a resulting Ref.

So, what can we do now?

Enter ZIO STM!

The solution to our problem is using ZIO STM (Software Transactional Memory). For that, ZIO provides two basic data types:

  • ZSTM[-R, +E, +A]: Represents an effect that can be performed transactionally, that requires an environment R to run and that may fail with an error E or succeed with a value A. Also, ZIO provides a type alias when no environment R is required: STM[+E, +A], which is equivalent to ZSTM[Any, E, A].
  • TRef[A]: Represents a Transactional Ref, meaning a purely functional mutable reference that can be used in the context of a transaction.

So, basically, an STM describes a bunch of operations across several TRefs.

Important things to notice:

  • STMs are composable (we can use them in for-comprehensions!)
  • All methods in TRef are very similar to the ones in Ref, but they return STM effects instead of ZIO effects.
  • To convert a STM effect to a ZIO effect, you need to commit the transaction: When you commit a transaction, all of its operations are performed in an atomic, consistent and isolated fashion, very similar to how relational databases work.

It’s also worth mentioning that we could use other classic concurrency structures from java.util.concurrent like Locks and Semaphores for solving concurrency issues, but that’s really complicated, low-level and error-prone, and race conditions or deadlocks are very likely to happen. Instead, ZIO STM replaces all of this low-level machinery with a high-level concept: transactions in memory, and we have no race conditions and no deadlocks!

Finally, ZIO STM provides other nice data structures that can participate in transactions (all of them are based in TRef):

  • TArray
  • TQueue
  • TSet
  • TMap
  • TPromise
  • TReentrantLock
  • TSemaphore

Our LRU Cache goes concurrent! Moving from ZIO Ref to ZIO STM

The concurrent version of our LRUCache will be very similar to what we had before, but we are going to make some changes to use ZIO STM:

final class ConcurrentLRUCache[K, V] private (

  private val capacity: Int,

  private val items: TMap[K, CacheItem[K, V]],

  private val startRef: TRef[Option[K]],

  private val endRef: TRef[Option[K]]

)
JavaScript

As you can see, we are changing Refs to TRefs, and instead of having items: TRef[Map[K, CacheItem[K, V]]], we are using the more convenient and efficient TMap data type that ZIO STM provides.

The smart constructor will also be very similar to the one we had before:

object ConcurrentLRUCache {

  def make[K, V](capacity: Int): IO[IllegalArgumentException, ConcurrentLRUCache[K, V]] =

    if (capacity > 0) {

      (for {

        itemsRef <- TMap.empty[K, CacheItem[K, V]]

        startRef <- TRef.make(Option.empty[K])

        endRef   <- TRef.make(Option.empty[K])

      } yield new ConcurrentLRUCache[K, V](capacity, itemsRef, startRef, endRef)).commit

    } else {

      ZIO.fail(new IllegalArgumentException("Capacity must be a positive number!"))

    }

}
JavaScript

The biggest difference in this smart constructor is that the for-comprehension returns a STM[Nothing, ConcurrentLRUCache[K, V]], and we need to commit it for getting a ZIO effect, which is what we want to return.

Next, we have the get and put methods for the ConcurrentLRUCache

def get(key: K): IO[NoSuchElementException, V] =

    (for {

      optionItem <- self.items.get(key)

      item      <- STM.fromOption(optionItem).mapError(_ => new NoSuchElementException(s"Key does not exist: $key"))

      _          <- removeKeyFromList(key) *> addKeyToStartOfList(key)

    } yield item.value).commitEither.refineToOrDie[NoSuchElementException]

  def put(key: K, value: V): UIO[Unit] =

    (for {

      optionStart <- self.startRef.get

      optionEnd   <- self.endRef.get

      _           <- STM.ifM(self.items.contains(key))(updateItem(key, value), addNewItem(key, value, optionStart, optionEnd))

    } yield ()).commitEither.orDie
JavaScript

Again, you can see these methods are very similar to the ones we had before! The only difference is that the for-comprehensions in both methods return values of type STM, so we need to commit the transactions (we are using commitEither in this case, so transactions are always committed despite errors, and failures are handled at the ZIO level).

Now, we can take a look at the same auxiliary functions we’ve seen before, but this time with ZIO STM. First, we have the removeKeyFromList function:

private def removeKeyFromList(key: K): STM[Error, Unit] =

    for {

      cacheItem      <- getExistingCacheItem(key)

      optionLeftKey  = cacheItem.left

      optionRightKey = cacheItem.right

      _ <- (optionLeftKey, optionRightKey) match {

            case (Some(l), Some(r)) =>

              updateLeftAndRightCacheItems(l, r)

            case (Some(l), None) =>

              setNewEnd(l)

            case (None, Some(r)) =>

              setNewStart(r)

            case (None, None) =>

              clearStartAndEnd

          }

    } yield ()
JavaScript

As you may have realized, the implementation is practically the same! The difference is that the function returns a STM effect instead of a ZIO effect. In this case (and the same happens for all private methods) we are not committing the transaction yet, that’s because we want to use these private functions in combination with others, to form bigger transactions that are committed in the get and put methods.

And, here is the getExistingCacheItem function implementation, again it’s very similar to the one we had before, but now an STM effect is returned, and also getting an element from the items Map is a lot easier now, thanks to TMap:

private def getExistingCacheItem(key: K): STM[Error, CacheItem[K, V]] =

    STM.require(new Error(s"Key does not exist: $key"))(self.items.get(key))
JavaScript

And for updateLeftAndRightCacheItems, putting elements into the items Map is a lot easier now too:

private def updateLeftAndRightCacheItems(l: K, r: K): STM[Error, Unit] =
    for {
      leftCacheItem  <- getExistingCacheItem(l)
      rightCacheItem <- getExistingCacheItem(r)
      _              <- self.items.put(l, leftCacheItem.copy(right = Some(r)))
      _              <- self.items.put(r, rightCacheItem.copy(left = Some(l)))
    } yield ()
JavaScript

And, we have addKeyToStartOfList, which again is very similar to the previous version:

private def addKeyToStartOfList(key: K): STM[Error, Unit] =

    for {

      oldOptionStart <- self.startRef.get

      _ <- getExistingCacheItem(key).flatMap { cacheItem =>

            self.items.put(key, cacheItem.copy(left = None, right = oldOptionStart))

          }

      _ <- oldOptionStart match {

            case Some(oldStart) =>

              getExistingCacheItem(oldStart).flatMap { oldStartCacheItem =>

                self.items.put(oldStart, oldStartCacheItem.copy(left = Some(key)))

              }

            case None => STM.unit

          }

      _ <- self.startRef.set(Some(key))

      _ <- self.endRef.updateSome { case None => Some(key) }

    } yield ()
JavaScript

Finally, before testing, let’s add a new zioStmImpl to the IntLRUCacheEnv module. This implementation should make use of the ConcurrentLRUCache we’ve just created:

object IntLRUCacheEnv {
   ...
    object Service {

      ...
      val zioStmImpl: ZLayer[Has[Int], IllegalArgumentException, IntLRUCacheEnv] =

        ZLayer.fromFunctionM { hasInt: Has[Int] =>

          ConcurrentLRUCache.make[Int, Int](hasInt.get).map { concurrentLruCache =>

            new Service {

              override def getInt(key: Int): IO[NoSuchElementException, Int] = concurrentLruCache.get(key)


              override def putInt(key: Int, value: Int): UIO[Unit] = concurrentLruCache.put(key, value)


              override val getCacheStatus: UIO[(Map[Int, CacheItem[Int, Int]], Option[Int], Option[Int])] =

                concurrentLruCache.getStatus

            }

          }

        }

    }

  }
JavaScript

Testing implementation with multiple fibers

Now that we have our ConcurrentLRUCache, let’s put it to test with the following testing code, which is practically the same one we had before (the only difference is that we are providing a IntLRUCacheEnv.Service.zioStmImpl now):

object UseConcurrentLRUCacheWithMultipleFibers extends App {

  def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =

    (for {

      fiberReporter  <- reporter.forever.fork

      fiberProducers <- ZIO.forkAll(ZIO.replicate(100)(producer.forever))

      fiberConsumers <- ZIO.forkAll(ZIO.replicate(100)(consumer.forever))

      _              <- getStrLn.orDie *> (fiberReporter <*> fiberProducers <*> fiberConsumers).interrupt

    } yield 0)

      .provideCustomLayer(ZLayer.succeed(3) >>> IntLRUCacheEnv.Service.zioStmImpl)

      .catchAll(ex => putStrLn(ex.getMessage) *> ZIO.succeed(1))

  val producer: URIO[Console with Random with IntLRUCacheEnv, Unit] =

    for {

      number <- nextInt(100)

      _      <- putStrLn(s"Producing ($number, $number)")

      _      <- putInt(number, number)

    } yield ()

  val consumer: URIO[Console with Random with IntLRUCacheEnv, Unit] =

    (for {

      key   <- nextInt(100)

      _     <- putStrLn(s"Consuming key: $key")

      value <- getInt(key)

      _     <- putStrLn(s"Consumed value: $value")

    } yield ()).catchAll(ex => putStrLn(ex.getMessage))

  val reporter: ZIO[Console with IntLRUCacheEnv, NoSuchElementException, Unit] =

    for {

      (items, optionStart, optionEnd) <- getCacheStatus

      _                               <- putStrLn(s"Items: $items, Start: $optionStart, End: $optionEnd")

    } yield ()

}
JavaScript

When we run this, everything works as it should! (and the best part is, we didn’t need to use Locks at all!) No more unexpected errors, and the reporter shows our cache keeps internal consistency, this is an example of what is printed to console for two executions of the reporter:

Items: Map(43 -> CacheItem(43,Some(16),None), 16 -> CacheItem(16,Some(32),Some(43)), 32 -> CacheItem(32,None,Some(16))), Start: Some(32), End: Some(43)

Items: Map(30 -> CacheItem(30,None,Some(69)), 53 -> CacheItem(53,Some(69),None), 69 -> CacheItem(69,Some(30),Some(53))), Start: Some(30), End: Some(53)

Writing unit tests for the Concurrent LRU Cache using ZIO Test

Finally, we can write some unit tests for our ConcurrentLRUCache using zio-test:

object ConcurrentLRUCacheTest extends DefaultRunnableSpec {

  def spec = suite("ConcurrentLRUCache")(

    testM("can't be created with non-positive capacity") {

      assertM(ConcurrentLRUCache.make[Int, Int](0).run)(

        fails(hasMessage(equalTo("Capacity must be a positive number!")))

      )

    },

    testM("works as expected") {

      val expectedOutput = Vector(

        "Putting (1, 1)\n",

        "Putting (2, 2)\n",

        "Getting key: 1\n",

        "Obtained value: 1\n",

        "Putting (3, 3)\n",

        "Getting key: 2\n",

        "Key does not exist: 2\n",

        "Putting (4, 4)\n",

        "Getting key: 1\n",

        "Key does not exist: 1\n",

        "Getting key: 3\n",

        "Obtained value: 3\n",

        "Getting key: 4\n",

        "Obtained value: 4\n"

      )

      for {

        lruCache <- ConcurrentLRUCache.make[Int, Int](2)

        _        <- put(lruCache, 1, 1)

        _        <- put(lruCache, 2, 2)

        _        <- get(lruCache, 1)

        _        <- put(lruCache, 3, 3)

        _        <- get(lruCache, 2)

        _        <- put(lruCache, 4, 4)

        _        <- get(lruCache, 1)

        _        <- get(lruCache, 3)

        _        <- get(lruCache, 4)

        output   <- TestConsole.output

      } yield {

        assert(output)(equalTo(expectedOutput))

      }

    }

  )

  private def get[K, V](concurrentLruCache: ConcurrentLRUCache[K, V], key: K): ZIO[Console, Nothing, Unit] =

    (for {

      _ <- putStrLn(s"Getting key: $key")

      v <- concurrentLruCache.get(key)

      _ <- putStrLn(s"Obtained value: $v")

    } yield ()).catchAll(ex => putStrLn(ex.getMessage))

  private def put[K, V](concurrentLruCache: ConcurrentLRUCache[K, V], key: K, value: V): ZIO[Console, Nothing, Unit] =

    putStrLn(s"Putting ($key, $value)") *> concurrentLruCache.put(key, value)

}
JavaScript

The first test is for asserting that trying to create a ConcurrentLRUCache with a non-positive capacity would result in a failure.

The second test is for asserting that the cache works as expected, for that we use the TestConsole module provided by zio-test, for asserting that the expected messages are printed to the console. 

I won’t go into more details about how zio-test works, but you can read about it in the ZIO documentation page.

Summary

In this article, we’ve seen a concrete example of writing concurrent data structures with ZIO: a concurrent LRU cache.  Because ZIO is based on functional programming principles – such as using pure functions and immutable values – it was really easy and painless to evolve our initial implementation. This didn’t support concurrency and was based on ZIO Ref, to a fully concurrent version, based on ZIO STM, without all the complicated stuff that comes when using lower-level concurrency structures such as Locks, and with no deadlocks or race conditions at all.

In addition, this was just a very specific example of what you can do with ZIO STM for writing concurrent data structures, so there’s a lot more you can do with it in your own projects, in a totally async, non-blocking and thread-safe fashion. So make sure to give it a try!

References

You have probably encountered this problem while working with SBT and bigger projects. I’m talking about compilation times and test execution times, in other words, having to wait instead of working. Imagine working with a build tool that rebuilds only what is necessary, using a distributed cache, so if module A is built by one of your team members you won’t have to do it again. Or imagine being able to run builds of different parts of your project in parallel, or run tests only for the affected code that has been changed. Sounds promising right? That’s why, in this tutorial, I will be showing you what Bazel build is and how to set your project up in Scala.

Introduction to Bazel build


Bazel is a build tool from Google, which allows you to easily manage builds and tests in huge projects. This tool gives huge flexibility when it comes to the configuration and granularity of the basic build unit. It can be a set of packages, one package or even just one file. The basic build unit is called a target, the target is an instance of rules. A rule is a function that has a set of inputs and outputs; if the inputs do not change then the outputs stay the same. By having more targets (the disadvantage of this solution is having more build files to maintain) where not all of them depend on each other, more builds can run in parallel because Bazel build uses incremental builds, so it rebuilds only the part of the dependency graph that has been changed, as well as only running tests for the affected parts.

It can distribute, build and test actions across multiple machines, and then build and reuse previously done cached work, which makes your builds even more scalable.

Bazel can also print out a dependency graph, the results of which can be visualized on this page webgraphviz.com

So if your project takes a lot of time to build, and you don’t want to waste any more time, this tool is what you need. Speed up your compile times, speed up your tests, speed up your whole team’s work!

In this tutorial, we will be using Bazel version 1.0.0.

Project structure

We will be working on a project with this structure:
├── BUILD
├── WORKSPACE
├── bazeltest
│   ├── BUILD
│   └── src
│       ├── main
│       │ └── scala
│       │ └── bazeltest
│       │     └── Main.scala
│       └── test
│           └── scala
│               └── bazeltest
│                   └── MainSpec.scala
├── dependencies.yaml
└── othermodule
    ├── BUILD
    └── src
        ├── main
        │   └── scala
        │       └── othermodule
        │           └── Worker.scala
        └── test
            └── scala
                └── othermodule
                    └── WorkerSpec.scala

So we have two modules called: bazeltest and othermodule.
Bazeltest will depend on othermodule.

Workspace file setup

Each project has one WORKSPACE file, where we will define things like Scala version and dependencies. If in the project directory there is a  subdirectory with a WORKSPACE file, then while doing our builds this subdirectory will be omitted.
To make it work with Scala, then let’s take an already prepared boilerplate WORKSPACE file from:
https://github.com/bazelbuild/rules_scala#getting-started

Be aware of the change in rules_scala_version. Rules_scala_version is commit’s sha. So if you want to use the newest version of the rules, check GitHub repository and copy-paste commit’s sha.
We also have to add at the end of the file:
load(“//3rdparty:workspace.bzl”, “maven_dependencies”)
maven_dependencies()

This will be used by a third-party tool called bazel-deps, but we will come back to this at the next step.

So after the changes:
rules_scala_version=“0f89c210ade8f4320017daf718a61de3c1ac4773” # update this as needed

load(“@bazel_tools//tools/build_defs/repo:http.bzl”, “http_archive”)
http_archive(
    name = “io_bazel_rules_scala”,
    strip_prefix = “rules_scala-%s” % rules_scala_version,
   type = “zip”,
    url = “https://github.com/bazelbuild/rules_scala/archive/%s.zip” % rules_scala_version,
)

load(“@io_bazel_rules_scala//scala:toolchains.bzl”, “scala_register_toolchains”)
scala_register_toolchains()

load(“@io_bazel_rules_scala//scala:scala.bzl”, “scala_repositories”)
scala_repositories()

# bazel-skylib 0.8.0 released 2019.03.20 (https://github.com/bazelbuild/bazel-skylib/releases/tag/0.8.0)
skylib_version = “0.8.0”
http_archive(
    name = “bazel_skylib”,
   type = “tar.gz”,
    url = “https://github.com/bazelbuild/bazel-skylib/releases/download/{}/bazel-skylib.{}.tar.gz”.format (skylib_version, skylib_version),
    sha256 = “2ef429f5d7ce7111263289644d233707dba35e39696377ebab8b0bc701f7818e”,
)

load(“//3rdparty:workspace.bzl”, “maven_dependencies”)
maven_dependencies()

scala_repositories((
    “2.12.8”,
    {
      “scala_compiler”: “f34e9119f45abd41e85b9e121ba19dd9288b3b4af7f7047e86dc70236708d170”,
      “scala_library”: “321fb55685635c931eba4bc0d7668349da3f2c09aee2de93a70566066ff25c28”,
      “scala_reflect”: “4d6405395c4599ce04cea08ba082339e3e42135de9aae2923c9f5367e957315a”
    }
))
 


If you wish to set a specific Scala version, add code from: https://github.com/bazelbuild/rules_scala#getting-started
scala_repositories((

    "2.12.8",

    {

       "scala_compiler": "f34e9119f45abd41e85b9e121ba19dd9288b3b4af7f7047e86dc70236708d170",

       "scala_library": "321fb55685635c931eba4bc0d7668349da3f2c09aee2de93a70566066ff25c28",

       "scala_reflect": "4d6405395c4599ce04cea08ba082339e3e42135de9aae2923c9f5367e957315a"

    }

))

In this file, we will setup the Scala rules and everything else that is needed to compile the Scala project.

BUILD files setup


To write BUILD files we will use the following methods:
  1. load – which loads the Bazel Scala rules, and extensions
  2. scala_binary – generates a Scala executable
  3. scala_library –  generates a .jar file from Scala source files.
  4. scala_test – generates a Scala executable that runs unit test suites written using the scalatest library.

Start from the BUILD file in a project folder.
load("@io_bazel_rules_scala//scala:scala.bzl", "scala_binary")
scala_binary(
    name = "App",
    deps = [
        "//bazeltest"
    ],
    main_class = "bazeltest.Main"
)
  We have named it App, just one dependency to the bazeltest package. In deps, we list our dependencies, where our own modules or third party can be. Main_class is our entry point.

In the bazeltest package BUILD file:
load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library", "scala_test")
 
scala_library(
   name = "bazeltest",
   srcs = ["src/main/scala/bazeltest/Main.scala"],
   deps = [
       "//othermodule",
       "//3rdparty/jvm/joda_time:joda_time"
   ],
   visibility = ["//:__pkg__"]
)
 
scala_test(
    name = "test-main",
    srcs = ["src/test/scala/bazeltest/MainSpec.scala"],
    deps = [":bazeltest"]
)

Our Main.scala file will use some external third party dependency such as joda date time, and Worker from the subpack package. In srcs we set our Main.scala file, but it could be a list of files, listed one by one or a  matching path pattern for example:
glob(["src/main/scala/bazeltest/*.scala"]) 
( then we use glob ), could even be a package with all the subpackages, such as:
glob(["src/main/scala/bazeltest/**/*..scala"]) 
and in deps all the necessary dependencies, so for this example our own sub pack package plus third part joda date time. For now, it points to the 3rdparty folder which does not exist yet, this will be done at one of the next steps so don’t worry. Visibility is used to define which other targets can use this target as a dependency, in this example, we define a project folder containing the main BUILD file.
Now the BUILD file for othermodule:
load("@io_bazel_rules_scala//scala:scala.bzl", "scala_library", "scala_test")
 
scala_library(
     name = "othermodule",
     srcs = glob(["src/main/scala/othermodule/*.scala"]),
     deps = [],
     visibility = ["//bazeltest:__pkg__"]
)
 
scala_test(
    name = "test-othermodule",
    srcs = ["src/test/scala/othermodule/WorkerSpec.scala"],
    deps = [":othermodule"]
)
Here we have set up a visibility param to the bazeltest package. So only this package can read from this one. If other packages try to reach this, we will see an error.  

Dependencies

We will use a third-party tool for this: https://github.com/johnynek/bazel-deps
Open the dependencies.yaml file and put this there:
options:
 buildHeader: [
   "load(\"@io_bazel_rules_scala//scala:scala_import.bzl\", \"scala_import\")",
   "load(\"@io_bazel_rules_scala//scala:scala.bzl\", \"scala_library\", \"scala_binary\", \"scala_test\")"
 ]
 languages: [ "java", "scala:2.12.8" ]
 resolverType: "coursier"
 resolvers:
   - id: "mavencentral"
     type: "default"
     url: https://repo.maven.apache.org/maven2/
   - id: "hmrc"
     type: "default"
     url: https://hmrc.bintray.com/releases
 strictVisibility: true
 transitivity: runtime_deps
 versionConflictPolicy: highest
 
dependencies:
 joda-time:
   joda-time:
     lang: java
     version: "2.10.4"
 
 com.typesafe.scala-logging:
   scala-logging:
     lang: scala
     version: "3.9.0"
 
 com.typesafe.akka:
   akka-http:
     lang: scala
     version: "10.1.7"
 
 org.scalatest:
   scalatest:
     lang: scala
     version: "3.0.8"
 
replacements:
 org.scala-lang:
   scala-library:
     lang: scala/unmangled
     target: "@io_bazel_rules_scala_scala_library//:io_bazel_rules_scala_scala_library"
   scala-reflect:
     lang: scala/unmangled
     target: "@io_bazel_rules_scala_scala_reflect//:io_bazel_rules_scala_scala_reflect"
   scala-compiler:
     lang: scala/unmangled
     target: "@io_bazel_rules_scala_scala_compiler//:io_bazel_rules_scala_scala_compiler"
 
 org.scala-lang.modules:
   scala-parser-combinators:
     lang: scala
     target:
       "@io_bazel_rules_scala_scala_parser_combinators//:io_bazel_rules_scala_scala_parser_combinators"
   scala-xml:
     lang: scala
     target:
       “@io_bazel_rules_scala_scala_xml//:io_bazel_rules_scala_scala_xml"
(Language is always required and may be one of java, Scala, Scala/unmangled. This is important, if you define an invalid language then errors will occur. Replacements are used for internal targets instead of Maven ones.)  

Save the system variable of this project path, for example (working on a Mac): export MY_PROJ_DIR=`pwd`
We will need this in a minute.

  Clone https://github.com/johnynek/bazel-deps and enter the bazel-deps folder. Ensure that this tool uses the same rules_scala commit sha.
Open the WORKSPACE file inside the bazel-deps and look for this:
git_repository(

    name = "io_bazel_rules_scala",

    remote = "https://github.com/bazelbuild/rules_scala",

    commit = "0f89c210ade8f4320017daf718a61de3c1ac4773" # HEAD as of 2019-10-17, update this as needed

)
  Commit is of course what we need to change ( if it is different than in our WORKSPACE file in rules_scala_version ).

  bazel run //:parse generate -- --repo-root "$MY_PROJ_DIR" --sha-file 3rdparty/workspace.bzl --deps dependencies.yaml

  This will download dependencies into a 3rdparty folder into your project directory.
INFO: Analyzed target //:parse (0 packages loaded, 0 targets configured).

INFO: Found 1 target...

Target //src/scala/com/github/johnynek/bazel_deps:parseproject up-to-date:

  bazel-bin/src/scala/com/github/johnynek/bazel_deps/parseproject

  bazel-bin/src/scala/com/github/johnynek/bazel_deps/parseproject.jar

INFO: Elapsed time: 0.168s, Critical Path: 0.01s

INFO: 0 processes.

INFO: Build completed successfully, 1 total action

INFO: Build completed successfully, 1 total action

wrote 26 targets in 8 BUILD files

The first run

Before doing the first run, let’s implement our Main and Worker classes.
package bazeltest
 
import othermodule.Worker
import org.joda.time.DateTime
 
object Main extends App {
  println("IN MAIN now: "+DateTime.now().plusYears(11))
  val worker = new Worker
  worker.doSomething()
 
 
  def status(): String = "OKi"
}
package othermodule
 
class Worker {
 
  def doSomething() : Int = {
    println("Doing something")
    12345
  }
 
  def pureFunc(): String = "ABC"
 
}
bazel run //:App
INFO: Analyzed target //:App (1 packages loaded, 2 targets configured).

INFO: Found 1 target...

INFO: From Linking external/com_google_protobuf/libprotobuf_lite.a [for host]:

/Library/Developer/CommandLineTools/usr/bin/libtool: file: bazel-out/host/bin/external/com_google_protobuf/_objs/protobuf_lite/io_win32.o has no symbols

INFO: From Linking external/com_google_protobuf/libprotobuf.a [for host]:

/Library/Developer/CommandLineTools/usr/bin/libtool: file: bazel-out/host/bin/external/com_google_protobuf/_objs/protobuf/error_listener.o has no symbols

INFO: From Building external/com_google_protobuf/libprotobuf_java.jar (122 source files, 1 source jar):

warning: -parameters is not supported for target value 1.7. Use 1.8 or later.

Target //:App up-to-date:

  bazel-bin/App

  bazel-bin/App.jar

INFO: Elapsed time: 52.246s, Critical Path: 23.22s

INFO: 194 processes: 189 darwin-sandbox, 5 worker.

INFO: Build completed successfully, 198 total actions

INFO: Build completed successfully, 198 total actions

IN MAIN now: 2030-10-11T11:26:07.533+01:00

Doing something
The first run takes some time because it has to download the dependencies, so don’t worry.

Unit tests

Now let’s write some simple unit tests:
package bazeltest
import org.scalatest._
 
class MainSpec extends FlatSpec with Matchers {
 
  "status" should "return OK" in {
    Main.status() shouldBe "OKi"
  }
 
}
package othermodule
import org.scalatest._
 
class WorkerSpec extends FlatSpec with Matchers {
 
    val worker = new Worker()
    
      "do something" should "return 12345" in {
        worker.doSomething() shouldBe 12345
      }
    
      "pureFunc" should "return ABC" in {
        worker.pureFunc() shouldBe "ABC"
 

And run them: bazel test //bazeltest:test-main
INFO: Analyzed target //bazeltest:test-main (0 packages loaded, 0 targets configured).

INFO: Found 1 test target...

Target //bazeltest:test-main up-to-date:

  bazel-bin/bazeltest/test-main

  bazel-bin/bazeltest/test-main.jar

INFO: Elapsed time: 1.047s, Critical Path: 0.89s

INFO: 3 processes: 2 darwin-sandbox, 1 worker.

INFO: Build completed successfully, 4 total actions

//bazeltest:test-main                                                    PASSED in 0.5s

 

Executed 1 out of 1 test: 1 test passes.

INFO: Build completed successfully, 4 total actions
bazel test //othermodule:test-othermodule

INFO: Analyzed target //othermodule:test-othermodule (0 packages loaded, 0 targets configured).

INFO: Found 1 test target...

Target //othermodule:test-othermodule up-to-date:

  bazel-bin/othermodule/test-othermodule

  bazel-bin/othermodule/test-othermodule.jar

INFO: Elapsed time: 1.438s, Critical Path: 1.29s

INFO: 2 processes: 1 darwin-sandbox, 1 worker.

INFO: Build completed successfully, 3 total actions

//othermodule:test-othermodule                                           PASSED in 0.6s

 

Executed 1 out of 1 test: 1 test passes.

INFO: Build completed successfully, 3 total actions
Try now to change the status method from Main, to return “OK” instead of “OKi”. Run the tests again: bazel test //bazeltest:test-main
INFO: Analyzed target //bazeltest:test-main (0 packages loaded, 0 targets configured).

INFO: Found 1 test target...

FAIL: //bazeltest:test-main (see /private/var/tmp/_bazel_maciejbak/16727409c9f0575889b09993f53ce424/execroot/__main__/bazel-out/darwin-fastbuild/testlogs/bazeltest/test-main/test.log)

Target //bazeltest:test-main up-to-date:

  bazel-bin/bazeltest/test-main

  bazel-bin/bazeltest/test-main.jar

INFO: Elapsed time: 1.114s, Critical Path: 0.96s

INFO: 3 processes: 2 darwin-sandbox, 1 worker.

INFO: Build completed, 1 test FAILED, 4 total actions

//bazeltest:test-main                                                    FAILED in 0.6s

  /private/var/tmp/_bazel_maciejbak/16727409c9f0575889b09993f53ce424/execroot/__main__/bazel-out/darwin-fastbuild/testlogs/bazeltest/test-main/test.log

 

INFO: Build completed, 1 test FAILED, 4 total actions
bazel test //othermodule:test-othermodule
INFO: Analyzed target //othermodule:test-othermodule (0 packages loaded, 0 targets configured).

INFO: Found 1 test target...

Target //othermodule:test-othermodule up-to-date:

  bazel-bin/othermodule/test-othermodule

  bazel-bin/othermodule/test-othermodule.jar

INFO: Elapsed time: 0.150s, Critical Path: 0.00s

INFO: 0 processes.

INFO: Build completed successfully, 1 total action

//othermodule:test-othermodule                                  (cached) PASSED in 0.6s

 

Executed 0 out of 1 test: 1 test passes.

INFO: Build completed successfully, 1 total action
Bazel build sees what has been changed, and runs tests only for the affected classes. So test results for othermodule are taken from the cache, and only the main tests run. The test failed because we didn’t change the results in the Spec file, so the change expected the result in the test to the Main.status() shouldBe “OK”. Run tests again: bazel test //bazeltest:test-main
INFO: Analyzed target //bazeltest:test-main (0 packages loaded, 0 targets configured).

INFO: Found 1 test target...

Target //bazeltest:test-main up-to-date:

  bazel-bin/bazeltest/test-main

  bazel-bin/bazeltest/test-main.jar

INFO: Elapsed time: 1.389s, Critical Path: 1.22s

INFO: 2 processes: 1 darwin-sandbox, 1 worker.

INFO: Build completed successfully, 3 total actions

//bazeltest:test-main                                                    PASSED in 0.6s

 

Executed 1 out of 1 test: 1 test passes.

INFO: Build completed successfully, 3 total actions

Dependency graph

We can easily visualize our dependency graph: In the command line run: bazel query --noimplicit_deps "deps(//:App)" --output graph
digraph mygraph {

  node [shape=box];

  "//:App"

  "//:App" -> "//bazeltest:bazeltest"

  "//bazeltest:bazeltest"

  "//bazeltest:bazeltest" -> "//bazeltest:src/main/scala/bazeltest/Main.scala"

  "//bazeltest:bazeltest" -> "//3rdparty/jvm/joda_time:joda_time"

  "//bazeltest:bazeltest" -> "//othermodule:othermodule"

  "//othermodule:othermodule"

  "//othermodule:othermodule" -> "//othermodule:src/main/scala/othermodule/Worker.scala"

  "//othermodule:src/main/scala/othermodule/Worker.scala"

  "//3rdparty/jvm/joda_time:joda_time"

  "//3rdparty/jvm/joda_time:joda_time" -> "//external:jar/joda_time/joda_time"

  "//external:jar/joda_time/joda_time"

  "//external:jar/joda_time/joda_time" -> "@joda_time_joda_time//jar:jar"

  "//bazeltest:src/main/scala/bazeltest/Main.scala"

  "@joda_time_joda_time//jar:jar"

  "@joda_time_joda_time//jar:jar" -> "@joda_time_joda_time//jar:joda_time_joda_time.jar\n@joda_time_joda_time//jar:joda_time_joda_time-sources.jar"

  "@joda_time_joda_time//jar:joda_time_joda_time.jar\n@joda_time_joda_time//jar:joda_time_joda_time-sources.jar"

}

Loading: 12 packages loaded

Paste results to webgraphviz.com Bazel build Scala graph

Generate jar

bazel build //:App
INFO: Analyzed target //:App (0 packages loaded, 0 targets configured).

INFO: Found 1 target...

Target //:App up-to-date:

  bazel-bin/App

  bazel-bin/App.jar

INFO: Elapsed time: 0.085s, Critical Path: 0.00s

INFO: 0 processes.

INFO: Build completed successfully, 1 total action

Bazel build: Summary

In this post, we showed what is bazel, when to use it, and how to make basic configuration. It can take some time to properly set up complex projects using bazel build, but I guarantee you, in the end, it will speed up the whole team’s work.


Useful links

  1. Official Bazel build documentation https://docs.bazel.build/versions/1.0.0/bazel-overview.html
  2. Building Scala with Bazel build- Natan Silnitsky https://www.youtube.com/watch?v=K2Ytk0S4PF0
  3. Building Java Applications with Bazel https://www.baeldung.com/bazel-build-tool

There are plenty of frameworks you can base your application on in Scala, and every one offers a different flavor of the language with its own set of patterns and solutions. Whatever your preference is, we all ultimately want the same: simple and powerful tools enabling us to write easily testable and reliable applications. A  new library has recently joined the competition. ZIO, with its first stable release coming soon, gives you a high-performance functional programming toolbox and lowers the entry barrier for beginners by dropping unnecessary jargon. In this blog post, you will learn how to structure a modular application using ZIO.

Designing a Tic-Tac-Toe game

Most command-line programs are stateless and rightfully so, as they can be easily integrated into scripts and chained via shell pipes. However, for this article, we need a slightly more complicated domain. So let’s write a Tic-Tac-Toe game. It will make the example more entertaining while still keeping it relatively simple to follow. Firstly, a few assumptions about our game. It will be a command-line application, so the game will be rendered into the console and the user will interact with it via text commands. The application will be divided into several modes, where a mode is defined by its state and a list of commands available to the user. Our program will read from the console, modify the state accordingly and write to the console in a loop. We’d also like to clear the console before each frame. For each of these concerns we will create a separate module with dependencies on other modules as depicted below:

TicTacToe game ZIO

Basic program

The basic building block of ZIO applications is the  ZIO[R, E, A] type, which describes effective computation, where:

  •  R is the type of environment required to run the effect
  •  E is the type of error that may be produced by the effect
  •  A is the type of value that may be produced by the effect

ZIO was designed around the idea of programming to an interface. Our application can be divided into smaller modules and any dependencies are expressed as constraints for the environment type R. First of all, we have to add the dependency on ZIO to SBT build:

libraryDependencies += "dev.zio" %% "zio" % "1.0.0-RC16"

We will start with a simple program printing the “TicTacToe game!” and gradually expand it.

package ioleo.tictactoe

import zio.{console, App , ZEnv, ZIO}
import zio.console.Console

object TicTacToe extends App {

  val program: ZIO[Console, Nothing, Unit] =
    console.putStrLn("TicTacToe game!")

  def run(args: List[String]): ZIO[ZEnv, Nothing, Int] =
    program.foldM(
        error => console.putStrLn(s"Execution failed with: $error") *> ZIO.succeed(1)
      , _ => ZIO.succeed(0)
    )
}

To make our lives easier ZIO provides the  App trait. All we need to do is to implement the run method. In our case, we can ignore the arguments the program is run with and return a simple program printing to the console. The program will be run in DefaultRuntime which provides the default environment with Blocking, Clock, Console, Random and System services. We can run this program using SBT: sbt tictactoe/runMain ioleo.tictactoe.TicTacToe.

Testing effects

ZIO also provides its own testing framework with features such as composable assertions, precise failure reporting, out-of-the-box support for effects and lightweight mocking framework (without reflection). First of all, we have to add the required dependencies and configuration to our SBT build:

libraryDependencies ++= Seq(
  "dev.zio" %% "zio-test" % "1.0.0-RC16" % "test",
  "dev.zio" %% "zio-test-sbt" % "1.0.0-RC16" % "test"
),

testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))

Now, we can define our first specification.

package ioleo.tictactoe

import zio.test.{assert, suite, testM, DefaultRunnableSpec}
import zio.test.environment.TestConsole
import zio.test.Assertion.equalTo

object TicTacToeSpec extends DefaultRunnableSpec(

  suite("TicTacToe")(
   testM("prints to console") {
     for {
       test <- TestConsole.makeTest(TestConsole.DefaultData)
       _ <- TicTacToe.program.provide(new TestConsole {
         val console = test
       })
       out  <- test.output
     } yield assert(out, equalTo(Vector("TicTacToe game!\n")))
   }
  )
)

In this example, we’re using the TestConsole implementation, which instead of interacting with the real console, stores the output in a vector, which we can access later and make assertions on. Available assertions can be found in the Assertion companion object. For more information on how to use test implementations, see the Testing effects doc.

Building the program bottom-up

One of the core design goals of ZIO is composability. It allows us to build simple programs solving smaller problems and combine them into larger programs. The so-called “bottom-up” approach is nothing new – it has been the backbone of many successful implementations in the aviation industry. It is simply cheaper, faster and easier to test and study small components in isolation and then, based on their well-known properties, assemble them into more complicated devices. The same applies to software engineering. When we start our application, we will land in MenuMode. Let’s define some possible commands for this mode:

package ioleo.tictactoe.domain

sealed trait MenuCommand

object MenuCommand {
  case object NewGame extends MenuCommand
  case object Resume  extends MenuCommand
  case object Quit extends MenuCommand
  case object Invalid extends MenuCommand
}

Next up, we will define our first module, MenuCommandParser which will be responsible for translating the user input into our domain model.

package ioleo.tictactoe.parser

import ioleo.tictactoe.domain.MenuCommand
import zio.ZIO

import zio.macros.annotation.{accessible, mockable}

@accessible(">")
@mockable
trait MenuCommandParser {
  val menuCommandParser: MenuCommandParser.Service[Any]
}

object MenuCommandParser {
  trait Service[R] {
    def parse(input: String): ZIO[R, Nothing, MenuCommand]
  }
}

This follows the Module pattern which I explain in more detail on the Use module pattern page in ZIO docs. The  MenuCommandParser is the module, which is just a container for the  MenuCommandParser.Service .

Note: By convention we name the value holding the reference to the same service name as the module, only with first letter lowercased. This is to avoid name collisions when mixing multiple modules to create the environment.

The service is just an ordinary interface, defining the capabilities it provides.

Note: By convention we place the service inside the companion object of the module and name it  Service . This is to have a consistent naming scheme  <Module>.Service[R] across the entire application. It is also the structure required by some macros in the zio-macros project.

The capability is a ZIO effect defined by the service. For these may be ordinary functions, if you want all the benefits ZIO provides, these should all be ZIO effects. You may also have noticed I annotated the module with  @accessible and  @mockable . I will expand on that later. For now, all you need to know is that they generate some boilerplate code which will be useful for testing. Note that to use them we need to add the dependency in SBT build:

libraryDependencies ++= Seq(
  "dev.zio" %% "zio-macros-core" % "0.5.0",
  "dev.zio" %% "zio-macros-test" % "0.5.0"
)

Next, we can define our  Live implementation as follows:

package ioleo.tictactoe.parser

import ioleo.tictactoe.domain.MenuCommand
import zio.UIO

trait MenuCommandParserLive extends MenuCommandParser {
  val menuCommandParser = new MenuCommandParser.Service[Any] {
    def parse(input: String): UIO[MenuCommand] = ???
  }
}

Though the implementation seems trivial, we will follow Test Driven Development and first, declare the desired behavior in terms of a runnable specification.

package ioleo.tictactoe.parser

import ioleo.tictactoe.domain.MenuCommand
import zio.test.{assertM, checkM, suite, testM, DefaultRunnableSpec, Gen}
import zio.test.Assertion.equalTo
import MenuCommandParserSpecUtils._

object MenuCommandParserSpec extends DefaultRunnableSpec(
    suite("MenuCommandParser")(
        suite("parse")(
            testM("`new game` returns NewGame command") {
              checkParse("new game", MenuCommand.NewGame)
            }
          , testM("`resume` returns Resume command") {
              checkParse("resume", MenuCommand.Resume)
            }
          , testM("`quit` returns Quit command") {
              checkParse("quit", MenuCommand.Quit)
            }
          , testM("any other input returns Invalid command") {
              checkM(invalidCommandsGen) { input =>
                checkParse(input, MenuCommand.Invalid)
              }
            }
        )
    )
)

object MenuCommandParserSpecUtils {

  val validCommands =
    List("new game", "resume", "quit")

  val invalidCommandsGen =
    Gen.anyString.filter(str => !validCommands.contains(str))

  def checkParse(input: String, command: MenuCommand) = {
    val app = MenuCommandParser.>.parse(input)
    val env = new MenuCommandParserLive {}
    val result = app.provide(env)

    assertM(result, equalTo(command))
  }
}

The  suite is just a named container for one or more tests. Each test must end with a single assertion, though assertions may be combined with  && and  || operators (boolean logic). The first three tests are straightforward input/output checks. The last test is more interesting. We’ve derived a custom invalid command generator from a predefined  Gen.anyString , and we’re using it to generate random inputs to prove that all other inputs will yield  MenuCommand.Invalid . This style is called Property-based testing and it boils down to generating and testing enough random samples from the domain to be confident that our implementation has the property of always yielding the desired result. This is useful when we can’t possibly cover the whole space of inputs with tests, as it is too big (possibly infinite) or too expensive computationally.

Access helper

In the test suite, we are referring directly to parse capability via the  MenuCommandParser.>.parse . This is possible thanks to the  @accessible macro we mentioned before. What it does underneath is to generate the helper object named  > placed within module‘s companion object with implementation delegating the calls on capabilities to the environment.

object > extends MenuCommandParser.Service[MenuCommandParser] {

  def parse(input: String) =
    ZIO.accessM(_.menuCommandParser.parse(input))
}

With our tests in place, we can go back and finish our implementation.

def parse(input: String): UIO[MenuCommand] =
  UIO.succeed(input) map {
    case "new game" => MenuCommand.NewGame
    case "resume"   => MenuCommand.Resume
    case "quit" => MenuCommand.Quit
    case _      => MenuCommand.Invalid
  }

Lifting pure functions into the effect system

You will have noticed that parse represents the effect that wraps a pure function. There are some functional programmers who would not lift this function into the effect system, to keep a clear distinction between pure functions and effects in your codebase. However, this requires a very disciplined and highly skilled team and the benefits are debatable. While this function by itself does not need to be declared as effectful, by making it so we make it dead simple to mock out when testing other modules that collaborate with this one. It is also much easier to design applications incrementally, by building up smaller effects and combining them into larger ones as necessary, without the burden of isolating side effects. This will be particularly appealing to programmers used to an imperative programming style.

Combining modules into a larger application

In this same fashion, we can implement parsers and renderers for all modes. At this point, all of the basic stuff is handled and properly tested. We can use these as building blocks for higher-level modules. We will explore this by implementing the  Terminal module. This module handles all of the input/output operations. ZIO already provides the  Console module for this, but we’ve now got additional requirements. Firstly, we assume getting input from the console never fails, because, well if it does, we’re simply going to crash the application, and we don’t really want to have to deal with that. Secondly, we want to clear the console before outputting the next frame.

package ioleo.tictactoe.cli

import zio.ZIO
import zio.macros.annotation.{accessible, mockable}

@accessible(">")
@mockable
trait Terminal {
  val terminal: Terminal.Service[Any]
}

object Terminal {
  trait Service[R] {
    val getUserInput: ZIO[R, Nothing, String]
    def display(frame: String): ZIO[R, Nothing, Unit]
  }
}

However, we don’t want to reinvent the wheel. So we are going to reuse the built-in  Console service in our  TerminalLive implementation.

package ioleo.tictactoe.cli

import zio.console.Console

trait TerminalLive extends Terminal {

  val console: Console.Service[Any]

  final val terminal = new Terminal.Service[Any] {
    val getUserInput =
      console.getStrLn.orDie

    def display(frame: String) =
      for {
        _ <- console.putStr(TerminalLive.ANSI_CLEARSCREEN)
        _ <- console.putStrLn(frame)
      } yield ()
  }
}

object TerminalLive {
  val ANSI_CLEARSCREEN: String = "\u001b[H\u001b[2J"
}

We’ve defined the dependency by adding an abstract value of type  Console.Service[Any] , which the compiler will require us to provide when we construct the environment that uses the  TerminalLive implementation. Note that here again, we rely on convention, we’re expecting the service to be held in a variable named after the module. The implementation is dead simple, but the question is… how do we test this? We could use the  TestConsole and indirectly test the behavior, but this is brittle and does not express our intent very well in the specification. This is where the ZIO Mock framework comes in. The basic idea is to express our expectations for the collaborating service and finally build a mock implementation of this service, which will check at runtime that our assumptions hold true.

package ioleo.tictactoe.cli

import zio.Managed
import zio.test.{assertM, checkM, suite, testM, DefaultRunnableSpec, Gen}
import zio.test.Assertion.equalTo
import zio.test.mock.Expectation.value
import zio.test.mock.MockConsole
import TerminalSpecUtils._

object TerminalSpec extends DefaultRunnableSpec(
    suite("Terminal")(
        suite("getUserInput")(
            testM("delegates to Console") {
              checkM(Gen.anyString) { input =>
                val app  = Terminal.>.getUserInput
                val mock = MockConsole.getStrLn returns value(input)
                val env  = makeEnv(mock)
                val result = app.provideManaged(env)
                assertM(result, equalTo(input))
              }
           }
        )
    )
)

object TerminalSpecUtils {
  def makeEnv(consoleEnv: Managed[Nothing, MockConsole]): Managed[Nothing, TerminalLive] =
    consoleEnv.map(c => new TerminalLive {
      val console = c.console
    })
}

There is a lot going on behind the scenes here, so let’s break it down, bit by bit. The basic specification structure remains the same. We’re using the helper generated by the  @accessible macro to reference the  getUserInput capability. Next, we’re constructing an environment that we’ll use to run it. Since we’re testing the  TerminalLive implementation, we need to provide the  val console: Console.Service[Any] . To construct the mock implementation, we express our expectations using the  MockConsole capability tags. In this case, we have a single expectation that  MockConsole.getStrLn returns the predefined string. If we had multiple expectations, we could combine them using flatMap:

import zio.test.mock.Expectation.{unit, value}

val mock: Managed[Nothing, MockConsole] = (
  (MockConsole.getStrLn returns value("first")) *>
  (MockConsole.getStrLn returns value("second")) *>
  (MockConsole.putStrLn(equalTo("first & second")) returns unit)
)

To refer to a specific method we’re using capability tags, which are simple objects extending  zio.test.mock.Method[M, A, B] where M is the module the method belongs to, A is the type of input arguments and B the type of output value. If the method takes arguments, we have to pass an assertion. Next, we use the returns method and one of the helpers defined in zio.test.mock.Expectation to provide the mocked result. The monadic nature of Expectation allows you to sequence expectations and combine them into one, but the actual construction of mock implementation is handled by a conditional implicit conversion Expectation[M, E, A] => Managed[Nothing, M] , for which you need a Mockable[M] in scope. This is where the @mockable macro comes in handy. Without it you would have to write all of this boilerplate machinery:

import zio.test.mock.{Method, Mock, Mockable}

object MockConsole {

  // ...
  object putStr   extends Method[MockConsole, String, Unit]
  object putStrLn extends Method[MockConsole, String, Unit]
  object getStrLn extends Method[MockConsole, Unit, String]

  implicit val mockable: Mockable[MockConsole] = (mock: Mock) =>
    new MockConsole {
      val console = new Service[Any] {
        def putStr(line: String): UIO[Unit]   = mock(Service.putStr, line)
        def putStrLn(line: String): UIO[Unit] = mock(Service.putStrLn, line)
        val getStrLn: IO[IOException, String] = mock(Service.getStrLn)
      }
    }
}

The final program

You’ve learned how to create and test programs using ZIO and then compose them into larger programs. You’ve got all of your parts in place and it’s time to run the game. We’ve started with a simple program printing to the console. Now let’s modify it to run our program in a loop.

package ioleo.tictactoe

import ioleo.tictactoe.app.RunLoop
import ioleo.tictactoe.domain.{ConfirmAction, ConfirmMessage, MenuMessage, State}
import zio.{Managed, ZIO}
import zio.clock.Clock
import zio.duration._
import zio.test.{assertM, suite, testM, DefaultRunnableSpec}
import zio.test.Assertion.{equalTo, isRight, isSome, isUnit}
import zio.test.mock.Expectation.{failure, value}
import TicTacToeSpecUtils._

object TicTacToeSpec extends DefaultRunnableSpec(
    suite("TicTacToe")(
        suite("program")(
            testM("repeats RunLoop.step until interrupted by Unit error") {
              val app  = TicTacToe.program
              val mock = (
                (RunLoop.step(equalTo(state0)) returns value(state1) *>
                (RunLoop.step(equalTo(state1)) returns value(state2) *>
                (RunLoop.step(equalTo(state2)) returns value(state3) *>
                (RunLoop.step(equalTo(state3)) returns failure(()))
              )
              val result = app.either.provideManaged(mock).timeout(500.millis).provide(Clock.Live)
              assertM(result, isSome(isRight(isUnit)))
            }
        )
    )
)

object TicTacToeSpecUtils {
  val state0 = State.default
  val state1 = State.Menu(None, MenuMessage.InvalidCommand)
  val state2 = State.Confirm(ConfirmAction.Quit, state0, state1, ConfirmMessage.Empty)
  val state3 = State.Confirm(ConfirmAction.Quit, state0, state1, ConfirmMessage.InvalidCommand)
}

And change the implementation to call our RunLoop service:

package ioleo.tictactoe

import ioleo.tictactoe.domain.State
import zio.{console, App, UIO, ZIO}

object TicTacToe extends App {
  val program = {
    def loop(state: State): ZIO[app.RunLoop, Nothing, Unit] =
      app.RunLoop.>.step(state).foldM(
          _         => UIO.unit
        , nextState => loop(nextState)
      )

    loop(State.default)
  }

  def run(args: List[String]): ZIO[Environment, Nothing, Int] =
    for {
      env <- prepareEnvironment
      out <- program.provide(env).foldM(
          error => console.putStrLn(s"Execution failed with: $error") *> UIO.succeed(1)
        , _     => UIO.succeed(0)
      )
    } yield out

  private val prepareEnvironment =
    UIO.succeed(
      new app.ControllerLive
        with app.RunLoopLive
        with cli.TerminalLive
        with logic.GameLogicLive
        with logic.OpponentAiLive
        with mode.ConfirmModeLive
        with mode.GameModeLive
        with mode.MenuModeLive
        with parser.ConfirmCommandParserLive
        with parser.GameCommandParserLive
        with parser.MenuCommandParserLive
        with view.ConfirmViewLive
        with view.GameViewLive
        with view.MenuViewLive
        with zio.console.Console.Live
        with zio.random.Random.Live {}
    )
}

I’ve skipped the details of many services, you can look up the finished code in the ioleo/zio-by-example repository. We don’t have to explicitly state the full environment type for our program. It only requires the  RunLoop , but as soon as we provide  RunLoopLive , the compiler will require that we provide  Terminal and  Controller services. When we provide the Live implementations of those, they, in turn, add further dependencies of their own. This way we build our final environment incrementally with the generous help of the Scala compiler, which will output readable and accurate errors if we forget to provide any required service.

Summary

In this blog entry, we’ve looked at how to build a modular command-line application using ZIO. We’ve also covered basic testing using the ZIO Test framework and mocking framework. However, this is just the tip of the iceberg. ZIO is much more powerful and we have not yet touched the powerful utilities for the asynchronous and concurrent programming it provides. To run the TicTacToe game, clone the ioleo/zio-by-example repository and run  sbt tictactoe/run . Have fun!

Check out more articles about ZIO on our blog:

Scale fast with Scalac – Scala development company ready to solve all your challenges.

Queueing and messaging platforms have been gaining in popularity in recent years. They solve numerous problems based on asynchronous message passing or consumer and producer patterns. In this blog post, we’re going to build a basic message broker functionality with ZIO for our internal clinic messaging system, specifically with ZIO Queues and ZIO Fibers.

In our clinic, we have x-ray rooms which produce x-ray photographs of hips and knees, which are sent via a messaging system. For any given body part, some physicians can perform a photographic analysis. Additionally, we want to be able to perform message logging for selected body parts.

This example accurately describes a message broker with topics: sending messages to defined topics, subscribing to them in two ways – the ‘one message one consumer’ type pattern and the multicast type pattern. We will be performing this subscribing via consumer groups to which consumers subscribe within any particular topic.

Each topic’s message is delivered to every consumer group (like multicast), but within each group, only one consumer can digest the message (like producers and consumers). Here’s an image showing this:

ZIO Fibers ZIO Queues Message broker

Of course, there are plenty of distributed platforms that can achieve this, e.g. RabbitMQ provides us with a so-called exchange – a broker between a producer and queues that decides which queues to send the message to. Broadcast is supplied via a funout exchange, as opposed to direct and topic exchange types which require a match to the message’s topic.

So let’s try to implement this concept one more time, but this time with ZIO Queues and ZIO Fibers in an effectful way.

ZIO Queues & ZIO Fibers

But first things first – let’s briefly introduce Fibers and Queues in ZIO.

So Fibers are data types for expressing concurrent computations. Fibers are loosely related to threads – a single Fiber can be executed on multiple threads by shifting between them – all with full resource safety!

What makes Fibers stronger is the seamless setting in ZIO. Having some effect e.g. UIO("work") we only need to call .fork on it to make it run on Fiber. Then it’s up to us what to do next: interruptstop Fiber by force, join – block current Fiber until it returns the result or races with another Fiber – runs two ZIO Fibers and returns the first that succeeded.

I should mention that the underlying implementation of race is done via raceWith – a powerful method that allows you to provide any logic for managing two separate Fibers. raceWith is used not only in race but also zipPar – for running two Fibers in parallel and returning both results as a tuple.

On the other hand, Queues in ZIO addresses issues that we can encounter while using BlockingQueue. The effectful, back-pressured ZIO Queue makes it easy to avoid blocked threads on Queues core operations such as offer and take.

Apart from a bounded back-pressured queue, ZIO Queues delivers other overflow behaviors such as sliding – for removing the last inserted element, or dropping – for discarding the newly received elements. All this in a non-blocking manner.

So the moment we use queue.offer(sth).fork on a filled back-pressured queue, we are sure that running a separate fiber will make it non-blocking for the main one. Other ZIO Queue assets are interruption (as fibers are) and safe shutdown.

Domain

We’ll start with defining our domain and request class with a topic field.

Additionally, we will implement RequestGenerator for generating Requests:

sealed trait Diagnostic

case object HipDiagnostic extends Diagnostic

case object KneeDiagnostic extends Diagnostic

case class Request[A](topic: Diagnostic, XRayImage: A)

trait RequestGenerator[R, A] {
  def generate(topic: Diagnostic): URIO[R, Request[A]]
} 

Imports required by our project:

import zio._
import zio.random._
import zio.console._
import zio.duration._

For the sake of simplicity let’s assume our x-ray images are simply Ints:

case class IntRequestGenerator() extends RequestGenerator[Random, Int] {
  override def generate(topic: Diagnostic): URIO[Random, Request[Int]] =
    nextIntBounded(1000) >>= (n => UIO(Request(topic, n)))
}

Before getting started with the first part, let’s take a look at the architecture diagram. It might look strange at first so let’s leave it this way for now:

`ZIO Fibers ZIO Queues architecture diagram

Consumer

The first component of our system is a Consumer[A]. Here we are providing two API methods – create for constructing a consumer wrapped in UIO and run that starts a new fiber that continuously waits for elements in its queue to process. The processing is rather dull but following console logs are definitely not!

It’s worth stressing that run returns (Queue, Fiber) in effect so apart from connecting the consumer to the system we can also interrupt or join the customer:

case class Consumer[A](title: String) {
  def run = for {
    queue <- Queue.bounded[A](10)
    loop = for {
      img  <- queue.take
      _    <- putStrLn(s"[$title] worker: Starting analyzing task $img")
      rand <- nextIntBounded(4)
      _    <- ZIO.sleep(rand.seconds)
      _    <- putStrLn(s"[$title] worker: Finished task $img")
    } yield ()
    fiber <- loop.forever.fork
  } yield (queue, fiber)
}

object Consumer {
  def create[A](title: String) = UIO(Consumer[A](title))
}

As we are more used to an imperative approach, let's focus for a moment on the advantages of using ZIO effects here.

Any potentially dangerous side effects here are kept inside the ZIO monad. This makes a unit println method more substantial and, referentially transparent. Also, having a physical grasp on everything is really beneficial when it comes to parallelism.

Here, we were able to build an arbitrary chain of computations and make it run forever on a separate ZIO Fiber with a pleasing .forever.fork.

Topic Queue

TopicQueue is kind of the most complicated part. It's in charge of the proper distribution of messages among subscribers. The subscribe method receives a subscriber's queue and the consumerGroup number. As you will no doubt recall, each message is passed to each consumerGroup and then to a random subscriber within each group. The run method follows the pattern from previous components - a continuous loop of acquiring messages and distributing them within the described scheme:

case class TopicQueue[A](queue: Queue[A], subscribers: Ref[Map[Int, List[Queue[A]]]]) {
  def subscribe(sub: Queue[A], consumerGroup: Int): UIO[Unit] =
    subscribers.update { map =>
      map.get(consumerGroup) match {
        case Some(value) =>
          map + (consumerGroup -> (value :+ sub))
        case None =>
          map + (consumerGroup -> List(sub))
      }
    }

  private val loop =
    for {
      elem <- queue.take
      subs <- subscribers.get
      _    <- ZIO.foreach(subs.values) { group =>
        for {
          idx <- nextIntBounded(group.length)
          _   <- group(idx).offer(elem)
        } yield ()
      }
    } yield ()

  def run = loop.forever.fork
}

object TopicQueue {
  def create[A](queue: Queue[A]): UIO[TopicQueue[A]] =
    Ref.make(Map.empty[Int, List[Queue[A]]]) >>= (map => UIO(TopicQueue(queue, map)))
}

In this part, immutability is what strikes us first. No explicit, side-effect modifications of a subscribers map can occur without our knowledge. Here we're using Ref from ZIO to store the map and perform updates.

It's worth mentioning that wrapping the constructor method in UIO is essential for consistency, as instantiating a new ZIO Queue should always be a part of our effect chain.

Exchange

Our Exchange is pretty similar to the RabbitMQ exchange. The constructor simply creates three queues - the input queue for incoming jobs (jobQueue) and two output queues for routing (queueHip and queueKnee). What our exchange is also doing is unwrapping XRayImage from Request:

case class Exchange[A]() {
  def run = for {
    jobQueue       <- Queue.bounded[Request[A]](10)
    queueHip       <- Queue.bounded[A](10)
    queueKnee      <- Queue.bounded[A](10)
    hipTopicQueue  <- TopicQueue.create(queueHip)
    kneeTopicQueue <- TopicQueue.create(queueKnee)
    loop = for {
      job <- jobQueue.take
      _   <- job.topic match {
        case HipDiagnostic =>
          queueHip.offer(job.XRayImage)
        case KneeDiagnostic =>
          queueKnee.offer(job.XRayImage)
      }
    } yield ()
    fiber <- loop.forever.fork
  } yield (jobQueue, hipTopicQueue, kneeTopicQueue, fiber)
}

object Exchange {
  def create[A] = UIO(Exchange[A]())
}

Producer

Producing is simply done by supplying a provided queue with Requests. You might have noticed that the run method follows some patterns. Building asynchronous computations with self-explanatory schedules and a lazy execution is easy:

case class Producer[R, A](queue: Queue[Request[A]], generator: RequestGenerator[R, A]) {
  def run = {
    val loop = for {
      _    <- putStrLn("[XRayRoom] generating hip and knee request")
      hip  <- generator.generate(HipDiagnostic)
      _    <- queue.offer(hip)
      knee <- generator.generate(KneeDiagnostic)
      _    <- queue.offer(knee)
      _    <- ZIO.sleep(2.seconds)
    } yield ()
    loop.forever.fork
  }
}

object Producer {
  def create[R, A](queue: Queue[Request[A]], generator: RequestGenerator[R, A]) = UIO(Producer(queue, generator))
}

Program

Finally, the Program. Now we will combine all the previous components to assemble a fully operational clinic messaging system. First, we instantiate Consumers and launch them (reminder: ZIO Fibers are lazy, unlike Futures). Then it’s time for Exchange and Producer. Notice that returning tuples gives a  possibility to ignore the fibers that we don't need. Finally, we subscribe Consumers for the output queues and, importantly, define the ConsumerGroup with the launch:

val program = for {

  physicianHip             <- Consumer.create[Int]("Hip")
  ctxPhHip                 <- physicianHip.run
  (phHipQueue, phHipFiber) = ctxPhHip

  loggerHip           <- Consumer.create[Int]("HIP_LOGGER")
  ctxLoggerHip        <- loggerHip.run
  (loggerHipQueue, _) = ctxLoggerHip

  physicianKnee    <- Consumer.create[Int]("Knee1")
  ctxPhKnee        <- physicianKnee.run
  (phKneeQueue, _) = ctxPhKnee

  physicianKnee2    <- Consumer.create[Int]("Knee2")
  ctxPhKnee2        <- physicianKnee2.run
  (phKneeQueue2, _) = ctxPhKnee2


  exchange                                         <- Exchange.create[Int]
  ctxExchange                                      <- exchange.run
  (inputQueue, outputQueueHip, outputQueueKnee, _) = ctxExchange


  generator = IntRequestGenerator()
  xRayRoom  <- Producer.create(inputQueue, generator)
  _         <- xRayRoom.run


  _ <- outputQueueHip.subscribe(phHipQueue, consumerGroup = 1)
  _ <- outputQueueHip.subscribe(loggerHipQueue, consumerGroup = 2)

  _ <- outputQueueKnee.subscribe(phKneeQueue, consumerGroup = 1)
  _ <- outputQueueKnee.subscribe(phKneeQueue2, consumerGroup = 1)

  _ <- outputQueueHip.run
  _ <- outputQueueKnee.run

  _ <- phHipFiber.join

} yield ()

Also after launching TopicQueues with run, we can still subscribe to them.

Running the program

Phew... that was a lot, let's put it into the ZIO application and run it:

object Main extends App {
  override def run(args: List[String]) = program.as(0)
}

Looking into the logs we see that:

1. Multicast for all the ConsumerGroups within the hip topic works as expected - hip physician and HIP_LOGGER receive the same messages.

2. Within a single ConsumerGroup the messages are routed in a random manner (definitely field for improvement!):

[XRayRoom] generating hip and knee request
[Knee1] worker: Starting analyzing task 474
[Hip] worker: Starting analyzing task 345
[Hip] worker: Finished task 345
[HIP_LOGGER] worker: Starting analyzing task 345
[HIP_LOGGER] worker: Finished task 345
[XRayRoom] generating hip and knee request
[Hip] worker: Starting analyzing task 179
[HIP_LOGGER] worker: Starting analyzing task 179
[Hip] worker: Finished task 179
[Knee1] worker: Finished task 474
[Knee1] worker: Starting analyzing task 154
[Knee1] worker: Finished task 154
[XRayRoom] generating hip and knee request
[Hip] worker: Starting analyzing task 763
[Knee1] worker: Starting analyzing task 562
[HIP_LOGGER] worker: Finished task 179
[HIP_LOGGER] worker: Starting analyzing task 763
[Hip] worker: Finished task 763
[Knee1] worker: Finished task 562
[HIP_LOGGER] worker: Finished task 763
[XRayRoom] generating hip and knee request
[Hip] worker: Starting analyzing task 474
[Knee2] worker: Starting analyzing task 997
[HIP_LOGGER] worker: Starting analyzing task 474
[Hip] worker: Finished task 474
[XRayRoom] generating hip and knee request
[Hip] worker: Starting analyzing task 184
[Knee1] worker: Starting analyzing task 578
[Knee2] worker: Finished task 997
[HIP_LOGGER] worker: Finished task 474

Conclusion

Our simple, yet operational, program shows how to implement a message broker with direct and multicast behaviors.

Having chosen ZIO we have managed to unearth only a fraction of its potential - by using ZIO Queues and ZIO Fibers within effects. Out of the box parallelism, immutability, referential transparency, and wrapped side effect managing are what has made this example painless and really very enjoyable to write.

To see complete example see gist link below.

Links

https://gist.github.com/mtsokol/0d6ab5473c04583899e3ffdcb7812959

https://github.com/zio/zio

https://zio.dev/docs/datatypes/datatypes_queue

https://zio.dev/docs/datatypes/datatypes_fiber

More about ZIO on our BLOG:

All of us are eager to start writing real-world applications using ZIO (Scala library). But ZIO is still quite new, and its ecosystem is still incomplete. So while we wait for ZIO-HTTP and ZIO-JDBC (or whatever else comes in the future) to happen, we will have to end up integrating ZIO with other frameworks that can communicate with the outside world. In this article, I want to explore what this kind of integration might look like for two of the most popular libraries – Slick and Akka HTTP.

Really quick introduction to GraphQL

What is GraphQL?
Many people think that GraphQL is ‘something’ related to Graph Databases, in my opinion they’re wrong. GraphQL is to Graph DB like Javascript to Java. Are based on similar concepts, but are used for completely different things.

GraphQL is a query language for API’s. Some people name it successor of REST, I’d rather say it supplementor of REST because both can work together. In this article you’ll find good comparison of both.

In short: GraphQL is a query language for APIs, optimized for performance, designed and open-sourced by Facebook. In GraphQL you can ask server for connected data and you’ll get in response only what you’ve asked for. Not more. If you need more information about this, you’ll find it on GraphQL webpage Read more


Welcome to our second article about Keycloak Server! Previously, we’ve learnt to setup the server and make use of it in a webapp -this time, we’re going to create a matching back-end API and also learn a little bit about the OAuth2/OpenID magic that powers our entire authn/authz mechanism. So let’s get started! Read more

Providing authentication and authorization for the non-public-facing components of your application is an important part of many systems. But all major Scala frameworks come ready-equipped with some native tools for doing that, with complexity and comprehensiveness ranging from basic HTTP schemes with Akka HTTP’s SecurityDirectives to numerous Play plugins such as Deadbolt 2 or Silhouette.

But it can be difficult to get what you need out of some of these. And if you have to relearn them for every new framework – it ain’t nice either.

Learn how to overcome the additional overhead when moving to an unfamiliar tech stack!