Created
September 3, 2019 10:32
-
-
Save gustavofranke/9d8fd29f78bacf2cd91e8644c053eab8 to your computer and use it in GitHub Desktop.
This file contains hidden or 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.io.StdIn.readLine | |
import scala.util.Try | |
/** | |
* Trivial code, anyone would understand what's going on here. | |
* But requirements change, and we never know in which direction the change will be. | |
* | |
* The code has a procedural and interactive nature, how would you go about this in another, let alone better way? | |
* Still, it has bugs; so let's find them and solve them first | |
*/ | |
object App0 extends App { | |
def main(): Unit = { | |
println("What is your name?") | |
val name = readLine() | |
println("Hello, " + name + ", welcome to the game!") | |
var exec = true | |
while (exec) { | |
val num = scala.util.Random.nextInt(5) + 1 | |
println("Dear " + name + ", please guess a number from 1 to 5:") | |
val guess = readLine().toInt | |
if (guess == num) println("You guessed right, " + name + "!") | |
else println("You guessed wrong, " + name + "! The number was: " + num) | |
println("Do you want to continue, " + name + "?") | |
readLine() match { | |
case "y" => exec = true | |
case "n" => exec = false | |
} | |
} | |
} | |
main() | |
} | |
/** | |
* Solve first bug | |
*/ | |
object App1 extends App { | |
def main(): Unit = { | |
println("What is your name?") | |
val name = readLine() | |
println("Hello, " + name + ", welcome to the game!") | |
var exec = true | |
while (exec) { | |
val num = scala.util.Random.nextInt(5) + 1 | |
println("Dear " + name + ", please guess a number from 1 to 5:") | |
val guess = readLine().toInt | |
if (guess == num) println("You guessed right, " + name + "!") | |
else println("You guessed wrong, " + name + "! The number was: " + num) | |
var cont = true | |
while (cont) { | |
cont = false | |
println("Do you want to continue, " + name + "?") | |
readLine() match { | |
case "y" => exec = true | |
case "n" => exec = false | |
case _ => cont = true | |
} | |
} | |
} | |
} | |
main() | |
} | |
/** | |
* Solve second bug | |
*/ | |
object App2 extends App { | |
def main(): Unit = { | |
println("What is your name?") | |
val name = readLine() | |
println("Hello, " + name + ", welcome to the game!") | |
var exec = true | |
while (exec) { | |
val num = scala.util.Random.nextInt(5) + 1 | |
println("Dear " + name + ", please guess a number from 1 to 5:") | |
try { | |
val guess = readLine().toInt | |
if (guess == num) println("You guessed right, " + name + "!") | |
else println("You guessed wrong, " + name + "! The number was: " + num) | |
} catch { | |
case _: Exception => | |
println("should be an int") | |
} | |
var cont = true | |
while (cont) { | |
cont = false | |
println("Do you want to continue, " + name + "?") | |
readLine() match { | |
case "y" => exec = true | |
case "n" => exec = false | |
case _ => cont = true | |
} | |
} | |
} | |
} | |
main() | |
} | |
/** | |
* The code doesn't seem so easy to understand now | |
* | |
* On the one hand, we know that we can re-state some side effects: | |
* 1. Partiality => Option | |
* 2. Failures => Either | |
* 3. Yield >1 answer => List | |
* 4. Dependency injection => Reader | |
* 5. Structured logging => Writer | |
* 6. In place Mutations => State | |
* | |
* On the other hand, even though every line in this program IS a side effect, and interactive as well, | |
* every action is taken after making a decision based on the previous one. | |
* So, none of the above seems to help... we need something else | |
* | |
* But for now, lets start refactoring, the old school way, by breaking it to smaller chunks. | |
* | |
* Let's add and use a `parseInt` function | |
*/ | |
object App3 extends App { | |
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption | |
def main(): Unit = { | |
println("What is your name?") | |
val name = readLine() | |
println("Hello, " + name + ", welcome to the game!") | |
var exec = true | |
while (exec) { | |
val num = scala.util.Random.nextInt(5) + 1 | |
println("Dear " + name + ", please guess a number from 1 to 5:") | |
parseInt(readLine()).fold(println("should be an int"))(guess => | |
if (guess == num) println("You guessed right, " + name + "!") | |
else println("You guessed wrong, " + name + "! The number was: " + num) | |
) | |
var cont = true | |
while (cont) { | |
cont = false | |
println("Do you want to continue, " + name + "?") | |
readLine() match { | |
case "y" => exec = true | |
case "n" => exec = false | |
case _ => cont = true | |
} | |
} | |
} | |
} | |
main() | |
} | |
/** | |
* Lets separate the `gameLoop` from the rest of the program | |
*/ | |
object App4 extends App { | |
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption | |
def gameLoop(name: String): Unit = { | |
var exec = true | |
while (exec) { | |
val num = scala.util.Random.nextInt(5) + 1 | |
println("Dear " + name + ", please guess a number from 1 to 5:") | |
parseInt(readLine()).fold(println("should be an int"))(guess => | |
if (guess == num) println("You guessed right, " + name + "!") | |
else println("You guessed wrong, " + name + "! The number was: " + num) | |
) | |
var cont = true | |
while (cont) { | |
cont = false | |
println("Do you want to continue, " + name + "?") | |
readLine() match { | |
case "y" => exec = true | |
case "n" => exec = false | |
case _ => cont = true | |
} | |
} | |
} | |
} | |
def main(): Unit = { | |
println("What is your name?") | |
val name = readLine() | |
println("Hello, " + name + ", welcome to the game!") | |
gameLoop(name) | |
} | |
main() | |
} | |
/** | |
* Replace `gameLoop`'s `while` loop with recursion | |
*/ | |
object App5 extends App { | |
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption | |
def gameLoop(name: String): Unit = { | |
// var exec = true | |
// while (exec) { | |
val num = scala.util.Random.nextInt(5) + 1 | |
println("Dear " + name + ", please guess a number from 1 to 5:") | |
parseInt(readLine()).fold(println("should be an int"))(guess => | |
if (guess == num) println("You guessed right, " + name + "!") | |
else println("You guessed wrong, " + name + "! The number was: " + num) | |
) | |
var cont = true | |
while (cont) { | |
cont = false | |
println("Do you want to continue, " + name + "?") | |
readLine() match { | |
case "y" => gameLoop(name) | |
case "n" => sys.exit() | |
case _ => cont = true | |
} | |
} | |
// } | |
} | |
def main(): Unit = { | |
println("What is your name?") | |
val name = readLine() | |
println("Hello, " + name + ", welcome to the game!") | |
gameLoop(name) | |
} | |
main() | |
} | |
/** | |
* Lets separate the `checkContinue` from the rest of the program | |
*/ | |
object App6 extends App { | |
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption | |
def gameLoop(name: String): Unit = { | |
val num = scala.util.Random.nextInt(5) + 1 | |
println("Dear " + name + ", please guess a number from 1 to 5:") | |
parseInt(readLine()).fold(println("should be an int"))(guess => | |
if (guess == num) println("You guessed right, " + name + "!") | |
else println("You guessed wrong, " + name + "! The number was: " + num) | |
) | |
checkContinue(name) | |
} | |
def checkContinue(name: String): Unit = { | |
var cont = true | |
while (cont) { | |
cont = false | |
println("Do you want to continue, " + name + "?") | |
readLine() match { | |
case "y" => gameLoop(name) | |
case "n" => sys.exit() | |
case _ => cont = true | |
} | |
} | |
} | |
def main(): Unit = { | |
println("What is your name?") | |
val name = readLine() | |
println("Hello, " + name + ", welcome to the game!") | |
gameLoop(name) | |
} | |
main() | |
} | |
/** | |
* Replace `checkContinue`'s `while` loop with recursion | |
*/ | |
object App7 extends App { | |
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption | |
def gameLoop(name: String): Unit = { | |
val num = scala.util.Random.nextInt(5) + 1 | |
println("Dear " + name + ", please guess a number from 1 to 5:") | |
parseInt(readLine()).fold(println("should be an int"))(guess => | |
if (guess == num) println("You guessed right, " + name + "!") | |
else println("You guessed wrong, " + name + "! The number was: " + num) | |
) | |
checkContinue(name) | |
} | |
def checkContinue(name: String): Unit = { | |
// var cont = true | |
// while (cont) { | |
// cont = false | |
println("Do you want to continue, " + name + "?") | |
readLine() match { | |
case "y" => gameLoop(name) | |
case "n" => sys.exit() | |
case _ => checkContinue(name) | |
} | |
// } | |
} | |
def main(): Unit = { | |
println("What is your name?") | |
val name = readLine() | |
println("Hello, " + name + ", welcome to the game!") | |
gameLoop(name) | |
} | |
main() | |
} | |
/** | |
* Push `checkContinue`'s side effect to `gameLoop` | |
*/ | |
object App8 extends App { | |
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption | |
def gameLoop(name: String): Unit = { | |
val num = scala.util.Random.nextInt(5) + 1 | |
println("Dear " + name + ", please guess a number from 1 to 5:") | |
parseInt(readLine()).fold(println("should be an int"))(guess => | |
if (guess == num) println("You guessed right, " + name + "!") | |
else println("You guessed wrong, " + name + "! The number was: " + num) | |
) | |
if (checkContinue(name)) gameLoop(name) else sys.exit() | |
} | |
def checkContinue(name: String): Boolean = { | |
println("Do you want to continue, " + name + "?") | |
readLine() match { | |
case "y" => true // gameLoop(name) | |
case "n" => false // sys.exit() | |
case _ => checkContinue(name) | |
} | |
} | |
def main(): Unit = { | |
println("What is your name?") | |
val name = readLine() | |
println("Hello, " + name + ", welcome to the game!") | |
gameLoop(name) | |
} | |
main() | |
} | |
/** | |
* On the one hand, we know that we can re-state some side effects: | |
* 1. Partiality => Option | |
* 2. Failures => Either | |
* 3. Yield >1 answer => List | |
* 4. Dependency injection => Reader | |
* 5. Structured logging => Writer | |
* 6. In place Mutations => State | |
* | |
* On the other hand, even though every line in this program IS a side effect, and interactive as well, | |
* every action is taken after making a decision based on the previous one. | |
* So, none of the above seems to help... we need something else | |
* | |
* FP is not about not having side effects, is about pushing them to the same level, the same "layer" | |
* To do so, we can make use of a very simple idea. | |
* We know functions are: | |
* 1. Idempotent, they always yield the same result | |
* 2. Total, they always return a value | |
* 3. No side effects | |
* | |
* {{{ | |
* The simple idea is this, using Scala's stdlib's random generator: | |
* scala> scala.util.Random.nextInt | |
* res0: Int = 1006322705 | |
* | |
* scala> scala.util.Random.nextInt | |
* res1: Int = -1256952748 | |
* | |
* scala> scala.util.Random.nextInt | |
* res2: Int = 1021545177 | |
* | |
* scala> scala.util.Random.nextInt | |
* res3: Int = 949318781 | |
* }}} | |
* That clearly breaks referential transparency. | |
* | |
* If we returned a function, like: | |
* {{{ | |
* scala> () => scala.util.Random.nextInt | |
* res4: () => Int = $$Lambda$6295/1092897090@75b7c961 | |
* | |
* scala> () => scala.util.Random.nextInt | |
* res5: () => Int = $$Lambda$6296/1888814249@20b99485 | |
* | |
* scala> () => scala.util.Random.nextInt | |
* res6: () => Int = $$Lambda$6297/491271379@68d49dcd | |
* }}} | |
* | |
* Every time I re-run that in the REPL, Scala returns a new instance of it's function runtime representation. | |
* | |
* But if, say I store it in a val, like: | |
* {{{ | |
* scala> val a = () => scala.util.Random.nextInt | |
* a: () => Int = $$Lambda$6300/1535219728@73bda8eb | |
* }}} | |
* | |
* And then evaluate it, I get something that is idempotent, total, and wraps the side effect... | |
* this does not break referential transparency | |
* {{{ | |
* scala> a | |
* res7: () => Int = $$Lambda$6300/1535219728@73bda8eb | |
* | |
* scala> a | |
* res8: () => Int = $$Lambda$6300/1535219728@73bda8eb | |
* | |
* scala> a | |
* res9: () => Int = $$Lambda$6300/1535219728@73bda8eb | |
* | |
* scala> a | |
* res10: () => Int = $$Lambda$6300/1535219728@73bda8eb | |
* }}} | |
* | |
* This separates the description of the computation, from its execution, | |
* and allows me to push the side effect to other layers of my app | |
* | |
* {{{ | |
* scala> a() | |
* res11: Int = 1691894378 | |
* | |
* scala> a() | |
* res12: Int = 840239133 | |
* | |
* scala> a() | |
* res13: Int = -738961929 | |
* }}} | |
* | |
* All we need now, is a way to chain and change these things. | |
*/ | |
case class IO[A](unsafeRun: () => A) { self => | |
def map[B](f: A => B): IO[B] = IO(() => f(self.unsafeRun())) | |
def flatMap[B](f: A => IO[B]): IO[B] = IO(() => f(self.unsafeRun()).unsafeRun()) | |
} | |
object IO { | |
def pure[A](a: A): IO[A] = IO(() => a) | |
} | |
/** | |
* wrap side effects by creating instances of `IO` returned by the following functions: | |
* `putStrLn`, `getStrLn` and `nextInt` | |
*/ | |
object App9 extends App { | |
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption | |
def putStrLn(line: String): IO[Unit] = IO(() => println(line)) | |
def getStrLn: IO[String] = IO(() => readLine()) | |
def nextInt(upper: Int) = IO(() => scala.util.Random.nextInt(upper)) | |
def gameLoop(name: String): Unit = { | |
val num = scala.util.Random.nextInt(5) + 1 | |
println("Dear " + name + ", please guess a number from 1 to 5:") | |
parseInt(readLine()).fold(println("should be an int"))(guess => | |
if (guess == num) println("You guessed right, " + name + "!") | |
else println("You guessed wrong, " + name + "! The number was: " + num) | |
) | |
if (checkContinue(name)) gameLoop(name) else sys.exit() | |
} | |
def checkContinue(name: String): Boolean = { | |
println("Do you want to continue, " + name + "?") | |
readLine() match { | |
case "y" => true | |
case "n" => false | |
case _ => checkContinue(name) | |
} | |
} | |
def main(): Unit = { | |
println("What is your name?") | |
val name = readLine() | |
println("Hello, " + name + ", welcome to the game!") | |
gameLoop(name) | |
} | |
main() | |
} | |
/** | |
* Use `putStrLn`, `getStrLn` and `nextInt`. | |
* | |
* Because `IO` has `map` and `flatMap` we can use it in `for` comprehensions | |
* `main`, `gameLoop`, `checkContinue` will now wrap the side effect by returning instances of `IO` | |
* | |
* If we execute `main()` nothing will happen until we explicitly call `unsafeRun()` | |
* | |
* This code is purely functional, as the side effect occur at the same level, which is the boundaries of the program | |
* What we see in the code, is a "recipe" or "description" of our computations. | |
* | |
* However, as pure as this is, this code is still difficult to test, and the recursion is not tailrec, so it could | |
* overflow the stack. | |
*/ | |
object App10 extends App { | |
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption | |
def putStrLn(line: String): IO[Unit] = IO(() => println(line)) | |
def getStrLn: IO[String] = IO(() => readLine()) | |
def nextInt(upper: Int) = IO(() => scala.util.Random.nextInt(upper)) | |
def gameLoop(name: String): IO[Unit] = for { | |
num <- nextInt(5).map(_ + 1) | |
_ <- putStrLn("Dear " + name + ", please guess a number from 1 to 5:") | |
in <- getStrLn | |
_ <- parseInt(in).fold(putStrLn("should be an int"))(guess => | |
if (guess == num) putStrLn("You guessed right, " + name + "!") | |
else putStrLn("You guessed wrong, " + name + "! The number was: " + num) | |
) | |
cont <- checkContinue(name) | |
_ <- if (cont) gameLoop(name) else IO.pure(sys.exit()) | |
} yield () | |
def checkContinue(name: String): IO[Boolean] = for { | |
_ <- putStrLn("Do you want to continue, " + name + "?") | |
in <- getStrLn | |
cont <- in match { | |
case "y" => IO.pure(true) | |
case "n" => IO.pure(false) | |
case _ => checkContinue(name) | |
} | |
} yield cont | |
def main(): IO[Unit] = for { | |
_ <- putStrLn("What is your name?") | |
name <- getStrLn | |
_ <- putStrLn("Hello, " + name + ", welcome to the game!") | |
_ <- gameLoop(name) | |
} yield () | |
main().unsafeRun() | |
} |
Author
gustavofranke
commented
Jul 15, 2020
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment