Maybe
Maybe[A] is a low-allocation alternative to Option[A] that uses null to represent the absence of a value. It is an opaque type alias for A | Null, allowing you to write nullable-like code with the safety and ergonomics of an Option-style API. Core types: Maybe[A].
Here's the type definition and basic construction:
opaque type Maybe[+A] = A | Null
val present: Maybe[Int] = Maybe.present(42)
val absent: Maybe[Int] = Maybe.absent
Motivation
When working with optional values, you face a choice: Option[A] provides type safety and functional composition but allocates a wrapper object for every value. Maybe[A] provides an alternative with different trade-offs depending on your Scala version.
On Scala 3: Maybe[A] eliminates allocation overhead by leveraging union types and null semantics—every value is either the unwrapped value itself or null. This is ideal for performance-critical code where allocations impact throughput or latency. The type is an opaque alias for A | Null, giving you a dedicated API (map, flatMap, filter, etc.) with zero runtime wrapper overhead—just null checks.
On Scala 2.13: Maybe[A] is implemented as a sealed trait (Present[A] | Absent). Present values allocate a wrapper, so the allocation savings versus Option are less pronounced. However, the unified API and interoperability benefits still apply.
Why Maybe over Option?
- Zero allocation: Every
Maybeis either the value itself ornull—no wrapper objects - Familiar API: All your favorite
Optioncombinators (map,flatMap,fold, etc.) - Type safety: The opaque type prevents accidentally mixing nullable and non-nullable values
- Interoperable: Seamless conversion to/from
OptionwithtoOptionandMaybe#fromOption
Installation
Add the zio-blocks-maybe module to your build:
libraryDependencies += "dev.zio" %% "zio-blocks-maybe" % "0.0.41"
For Scala.js:
libraryDependencies += "dev.zio" %%% "zio-blocks-maybe" % "0.0.41"
Supported Scala versions: 2.13.x and 3.x
Overview
Maybe[A] provides a complete set of operations for working with optional values:
- Constructors:
Maybe.apply,Maybe.present,Maybe.absent,Maybe.fromOption - Predicates:
Maybe#isAbsent,isPresent,isEmpty,isDefined,nonEmpty - Access:
get,Maybe#getOrElse,orElse,orNull - Transformations:
map,flatMap,flatten - Filtering:
filter,filterNot,collect - Logical:
exists,forall,contains - Conversions:
toOption,toList,toSeq,iterator,toRight,toLeft - Composition:
Maybe#zip,unzip,unzip3,fold,foreach
How It Works
The workflow is straightforward: create a Maybe from a value or None, transform and filter it using functional operations, extract the result with safe accessors, and fall back to defaults when needed:
Value ─→ Maybe.apply (wrap) ─→ map / flatMap (transform)
↓ ↓
[null] ←─────── isAbsent (test) ← filter / collect (refine)
↓ ↓
absent ─→ orElse (fallback) ─→ get / getOrElse (extract)
Typical Data Flow
- Create a
MaybeusingMaybe.apply,Maybe.present,Maybe.fromOption, or explicitly withMaybe.absentfor no value - Transform values using
maporflatMap— operations on absent values short-circuit and remain absent - Filter with
filterorcollectto refine the value or produce absence based on a predicate - Compose with other
Maybevalues usingMaybe#zipor flatMap chains to build complex workflows - Extract the result using
get(throws on absence),Maybe#getOrElse(provides a default),toOption(convert toOption), orfold(handle both branches)
Common Patterns
Here are key patterns for working effectively with Maybe:
Present and Absent States
Every Maybe is either present (holds a non-null value) or absent (is null). Test the state with predicates:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(42)
if (value.isPresent) {
println(value.get) // Safe: we know it's present
} else {
println("No value")
}
Safe Extraction with Defaults
Use Maybe#getOrElse to provide a fallback when the value is absent, avoiding the exception risk of get:
import zio.blocks.maybe._
val userId: Maybe[String] = Maybe.absent
val name = userId.getOrElse("anonymous")
Chaining Transformations
Combine map and flatMap to thread operations through optional values. Absence propagates automatically:
import zio.blocks.maybe._
val userId: Maybe[Int] = Maybe.present(123)
val greeting = userId
.map(id => s"User $id")
.map(msg => s"Hello, $msg!")
println(greeting) // Maybe[String] containing "Hello, User 123!"
Filtering with Predicates
Use filter to keep a value only if it satisfies a condition, or collect with a partial function for both filtering and transformation:
import zio.blocks.maybe._
val age: Maybe[Int] = Maybe.present(25)
val isAdult = age.filter(_ >= 18) // Maybe[Int] containing 25
val isChild = age.filter(_ < 18) // Maybe[Int] absent
Combining Multiple Maybes
Use Maybe#zip to combine two Maybe values into a tuple, or chain multiple operations with flatMap:
import zio.blocks.maybe._
val firstName: Maybe[String] = Maybe.present("Alice")
val lastName: Maybe[String] = Maybe.present("Smith")
val fullName = firstName.zip(lastName).map { case (f, l) => s"$f $l" }
Converting to and from Option
Interoperate seamlessly with Option using toOption and Maybe#fromOption:
import zio.blocks.maybe._
val opt: Option[String] = Some("value")
val m: Maybe[String] = Maybe.fromOption(opt)
val backToOpt = m.toOption
Handling Errors with Either
Convert a Maybe to an Either to propagate errors in fail-fast computations:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(10)
val result: Either[String, Int] = value.toRight("Value not found")
Integration Points
Maybe[A] integrates with the broader Scala and ZIO ecosystem:
- Option interoperability: Convert bidirectionally with
toOptionandMaybe#fromOption; aConversion[Option[A], Maybe[A]]is provided for implicit conversions - Schema support: Schema codecs use private unsafe methods (
unsafeIsAbsent,unsafeGet,unsafeWrap) for efficient serialization and deserialization - For-comprehensions: Supports
withFilterfor guard clauses in for-expressions - Collections: Convert to
List,Seq, orIteratorfor bulk operations - Pattern matching: Works naturally in match expressions, though testing with predicates is more common
Example: For-Comprehension with Guards
Combine multiple Maybe values with filter guards:
import zio.blocks.maybe._
val maybeX: Maybe[Int] = Maybe.present(5)
val maybeY: Maybe[Int] = Maybe.present(10)
val result = for {
x <- maybeX
y <- maybeY
if x + y > 10
} yield x + y
println(result) // Maybe[Int] containing 15
Operations Reference
All Maybe operations are organized by category. Each subsection documents a group of related methods with examples.
Constructors
Create Maybe values using these factory methods:
Maybe.apply
Wraps a value in Maybe, treating null as Maybe.absent:
import zio.blocks.maybe._
val present = Maybe(42) // Maybe[Int] containing 42
val absent: Maybe[String] = Maybe(null) // Maybe[String] absent
Maybe.present
Explicitly wraps a non-null value:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(100)
Maybe.absent
Creates an absent value for any type:
import zio.blocks.maybe._
val empty: Maybe[String] = Maybe.absent
Maybe.empty
Alias for Maybe.absent:
import zio.blocks.maybe._
val empty: Maybe[Double] = Maybe.empty
Maybe.fromOption
Converts an Option to Maybe:
import zio.blocks.maybe._
val fromSome = Maybe.fromOption(Some(5)) // Maybe[Int] containing 5
val fromNone = Maybe.fromOption(None) // Maybe[Nothing] absent
State Testing
Test whether a Maybe is present or absent:
isPresent, isDefined, nonEmpty
All three are equivalent—return true if the value is non-null:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(42)
println(value.isPresent) // true
println(value.isDefined) // true
println(value.nonEmpty) // true
isAbsent, isEmpty
Both return true if the value is null:
import zio.blocks.maybe._
val empty: Maybe[Int] = Maybe.absent
println(empty.isAbsent) // true
println(empty.isEmpty) // true
Access
Retrieve the value or provide a fallback:
get
Unwraps the value, throwing NoSuchElementException if absent:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(10)
println(value.get) // 10
try {
Maybe.absent[Int].get
} catch {
case e: NoSuchElementException => println(e.getMessage) // "Maybe.absent.get"
}
getOrElse
Returns the value if present, or evaluates the default:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.absent
val withDefault: Int = value.getOrElse(99)
println(withDefault) // 99
orElse
Returns the current Maybe if present, or another Maybe:
import zio.blocks.maybe._
val first: Maybe[Int] = Maybe.absent
val second: Maybe[Int] = Maybe.present(42)
val result = first.orElse(second)
println(result.get) // 42
orNull
Converts to nullable, returning the value or null:
import zio.blocks.maybe._
val value: Maybe[String] = Maybe.absent
val nullable: String = value.orNull
println(nullable) // null
Transformations
Transform values or short-circuit on absence:
map
Applies a function if present, remains absent otherwise:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(5)
val doubled: Maybe[Int] = value.map(_ * 2)
val absent: Maybe[String] = Maybe.absent.map(_ => "never runs")
println(doubled.get) // 10
println(absent.isAbsent) // true
flatMap
Chains operations that return Maybe, flattening the result:
import zio.blocks.maybe._
def safeDivide(a: Int, b: Int): Maybe[Int] =
if (b == 0) Maybe.absent else Maybe(a / b)
val result = Maybe.present(10).flatMap(safeDivide(_, 2))
println(result.get) // 5
val divideByZero = Maybe.present(10).flatMap(safeDivide(_, 0))
println(divideByZero.isAbsent) // true
flatten
Unwraps a nested Maybe:
import zio.blocks.maybe._
val nested: Maybe[Maybe[Int]] = Maybe.present(Maybe.present(42))
val flat: Maybe[Int] = nested.flatten
println(flat.get) // 42
Filtering
Keep or discard values based on predicates:
filter
Keeps the value only if the predicate is true:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(10)
val even = value.filter(_ % 2 == 0) // Present: 10
val odd = value.filter(_ % 2 != 0) // Absent
println(even.get) // 10
println(odd.isAbsent) // true
filterNot
Inverse of filter—keeps the value if the predicate is false:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(5)
val notOdd: Maybe[Int] = value.filterNot(_ % 2 != 0)
println(notOdd.isAbsent) // true
collect
Combines filtering with transformation using a partial function:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(8)
val result = value.collect { case n if n % 2 == 0 => s"even: $n" }
println(result.get) // "even: 8"
val odd: Maybe[Int] = Maybe.present(7)
val noMatch: Maybe[String] = odd.collect { case n if n % 2 == 0 => s"even: $n" }
println(noMatch.isAbsent) // true
Logical Predicates
Test conditions without extracting the value:
contains
Returns true if the value equals the given element:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(42)
println(value.contains(42)) // true
println(value.contains(41)) // false
println(Maybe.absent[Int].contains(42)) // false
exists
Returns true if the value satisfies the predicate:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(10)
println(value.exists(_ > 5)) // true
println(value.exists(_ > 20)) // false
println(Maybe.absent[Int].exists(_ => true)) // false
forall
Returns true if the value satisfies the predicate, or if absent (vacuous truth):
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(10)
println(value.forall(_ > 5)) // true
println(value.forall(_ > 20)) // false
println(Maybe.absent[Int].forall(_ => false)) // true (absent = vacuously true)
Iteration and Conversion
Convert Maybe to other types or iterate its contents:
foreach
Executes a side effect if present:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(42)
value.foreach(x => println(s"Value: $x"))
Maybe.absent[Int].foreach(_ => println("not called"))
toOption
Converts to Option[A]:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(7)
val some: Option[Int] = value.toOption
val noneVal: Option[Int] = Maybe.absent[Int].toOption
println(some) // Some(7)
println(noneVal) // None
toList
Converts to List[A]:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(5)
val list: List[Int] = value.toList
val emptyList: List[Int] = Maybe.absent[Int].toList
println(list) // List(5)
println(emptyList) // List()
toSeq
Converts to Seq[A]:
import zio.blocks.maybe._
val value: Maybe[String] = Maybe.present("hello")
val seq: Seq[String] = value.toSeq
println(seq) // Seq("hello")
iterator
Creates an iterator over the value:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(42)
val it = value.iterator
println(it.toList) // List(42)
Maybe.absent[Int].iterator.toList // List()
Either Conversion
Convert Maybe to Either for error handling:
toRight
Converts to Either[X, A] with a left error value:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(10)
val right: Either[String, Int] = value.toRight("not found")
println(right) // Right(10)
val absent: Maybe[Int] = Maybe.absent
val left: Either[String, Int] = absent.toRight("not found")
println(left) // Left("not found")
toLeft
Converts to Either[A, X] with a right success value:
import zio.blocks.maybe._
val value: Maybe[String] = Maybe.present("error")
val left: Either[String, Unit] = value.toLeft(())
println(left) // Left("error")
val absent: Maybe[String] = Maybe.absent
val right: Either[String, Unit] = absent.toLeft(())
println(right) // Right(())
Composition
Combine multiple Maybe values:
zip
Combines two Maybe values into a tuple:
import zio.blocks.maybe._
val x: Maybe[Int] = Maybe.present(1)
val y: Maybe[String] = Maybe.present("a")
val tuple = x.zip(y)
println(tuple.get) // (1, "a")
val absent: Maybe[String] = Maybe.absent
val zipped = x.zip(absent)
println(zipped.isAbsent) // true
unzip
Splits a Maybe[(A, B)] into a tuple of Maybe[A] and Maybe[B]:
import zio.blocks.maybe._
val tuple: Maybe[(Int, String)] = Maybe.present((42, "answer"))
val (x, y) = tuple.unzip
println(x.get) // 42
println(y.get) // "answer"
val absent: Maybe[(Int, String)] = Maybe.absent
val (ax, ay) = absent.unzip
println(ax.isAbsent && ay.isAbsent) // true
unzip3
Splits a Maybe[(A, B, C)] into three Maybe values:
import zio.blocks.maybe._
val triple: Maybe[(Int, String, Double)] = Maybe.present((1, "one", 1.0))
val (a, b, c) = triple.unzip3
println(a.get) // 1
println(b.get) // "one"
println(c.get) // 1.0
Folding
Reduce a Maybe to a value by handling both branches:
fold
Applies one of two functions based on presence:
import zio.blocks.maybe._
val value: Maybe[Int] = Maybe.present(10)
val result = value.fold(-1)((x: Int) => x * 2)
println(result) // 20
val absent: Maybe[Int] = Maybe.absent
val fallback = absent.fold(-1)((x: Int) => x * 2)
println(fallback) // -1
For-Comprehensions
Use withFilter to support guard clauses:
withFilter
Enables guarded for-expressions:
import zio.blocks.maybe._
val x: Maybe[Int] = Maybe.present(5)
val y: Maybe[Int] = Maybe.present(15)
val result = for {
a <- x
b <- y
if a + b > 15
} yield a + b
println(result.get) // 20
Running Examples
End-to-end workflows combining multiple Maybe operations:
Example: Parsing and Transforming User Input
Build a pipeline that parses user input, validates it, and transforms the result:
import zio.blocks.maybe._
case class User(id: Int, name: String, age: Int)
def parseId(input: String): Maybe[Int] =
Maybe.fromOption(input.toIntOption)
def validateAge(age: Int): Maybe[Int] =
Maybe.absent[Int].orElse(
if (age >= 0 && age <= 150) Maybe.present(age) else Maybe.absent
)
val input = "42"
val processedAge = parseId(input)
.flatMap(id => Maybe.present(User(id, "Alice", 30)))
.map(_.age)
.flatMap(validateAge)
println(processedAge.getOrElse(-1)) // 30
Example: Chaining Optional Database Results
Work with nullable database results, filtering and transforming as needed:
import zio.blocks.maybe._
case class Product(id: Int, name: String, price: Double, inStock: Boolean)
def findProduct(id: Int): Maybe[Product] =
if (id > 0) Maybe.present(Product(id, s"Product $id", 99.99, true))
else Maybe.absent
def applyDiscount(product: Product, percent: Int): Maybe[Product] =
if (percent >= 0 && percent <= 100)
Maybe.present(product.copy(price = product.price * (1 - percent / 100.0)))
else Maybe.absent
val discountedProduct = findProduct(123)
.filter(_.inStock)
.flatMap(applyDiscount(_, 20))
.map(p => s"${p.name} now costs $$${p.price}")
println(discountedProduct.getOrElse("Product not available"))
Example: Combining Multiple Maybes with Fallbacks
Handle scenarios where multiple optional values need to be combined:
import zio.blocks.maybe._
case class Config(host: Maybe[String], port: Maybe[Int], timeout: Maybe[Long])
def buildConnection(config: Config): String = {
val host = config.host.getOrElse("localhost")
val port = config.port.getOrElse(8080)
val timeout = config.timeout.getOrElse(5000L)
s"Connection to $host:$port with timeout $timeout ms"
}
val config = Config(
host = Maybe.present("api.example.com"),
port = Maybe.absent,
timeout = Maybe.present(10000L)
)
println(buildConnection(config))
Example: Error Handling with Either Conversion
Convert Maybe to Either for composable error handling in result types:
import zio.blocks.maybe._
case class Request(id: Maybe[String], method: Maybe[String])
def validateRequest(req: Request): Either[String, (String, String)] = {
for {
id <- req.id.toRight("Missing request ID")
method <- req.method.toRight("Missing HTTP method")
} yield (id, method)
}
val validReq = Request(
id = Maybe.present("req-123"),
method = Maybe.present("GET")
)
val invalidReq = Request(
id = Maybe.absent,
method = Maybe.present("POST")
)
println(validateRequest(validReq)) // Right((req-123, GET))
println(validateRequest(invalidReq)) // Left(Missing request ID)
Example: Building a Computation Pipeline
Chain multiple transformations with fallback at each step:
import zio.blocks.maybe._
def getUser(id: Int): Maybe[String] =
if (id > 0) Maybe.present(s"user_$id") else Maybe.absent
def getUserEmail(username: String): Maybe[String] =
if (username.nonEmpty) Maybe.present(s"$username@example.com") else Maybe.absent
def getUserProfile(id: Int): Maybe[String] = {
val username = getUser(id)
username
.flatMap(getUserEmail)
.map(email => s"Profile for $email")
.orElse(Maybe.present("Guest user"))
}
println(getUserProfile(42)) // Profile for user_42@example.com
println(getUserProfile(-1)) // Guest user