Skip to main content
Version: 2.x

Coding Guidelines

These are coding guidelines strictly for ZIO contributors working on ZIO projects and not general conventions to be applied by the Scala community at large.

Additionally, bear in mind that, although we try to enforce these rules to the best of our ability, both via automated rules (scalafmt) and strict reviewing processes, it is still possible to find existing code that does not comply with these rules. If that is the case, we would be extremely grateful if you could make a contribution by providing a fix for the issue.

Last, but not least, these rules are continuously evolving and, as such, refer to them once in a while when in doubt.

Defining classes and traits​

  1. Value classes must be final and extend AnyVal. This is done to avoid allocating runtime objects.
  2. Method extension classes must be final and extend AnyVal.
  3. Avoid overloading standard interfaces. When creating services, avoid using the same names as well-known standard interfaces. Example: Instead of having a service Random with methods nextLong(n) and nextInt(n), consider choosing something like nextLongBounded(n) and nextIntBounded(n).
  4. Sealed traits that are ADTs (algebraic data types) should extend Product and Serializable. This is done to help the compiler infer types.
  5. Regular traits and sealed traits that do not form ADTs should extend Serializable but not Product.
  6. Traits should always extend Serializable (e.g. ZIO).

Final and private modifiers​

  1. All methods on classes / traits are declared final, by default.
  2. No methods on objects are declared final, because they are final by default.
  3. No methods on final classes are declared final, because they are final by default.
  4. All classes inside objects should be defined final, because otherwise they could still be extended.
  5. In general, classes that are not case classes have their constructors and constructor parameters private. Typically, it is not good practice to expose constructors and constructor parameters, but exceptions apply (i.e. Assertion and TestAnnotation).
  6. All vals are declared final, even in objects or final classes, if they are constant expressions and without type annotations.
  7. Package-private vals and methods should be declared final.

Refactoring​

  1. If a class has all its members final, the class should be declared final and the final member annotations should be removed, except for constant expressions.
  2. All type annotations should use the least powerful type alias. This means that, for example, a ZIO effect that has no dependencies but throws an arbitrary error should be defined as IO.
  3. Use def in place of val for an abstract data member to avoid NullPointerException risk.

Understanding naming of parameters or values​

ZIO code often uses the following naming conventions, and you might be asked to change method parameters to follow these conventions. This guide can help you understand where the names come from. Naming expectations can be helpful in understanding the role of certain parameters without even glancing at their type signatures when reading code or class/method signatures.

  1. Partial functions have a shortened name pf.
  2. In ZIO, implicit parameters are often used as compiler evidences. These evidences help you, as a developer, prove something to the compiler (at compile time), and they have the ability to add constraints to a method. They are typically called ev if there is only one, or ev1, ev2, ... if there is more than one.
  3. Promises are called p (unless in their own class methods, in which case they are called that, as point 7 defines).
  4. Functions are called fn, fn1, unless they bear specific meaning: use, release.
  5. ZIO effects are called f, unless they bear specific meaning like partially providing an environment: r0.
  6. Iterables are called in.
  7. When a parameter type equals its own (in a method of a trait), call it that.
  8. Be mindful of using by-name parameters. Mind the Function[0] extra allocation and loss of clean syntax when invoking the method. Loss of syntax means that instead of being able to do something like f.flatMap(ZIO.success) you are required to explicitly do f.flatMap(ZIO.success(_)).
  9. Fold or fold-variant initial values are called zero.

Understanding naming of methods​

ZIO goes to great lengths to define method names that are intuitive to the library user. Naming is hard! This section will attempt to provide some guidelines and examples to document, guide, and explain the naming of methods in ZIO.

  1. All operators that return effects should be lazy in all parameters that are not lambdas. Strict parameters are a source of bugs when users inadvertently call side-effecting code in arguments to these parameters. Preventing these bugs is the responsibility of the effect system, and lazy parameters allow the runtime to properly manage these side effects. There are two exceptions. First, strict parameters should be used when by-name parameters would cause duplicate method signatures due to type erasure. Second, operators on high-performance concurrent data structures such as Ref, Queue, and Hub should be strict in their parameters.
  2. Methods that have the form of List#zip are called zip, and have an alias called <*>. The parallel version, if applicable, has the name zipPar, with an alias called <&>.
  3. Avoid the use of effect in constructors, as this pushes responsibility onto users to identify what code is or is not side-effecting, whereas the effect system should handle this correctly regardless. Instead, prefer more specific names such as succeed, attempt, or suspend.
  4. The dual of zip, which is trying either a left or right side, producing an Either of the result, should be called orElseEither, with alias <+>. The simplified variant where both left and right have the same type should be called orElse, with alias <>.
  5. Constructors for a data type X that are based on another data type Y should be placed in the companion object X and named fromY. For example, ZIO.fromOption, ZStream.fromEffect.
  6. Parallel versions of methods should be named the same, but with a Par suffix.
  7. foreach should be used for operators that effectually iterate over a collection. For example, ZIO.foreach.
  8. Variants of operators that accept arguments or return results in the context of an effect type should be suffixed by the name of the effect type, for example mapZIO or mapSTM. The M suffix should not be used, as it is not idiomatic Scala and does not specify what effect type is involved.
  9. Use the Discard suffix for variants of methods that discard their results. For example, foreachDiscard. The _ suffix should not be used, as it is not idiomatic Scala and does not describe what it does.
  10. Methods that are necessarily side-effecting should be prefixed with unsafe, for example unsafeRun. This does not apply to methods on internal data types that are inherently imperative in nature, for example MutableConcurrentQueue.

Type annotations​

ZIO goes to great lengths to take advantage of the Scala compiler in varied ways. Type variance is one of them. The following rules are good to have in mind when adding new types, traits, or classes that have either covariant or contravariant types.

  1. Generalized ADTs should always have type annotations (i.e. final case class Fail[+E](value: E) extends Cause[E]).
  2. Type aliases should always have type annotations. Much like in generalized ADTs, defining type aliases should carry the type annotations (i.e. type IO[+E, +A] = ZIO[Any, E, A]).

When defining new methods, keep in mind the following rules:

  1. Accept the most general type possible. For example, if a method accepts a collection, prefer Iterable[A] to List[A].
  2. Return the most specific type possible, e.g. prefer UIO[Unit] to UIO[Any].

Method alphabetization​

In general, the following rules should be applied regarding method alphabetization. To fix forward references of values, we recommend the programmer make them lazy (lazy val). Operators are any methods that only have non-letter characters (i.e. <*>, <>, *>).

  1. Public abstract defs / vals are listed first, and alphabetized, with operators appearing before names.
  2. Public concrete defs / vals are listed second, and alphabetized, with operators appearing before names.
  3. Private implementation details are listed third, and alphabetized, with operators appearing before names.

Scala documentation​

It is strongly recommended to use Scaladoc links when referring to other members. This both makes it easier for users to navigate the documentation and enforces that the references are accurate. Good examples of this are the ZIO type aliases that are extremely pervasive in the codebase: Task, RIO, URIO, and UIO. To make it easy for developers to see the implementation, Scaladoc links are used, for example:

/**
* @see See [[zio.ZIO.absolve]]
*/
def absolve[R, A](v: RIO[R, Either[Throwable, A]]): RIO[R, A] =
ZIO.absolve(v)