Skip to content

Instantly share code, notes, and snippets.

@kirked
Last active June 2, 2017 05:48
Show Gist options
  • Save kirked/c55b0e52a776a5cdfcfdc4aab6ebdf48 to your computer and use it in GitHub Desktop.
Save kirked/c55b0e52a776a5cdfcfdc4aab6ebdf48 to your computer and use it in GitHub Desktop.
The `Find` monad combines `Option` and `Try`
import scala.util.{Try, Success, Failure}
import scala.util.control.NonFatal
object Find {
def empty[A]: Find[A] = NotFound
def apply[A](value: => A): Find[A] =
try {
if (value == null) NotFound else Found(value)
}
catch {
case NonFatal(e) => Failed(e)
}
implicit def findToIterable[A](find: Find[A]): Iterable[A] =
if (find.isFound) find.get :: Nil else Nil
implicit def optionToFind[A](opt: Option[A]): Find[A] = opt match {
case Some(a) => Found(a)
case None => NotFound
}
implicit def tryToFind[A](value: Try[A]): Find[A] = value match {
case Success(a) => Found(a)
case Failure(err) => Failed(err)
}
def NotFailedError = new NoSuchElementException("`error` on a non-failed Find")
def NotFoundError = new NoSuchElementException("not found")
}
sealed abstract class Find[+A] extends Product with Serializable { self =>
def isFound: Boolean
def isNotFound: Boolean
def isFailed: Boolean
def get: A
def error: Throwable
@inline final def isEmpty: Boolean = !isFound
@inline final def nonEmpty: Boolean = isFound
@inline final def toOption: Option[A] = if (isFound) Some(get) else None
@inline final def toTry: Try[A] =
if (isFound) Success(get) else if (isFailed) Failure(error) else Failure(Find.NotFoundError)
@inline final def getOrElse[B >: A](default: => B): B =
if (!isFound) default else this.get
@inline final def orElse[B >: A](default: => Find[B]): Find[B] =
if (!isFound) default else this
@inline final def foreach[B](f: A => B): Unit =
if (isFound) f(get)
@inline final def forall(f: A => Boolean): Boolean = isEmpty || f(this.get)
@inline final def map[B](f: A => B): Find[B] = this match {
case NotFound => NotFound
case Failed(e) => Failed(e)
case Found(a) =>
try {
Find(f(a))
}
catch {
case NonFatal(e) => Failed(e)
}
}
@inline final def flatMap[B](f: A => Find[B]): Find[B] = this match {
case NotFound => NotFound
case Failed(e) => Failed(e)
case Found(a) =>
try {
f(a)
}
catch {
case NonFatal(e) => Failed(e)
}
}
@inline final def flatten[B](implicit ev: A <:< Find[B]): Find[B] = this match {
case NotFound => NotFound
case Failed(e) => Failed(e)
case Found(a) => a
}
@inline final def filter(f: A => Boolean): Find[A] =
try {
if (isFound || f(get)) this else NotFound
}
catch {
case NonFatal(e) => Failed(e)
}
@inline final def recover[B >: A](f: PartialFunction[Throwable, B]): Find[B] = this match {
case Failed(e) =>
try {
if (f isDefinedAt error) Find(f(error))
else this
}
catch {
case NonFatal(e) => Failed(e)
}
case _ => this
}
@inline final def recoverWith[B >: A](f: PartialFunction[Throwable, Find[B]]): Find[B] = this match {
case Failed(e) =>
try {
if (f isDefinedAt error) f(error)
else this
}
catch {
case NonFatal(e) => Failed(e)
}
case _ => this
}
/** Keep Find from being implicitly converted to Iterable in `for` comprehensions. */
@inline final def withFilter(p: A => Boolean): WithFilter = new WithFilter(p)
class WithFilter(p: A => Boolean) {
def map[B](f: A => B): Find[B] = self filter p map f
def flatMap[B](f: A => Find[B]): Find[B] = self filter p flatMap f
def foreach[B](f: A => B): Unit = self filter p foreach f
def withFilter(q: A => Boolean): WithFilter = new WithFilter(x => p(x) && q(x))
}
}
final case object NotFound extends Find[Nothing] {
final override val isFound = false
final override val isNotFound = true
final override val isFailed = false
@inline override final def get: Nothing = throw Find.NotFoundError
@inline override final def error: Throwable = throw Find.NotFailedError
}
final case class Failed[+A](override val error: Throwable) extends Find[A] {
final override val isFound = false
final override val isNotFound = false
final override val isFailed = true
@inline override final def get: A = throw error
}
final case class Found[+A](value: A) extends Find[A] {
final override val isFound = true
final override val isNotFound = false
final override val isFailed = false
@inline final override def get: A = value
@inline final override def error: Throwable = throw Find.NotFailedError
}
@kirked
Copy link
Author

kirked commented Apr 11, 2016

Using Find, you have one of three possible results:

  • Found(a) when the value is present (akin to Some);
  • NotFound when it isn't (like None);
  • or Failed(Throwable) on failure (like Failure in the Try monad)

This makes code look a little nicer than having a bunch of Try[Option[A]] return types in services.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment