Last active
June 2, 2017 05:48
-
-
Save kirked/c55b0e52a776a5cdfcfdc4aab6ebdf48 to your computer and use it in GitHub Desktop.
The `Find` monad combines `Option` and `Try`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Using
Find
, you have one of three possible results:Found(a)
when the value is present (akin toSome
);NotFound
when it isn't (likeNone
);Failed(Throwable)
on failure (likeFailure
in the Try monad)This makes code look a little nicer than having a bunch of
Try[Option[A]]
return types in services.