Last active
December 1, 2017 12:58
-
-
Save davegurnell/9e3adcdbdb27c0090e5700d2a6d3bdfb to your computer and use it in GitHub Desktop.
Postfix calculator with ADT-based error handling
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
package calc | |
sealed trait Term | |
case object Add extends Term | |
case object Sub extends Term | |
case object Mul extends Term | |
case object Div extends Term | |
case class Num(value: Int) extends Term | |
sealed trait Error | |
case object BadTerm extends Error | |
case object EmptyStack extends Error | |
case object TooManyResults extends Error | |
case object NotEnoughResults extends Error | |
case object NotEnoughOperands extends Error | |
object PostfixCalc { | |
type ErrorOr[A] = Either[Error, A] | |
def apply(line: String): ErrorOr[Int] = | |
for { | |
terms <- parse(line) | |
result <- evaluate(terms) | |
} yield result | |
// -------------------- | |
def parse(line: String): ErrorOr[List[Term]] = { | |
val seed: ErrorOr[List[Term]] = Right(Nil) | |
tokenize(line).foldRight(seed) { (str, accum) => | |
for { | |
terms <- accum | |
term <- parseTerm(str) | |
} yield term :: terms | |
} | |
} | |
def tokenize(line: String): List[String] = | |
line.trim.split("[ \\t]+").toList | |
def parseTerm(str: String): ErrorOr[Term] = | |
str match { | |
case "+" => Right(Add) | |
case "-" => Right(Sub) | |
case "*" => Right(Mul) | |
case "/" => Right(Div) | |
case str => | |
try { | |
Right(Num(str.toInt)) | |
} catch { | |
case exn: NumberFormatException => | |
Left(BadTerm) | |
} | |
} | |
// -------------------- | |
def evaluate(terms: List[Term]): ErrorOr[Int] = { | |
val result: ErrorOr[List[Int]] = | |
terms.foldLeft(emptyStack)(step) | |
result.flatMap { | |
case result :: Nil => Right(result) | |
case result :: tail => Left(TooManyResults) | |
case Nil => Left(NotEnoughResults) | |
} | |
} | |
val emptyStack: ErrorOr[List[Int]] = Right(Nil) | |
def step(stack: ErrorOr[List[Int]], term: Term): ErrorOr[List[Int]] = | |
stack.flatMap { stack => | |
(term, stack) match { | |
case (Num(n), list) => Right(n :: list) | |
case (Add, b :: a :: tail) => Right((a + b) :: tail) | |
case (Sub, b :: a :: tail) => Right((a - b) :: tail) | |
case (Mul, b :: a :: tail) => Right((a * b) :: tail) | |
case (Div, b :: a :: tail) => Right((a / b) :: tail) | |
case (Add | Sub | Mul | Div, _) => | |
Left(NotEnoughOperands) | |
} | |
} | |
} |
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
package calc | |
import scala.concurrent._ | |
import scala.concurrent.duration._ | |
import scala.concurrent.ExecutionContext.Implicits.global | |
sealed trait Term | |
case object Add extends Term | |
case object Sub extends Term | |
case object Mul extends Term | |
case object Div extends Term | |
case class Num(value: Int) extends Term | |
sealed abstract class Error(message: String) extends Exception(message) | |
case class BadTerm() extends Error("BadTerm") | |
case class EmptyStack() extends Error("EmptyStack") | |
case class TooManyResults() extends Error("TooManyResults") | |
case class NotEnoughResults() extends Error("NotEnoughResults") | |
case class NotEnoughOperands() extends Error("NotEnoughOperands") | |
object PostfixCalc { | |
def apply(line: String): Future[Int] = | |
for { | |
terms <- parse(line) | |
result <- evaluate(terms) | |
} yield result | |
// -------------------- | |
def parse(line: String): Future[List[Term]] = { | |
val seed: Future[List[Term]] = Future.successful(Nil) | |
tokenize(line).foldRight(seed) { (str, accum) => | |
for { | |
terms <- accum | |
term <- parseTerm(str) | |
} yield term :: terms | |
} | |
} | |
def tokenize(line: String): List[String] = | |
line.trim.split("[ \\t]+").toList | |
def parseTerm(str: String): Future[Term] = | |
str match { | |
case "+" => Future.successful(Add) | |
case "-" => Future.successful(Sub) | |
case "*" => Future.successful(Mul) | |
case "/" => Future.successful(Div) | |
case str => | |
try { | |
Future.successful(Num(str.toInt)) | |
} catch { | |
case exn: NumberFormatException => | |
Future.failed(new BadTerm()) | |
} | |
} | |
// -------------------- | |
def evaluate(terms: List[Term]): Future[Int] = { | |
val result: Future[List[Int]] = | |
terms.foldLeft(emptyStack)(step) | |
result.flatMap { | |
case result :: Nil => Future.successful(result) | |
case result :: tail => Future.failed(new TooManyResults()) | |
case Nil => Future.failed(new NotEnoughResults()) | |
} | |
} | |
val emptyStack: Future[List[Int]] = Future.successful(Nil) | |
def step(stack: Future[List[Int]], term: Term): Future[List[Int]] = | |
stack.flatMap { stack => | |
(term, stack) match { | |
case (Num(n), list) => Future.successful(n :: list) | |
case (Add, b :: a :: tail) => Future.successful((a + b) :: tail) | |
case (Sub, b :: a :: tail) => Future.successful((a - b) :: tail) | |
case (Mul, b :: a :: tail) => Future.successful((a * b) :: tail) | |
case (Div, b :: a :: tail) => Future.successful((a / b) :: tail) | |
case (Add | Sub | Mul | Div, _) => | |
Future.failed(new NotEnoughOperands()) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment