Here1 (credits to Ben Lynn2) is a simple calculator in Haskell.

I decided to try porting it over3 to Scala. The original example also makes this work in Javascript using `Haste`; I’ve left this out for now (though I might come back and get this to work under `Scala.js` later).

The underlying basis is the same, using the awesome `fastparse` library to stand-in for `Parsec`/`Megaparsec` from Haskell, and the code feels quite concise and readable to me4.

This is the entirety of the parsing code we need:

```  def numParser[_: P] = P( ("-".? ~ CharIn("0-9").rep ~ ".".? ~ CharIn("0-9").rep).!.map(_.toDouble) )
def parenParser[_: P]: P[Double] = P( "(" ~/ exprParser ~ ")"  )  // needs explicit type because it's recursive.
def factorParser[_: P] = P( parenParser | numParser )
def termParser[_: P] = P( factorParser ~ (CharIn("*/").! ~/ factorParser).rep ).map(eval)
def exprParser[_: P] = P( termParser ~ (CharIn("+\\-").! ~/ termParser).rep ).map(eval)

```

… along with a tiny bit of evaluation:

```  def eval(ast: (Double, Seq[(String, Double)])): Double = {
val (base, ops) = ast
ops.foldLeft(base) {
case (n1, (op, n2)) =>
op match {
case "*" => n1 * n2
case "/" => n1 / n2
case "+" => n1 + n2
case "-" => n1 - n2
}
}
}

```

That’s it.

That’s all you need to take in a `String` and convert it to a `Double`.

What Ben says about the evolution of parsing-as-it-used is true: it was completely normal to resort to lex/yacc and then flex/bison for these earlier, but Parser Combinators and PEGs take it to a whole other level, and once you’ve become comfortable with them, there’s no going back to the verbose old days.

Just to break down the parsing code a bit:

This parses a number: `def numParser[_: P] = P( ("-".? ~ CharIn("0-9").rep ~ ".".? ~ CharIn("0-9").rep).!.map(_.toDouble) )`. Or rather, it returns a function that can parse a number.

It’s possible to play5 with this within a repl; I use `sbt console`6 when something depends on a class/object within a project, but this is an independent enough chunk that you could use `ammonite`7 instead too.

```agam-agam@ import fastparse._
import fastparse._
agam-agam@ import NoWhitespace._
import NoWhitespace._
agam-agam@ def numParser[_: P] = P( ("-".? ~ CharIn("0-9").rep ~ ".".? ~ CharIn("0-9").rep).!.map(_.toDouble) )
defined function numParser
agam-agam@ parse("45", numParser(_))
res263: Parsed[Double] = Success(45.0, 2)
agam-agam@ parse("4.5", numParser(_))
res264: Parsed[Double] = Success(4.5, 3)

```

Where it gets interesting is composing these parsing functions.

Given a function to parse a `factor` (a number, or sub-expression within parentheses), we can define a function to parse combinations of multiplying/dividing these as:

```def termParser[_: P] = P( factorParser ~ (CharIn("*/").! ~/ factorParser).rep ).map(eval)

```

… which reads very “regex-like”, imo!

Anyway, I added a basic I/o driver around this, so it can be “run” as:

```➜ sbt run
[info] Set current project to hello-world (in build file:/home/agam/code/simplecalc/)
[info] Compiling 1 Scala source to /home/agam/code/simplecalc/target/scala-2.13/classes ...
[warn] there was one deprecation warning (since 2.11.0); re-run with -deprecation for details
[warn] one warning found
[info] running Main
Simple calculator
Enter an expression, or 'END' to quit: 5   6
Result: 11.0
Enter an expression, or 'END' to quit: 2 - 5.2 * 1.5
Result: -5.800000000000001
Enter an expression, or 'END' to quit: 1.8   2.3 / 4.5
Result: 2.311111111111111
Enter an expression, or 'END' to quit: 42   1 - 5
Result: 38.0
Enter an expression, or 'END' to quit: END
[success] Total time: 55 s, completed Aug 6, 2020, 11:36:47 PM

```

(Yeah, the weird decimal point behavior is just `Double` being a double; that is beyond the scope of this example 😐)

1. ”Parser Combinators” ↩︎
2. Indeed, Ben has a bunch of other awesome stuff like this, and I might just go Scala-ize more of them 🙂 ↩︎
3. Repo (with this core code, as well as a `Main` driver and tests) is here ↩︎
4. My subjective opinion is that Scala is a sweet spot between ↩︎
5. I’ve taken a “shortcut” with my `map(_.toDouble)` bit there, so it doesn’t fail properly right now and you get `java.lang.NumberFormatException: empty String`, but normally you’d get a `Parsed.Failure` back ↩︎
6. Console ↩︎
7. Ammonite ↩︎