Skip to content

Instantly share code, notes, and snippets.

@pjambet
Last active July 9, 2020 13:09
Show Gist options
  • Save pjambet/6d5aa587b29f9c4635bb7ed7796cb0b9 to your computer and use it in GitHub Desktop.
Save pjambet/6d5aa587b29f9c4635bb7ed7796cb0b9 to your computer and use it in GitHub Desktop.
ROP in Scala
object Railway {
sealed trait TwoTrack[F]
case class Success[S](data: S) extends TwoTrack[S]
case class Failure[S](message: String) extends TwoTrack[S]
def succeed[S](x: S) = Success(x)
def fail[S](message: String) = Failure[S](message)
def bind[A, B](switchFunction: A => TwoTrack[B])(twoTrackInput: TwoTrack[A]): TwoTrack[B] = {
twoTrackInput match {
case Success(s) => switchFunction(s)
case Failure(f) => fail(f)
}
}
def bind2[A, B](switchFunction: A => TwoTrack[B]): TwoTrack[A] => TwoTrack[B] = {
{ (twoTrackInput: TwoTrack[A]) =>
twoTrackInput match {
case Success(s) => switchFunction(s)
case Failure(f) => fail(f)
}
}
}
def map[A, B](singleTrackFunction: A => B): TwoTrack[A] => TwoTrack[B] = { twoTrackInput: TwoTrack[A] =>
twoTrackInput match {
case Success(s) => succeed(singleTrackFunction(s))
case Failure(f) => fail(f)
}
}
def map2[A, B](singleTrackFunction: A => B): TwoTrack[A] => TwoTrack[B] = {
bind(singleTrackFunction.andThen(Success.apply))
}
def tee[A](deadEndFunction: A => Unit)(a: A): A = {
deadEndFunction(a)
a
}
def tryCatch[A, B](f: A => B)(exnHandler: Throwable => String)(x: A): TwoTrack[B] = try {
succeed(f(x))
} catch {
case ex: Throwable =>
fail(exnHandler(ex))
}
def doubleMap[A, B](successFunc: A => B)
(failureFunc: String => String)
(twoTrackInput: TwoTrack[A]): TwoTrack[B] = twoTrackInput match {
case Success(s) => succeed(successFunc(s))
case Failure(f) => fail(failureFunc(f))
}
def log[A](twoTrackInput: TwoTrack[A]): TwoTrack[A] = {
val success = { x: A => println(s"DEBUG. Success so far: $x"); x }
val failure = { x: String => println(s"ERROR. $x"); x }
doubleMap(success)(failure)(twoTrackInput)
}
// ---
case class Request(name: String, email: String)
def nameNotBlank(request: Request): TwoTrack[Request] =
if (request.name == "") {
fail("Name must not be blank")
} else {
succeed(request)
}
def name50(request: Request): TwoTrack[Request] =
if (request.name.length > 50) {
fail("Name must not be longer than 50 chars")
} else {
succeed(request)
}
def emailNotBlank(request: Request): TwoTrack[Request] =
if (request.email == "") {
fail("Email must not be blank")
} else {
succeed(request)
}
def validateRequest(twoTrackInput: TwoTrack[Request]): TwoTrack[Request] = {
bind2(nameNotBlank)
.andThen(bind(name50))
.andThen(bind(emailNotBlank))(twoTrackInput)
}
def updateDB(request: Request): Unit = {
throw new RuntimeException("Fake DB Error")
()
}
val updateDBStep: Request => TwoTrack[Request] =
tryCatch(tee(updateDB))(ex => ex.getMessage)
def canonicalizeEmail(request: Request): Request = {
request.copy(email = request.email.trim().toLowerCase())
}
def main(args: Array[String]): Unit = {
val railway = (succeed[Request] _)
.andThen(validateRequest)
.andThen(map(canonicalizeEmail))
.andThen(bind(updateDBStep))
.andThen(log)
railway.apply(Request(name = "Pierre", email = "a"))
}
}
import scala.util.{Failure, Success, Try}
object RailwayEither {
type TwoTrack[S] = Either[String, S]
def fail[S](message: String) = Left(message)
def succeed[S](x: S) = Right(x)
def switch[A, B](fn: A => B): A => TwoTrack[B] =
fn.andThen(Right.apply)
def tryCatch[A](fn: A => Unit)(x: A): Either[String, A] = {
Try(fn(x)) match {
case Failure(exception) => Left(exception.getMessage)
case Success(_) => Right(x)
}
}
case class Request(name: String, email: String)
def nameNotBlank(request: Request): TwoTrack[Request] =
if (request.name == "") {
fail("Name must not be blank")
} else {
succeed(request)
}
def name50(request: Request): TwoTrack[Request] =
if (request.name.length > 50) {
fail("Name must not be longer than 50 chars")
} else {
succeed(request)
}
def emailNotBlank(request: Request): TwoTrack[Request] =
if (request.email == "") {
fail("Email must not be blank")
} else {
succeed(request)
}
def validateRequest(twoTrackInput: TwoTrack[Request]): TwoTrack[Request] = {
for {
r <- twoTrackInput
r <- nameNotBlank(r)
r <- name50(r)
r <- emailNotBlank(r)
} yield r
}
def updateDB(request: Request): Unit = {
// throw new RuntimeException("Fake DB Error")
()
}
def canonicalizeEmail(request: Request): Request = {
request.copy(email = request.email.trim().toLowerCase())
}
def logSuccess[A](x: A): TwoTrack[A] = {
println(s"DEBUG. Success so far: $x");
succeed(x)
}
def logFailure[A](x: String): TwoTrack[A] = {
println(s"ERROR. $x");
fail(x)
}
def main(args: Array[String]): Unit = {
val request = Request(name = "Pierre", email = "pierre@pjam.me")
val updateDBStep: Request => TwoTrack[Request] = tryCatch(updateDB)
val railway = validateRequest(succeed(request))
.flatMap(switch(canonicalizeEmail))
.flatMap(updateDBStep)
.fold(logFailure, logSuccess)
println(railway)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment