Skip to content

Instantly share code, notes, and snippets.

@gustavofranke
Created September 3, 2019 10:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gustavofranke/9d8fd29f78bacf2cd91e8644c053eab8 to your computer and use it in GitHub Desktop.
Save gustavofranke/9d8fd29f78bacf2cd91e8644c053eab8 to your computer and use it in GitHub Desktop.
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()
}
@gustavofranke
Copy link
Author

  def putStrLn(line: String): IO[Unit] = IO(() =>println(line))
  def getStrLn: IO[String] = IO(() => readLine())

  val program = {
    putStrLn("What is your name?").flatMap { _ =>
      getStrLn.flatMap { name =>
        putStrLn("Hello, " + name + ", welcome to the game!")
      }
    }
  }

  val program2 = for {
    _    <- putStrLn("What is your name?")
    name <- getStrLn
    _    <- putStrLn("Hello, " + name + ", welcome to the game!")
  } yield ()
  program2.unsafeRun()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment