Skip to content

Instantly share code, notes, and snippets.

@jdegoes
Created July 13, 2018 03:18
Show Gist options
  • Save jdegoes/1b43f43e2d1e845201de853815ab3cb9 to your computer and use it in GitHub Desktop.
Save jdegoes/1b43f43e2d1e845201de853815ab3cb9 to your computer and use it in GitHub Desktop.
FP to the Max — Code Examples
package fpmax
import scala.util.Try
import scala.io.StdIn.readLine
object App0 {
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
}
}
}
}
object App1 {
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption
trait Program[F[_]] {
def finish[A](a: => A): F[A]
def chain[A, B](fa: F[A], afb: A => F[B]): F[B]
def map[A, B](fa: F[A], ab: A => B): F[B]
}
object Program {
def apply[F[_]](implicit F: Program[F]): Program[F] = F
}
implicit class ProgramSyntax[F[_], A](fa: F[A]) {
def map[B](f: A => B)(implicit F: Program[F]): F[B] = F.map(fa, f)
def flatMap[B](afb: A => F[B])(implicit F: Program[F]): F[B] = F.chain(fa, afb)
}
def finish[F[_], A](a: => A)(implicit F: Program[F]): F[A] = F.finish(a)
trait Console[F[_]] {
def putStrLn(line: String): F[Unit]
def getStrLn: F[String]
}
object Console {
def apply[F[_]](implicit F: Console[F]): Console[F] = F
}
def putStrLn[F[_]: Console](line: String): F[Unit] = Console[F].putStrLn(line)
def getStrLn[F[_]: Console]: F[String] = Console[F].getStrLn
trait Random[F[_]] {
def nextInt(upper: Int): F[Int]
}
object Random {
def apply[F[_]](implicit F: Random[F]): Random[F] = F
}
def nextInt[F[_]](upper: Int)(implicit F: Random[F]): F[Int] = Random[F].nextInt(upper)
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 point[A](a: => A): IO[A] = IO(() => a)
implicit val ProgramIO = new Program[IO] {
def finish[A](a: => A): IO[A] = IO.point(a)
def chain[A, B](fa: IO[A], afb: A => IO[B]): IO[B] = fa.flatMap(afb)
def map[A, B](fa: IO[A], ab: A => B): IO[B] = fa.map(ab)
}
implicit val ConsoleIO = new Console[IO] {
def putStrLn(line: String): IO[Unit] = IO(() => println(line))
def getStrLn: IO[String] = IO(() => readLine())
}
implicit val RandomIO = new Random[IO] {
def nextInt(upper: Int): IO[Int] = IO(() => scala.util.Random.nextInt(upper))
}
}
case class TestData(input: List[String], output: List[String], nums: List[Int]) {
def putStrLn(line: String): (TestData, Unit) =
(copy(output = line :: output), ())
def getStrLn: (TestData, String) =
(copy(input = input.drop(1)), input.head)
def nextInt(upper: Int): (TestData, Int) =
(copy(nums = nums.drop(1)), nums.head)
def showResults = output.reverse.mkString("\n")
}
case class TestIO[A](run: TestData => (TestData, A)) { self =>
def map[B](ab: A => B): TestIO[B] =
TestIO(t => self.run(t) match { case (t, a) => (t, ab(a)) })
def flatMap[B](afb: A => TestIO[B]): TestIO[B] =
TestIO(t => self.run(t) match { case (t, a) => afb(a).run(t) })
def eval(t: TestData): TestData = run(t)._1
}
object TestIO {
def point[A](a: => A): TestIO[A] = TestIO(t => (t, a))
implicit val ProgramTestIO = new Program[TestIO] {
def finish[A](a: => A): TestIO[A] = TestIO.point(a)
def chain[A, B](fa: TestIO[A], afb: A => TestIO[B]): TestIO[B] = fa.flatMap(afb)
def map[A, B](fa: TestIO[A], ab: A => B): TestIO[B] = fa.map(ab)
}
implicit val ConsoleTestIO = new Console[TestIO] {
def putStrLn(line: String): TestIO[Unit] = TestIO(t => t.putStrLn(line))
def getStrLn: TestIO[String] = TestIO(t => t.getStrLn)
}
implicit val RandomTestIO = new Random[TestIO] {
def nextInt(upper: Int): TestIO[Int] = TestIO(t => t.nextInt(upper))
}
}
def checkContinue[F[_]: Program: Console](name: String): F[Boolean] =
for {
_ <- putStrLn("Do you want to continue, " + name + "?")
input <- getStrLn.map(_.toLowerCase)
cont <- input match {
case "y" => finish(true)
case "n" => finish(false)
case _ => checkContinue(name)
}
} yield cont
def printResults[F[_]: Console](input: String, num: Int, name: String): F[Unit] =
parseInt(input).fold(
putStrLn("You did not enter a number")
)(guess =>
if (guess == num) putStrLn("You guessed right, " + name + "!")
else putStrLn("You guessed wrong, " + name + "! The number was: " + num)
)
def gameLoop[F[_]: Program: Random: Console](name: String): F[Unit] =
for {
num <- nextInt(5).map(_ + 1)
_ <- putStrLn("Dear " + name + ", please guess a number from 1 to 5:")
input <- getStrLn
_ <- printResults(input, num, name)
cont <- checkContinue(name)
_ <- if (cont) gameLoop(name) else finish(())
} yield ()
def main[F[_]: Program: Random: Console]: F[Unit] =
for {
_ <- putStrLn("What is your name?")
name <- getStrLn
_ <- putStrLn("Hello, " + name + ", welcome to the game!")
_ <- gameLoop(name)
} yield ()
def mainIO: IO[Unit] = main[IO]
def mainTestIO: TestIO[Unit] = main[TestIO]
val TestExample =
TestData(
input = "John" :: "1" :: "n" :: Nil,
output = Nil,
nums = 0 :: Nil
)
def runTest = mainTestIO.eval(TestExample).showResults
/*
What is your name?
Hello, John, welcome to the game!
Dear John, please guess a number from 1 to 5:
You guessed right, John!
Do you want to continue, John?
*/
}
object App2 {
object stdlib {
trait Program[F[_]] {
def finish[A](a: A): F[A]
def chain[A, B](fa: F[A], afb: A => F[B]): F[B]
def map[A, B](fa: F[A], ab: A => B): F[B]
}
object Program {
def apply[F[_]](implicit F: Program[F]): Program[F] = F
}
implicit class ProgramSyntax[F[_], A](fa: F[A]) {
def map[B](ab: A => B)(implicit F: Program[F]): F[B] = F.map(fa, ab)
def flatMap[B](afb: A => F[B])(implicit F: Program[F]): F[B] = F.chain(fa, afb)
}
def finish[F[_], A](a: A)(implicit F: Program[F]): F[A] = F.finish(a)
final case class IO[A](unsafeRun: () => A) { self =>
final def map[B](f: A => B): IO[B] = IO(() => f(self.unsafeRun()))
final def flatMap[B](f: A => IO[B]): IO[B] =
IO(() => f(self.unsafeRun()).unsafeRun())
}
object IO {
def point[A](a: => A): IO[A] = IO(() => a)
implicit val ProgramIO = new Program[IO] {
def finish[A](a: A): IO[A] = IO.point(a)
def chain[A, B](fa: IO[A], afb: A => IO[B]): IO[B] = fa.flatMap(afb)
def map[A, B](fa: IO[A], ab: A => B): IO[B] = fa.map(ab)
}
}
trait Console[F[_]] {
def putStrLn(line: String): F[Unit]
def getStrLn: F[String]
}
object Console {
def apply[F[_]](implicit F: Console[F]): Console[F] = F
implicit val ConsoleIO = new Console[IO] {
def putStrLn(line: String): IO[Unit] = IO(() => println(line))
def getStrLn: IO[String] = IO(() => readLine())
}
}
def putStrLn[F[_]: Console](line: String): F[Unit] = Console[F].putStrLn(line)
def getStrLn[F[_]: Console]: F[String] = Console[F].getStrLn
trait Random[F[_]] {
def nextInt(upper: Int): F[Int]
}
object Random {
def apply[F[_]](implicit F: Random[F]): Random[F] = F
implicit val RandomIO = new Random[IO] {
def nextInt(upper: Int): IO[Int] = IO(() => scala.util.Random.nextInt(upper))
}
}
def nextInt[F[_]: Random](upper: Int): F[Int] = Random[F].nextInt(upper)
}
import stdlib._
case class TestData(input: List[String], output: List[String], nums: List[Int]) {
def showResults = output.reverse.mkString("\n")
def nextInt: (TestData, Int) = (copy(nums = nums.drop(1)), nums.head)
def putStrLn(line: String): (TestData, Unit) = (copy(output = line :: output), ())
def getStrLn: (TestData, String) = (copy(input = input.drop(1)), input.head)
}
case class TestIO[A](run: TestData => (TestData, A)) { self =>
def map[B](f: A => B): TestIO[B] =
TestIO(t => self.run(t) match { case (t, a) => (t, f(a)) })
def flatMap[B](f: A => TestIO[B]): TestIO[B] =
TestIO(t => self.run(t) match { case (t, a) => f(a).run(t) })
def eval(t: TestData): TestData = self.run(t)._1
}
object TestIO {
def point[A](a: => A): TestIO[A] = TestIO(t => (t, a))
implicit val RandomTestIO = new Random[TestIO] {
def nextInt(upper: Int): TestIO[Int] =
TestIO(t => t.nextInt)
}
implicit val ProgramTestIO = new Program[TestIO] {
def finish[A](a: A): TestIO[A] = TestIO.point(a)
def chain[A, B](fa: TestIO[A], afb: A => TestIO[B]): TestIO[B] = fa.flatMap(afb)
def map[A, B](fa: TestIO[A], ab: A => B): TestIO[B] = fa.map(ab)
}
implicit val ConsoleTestIO = new Console[TestIO] {
def putStrLn(line: String): TestIO[Unit] =
TestIO(t => t.putStrLn(line))
def getStrLn: TestIO[String] =
TestIO(t => t.getStrLn)
}
}
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption
def checkAnswer[F[_]: Console](name: String, num: Int, guess: Int): F[Unit] =
if (num == guess) putStrLn("You guessed right, " + name + "!")
else putStrLn("You guessed wrong, " + name + "! The number was: " + num)
def checkContinue[F[_]: Program: Console](name: String): F[Boolean] =
for {
_ <- putStrLn("Do you want to continue, " + name + "?")
choice <- getStrLn.map(_.toLowerCase)
cont <- if (choice == "y") finish(true)
else if (choice == "n") finish(false)
else checkContinue(name)
} yield cont
def gameLoop[F[_]: Program: Console: Random](name: String): F[Unit] =
for {
num <- nextInt(5).map(_ + 1)
_ <- putStrLn("Dear " + name + ", please guess a number from 1 to 5:")
guess <- getStrLn
_ <- parseInt(guess).fold(
putStrLn("That is not a valid selection, " + name + "!")
)((guess: Int) => checkAnswer(name, num, guess))
cont <- checkContinue(name)
_ <- if (cont) gameLoop(name) else finish(())
} yield ()
def main[F[_]: Program: Console: Random]: F[Unit] =
for {
_ <- putStrLn("What is your name?")
name <- getStrLn
_ <- putStrLn("Hello, " + name + ", welcome to the game!")
_ <- gameLoop(name)
} yield ()
def mainIO: IO[Unit] = main[IO]
def mainTestIO: TestIO[Unit] = main[TestIO]
val TestExample = TestData(
input = "john" :: "1" :: "n" :: Nil,
output = Nil,
nums = 0 :: Nil)
}
object App3 {
object stdlib {
trait Program[F[_]] {
def finish[A](a: A): F[A]
def chain[A, B](fa: F[A], afb: A => F[B]): F[B]
def map[A, B](fa: F[A], ab: A => B): F[B]
}
object Program {
def apply[F[_]](implicit F: Program[F]): Program[F] = F
}
implicit class ProgramSyntax[F[_], A](fa: F[A]) {
def map[B](ab: A => B)(implicit F: Program[F]): F[B] = F.map(fa, ab)
def flatMap[B](afb: A => F[B])(implicit F: Program[F]): F[B] = F.chain(fa, afb)
}
def finish[F[_], A](a: A)(implicit F: Program[F]): F[A] = F.finish(a)
final case class IO[A](unsafeRun: () => A) { self =>
final def map[B](f: A => B): IO[B] = IO(() => f(self.unsafeRun()))
final def flatMap[B](f: A => IO[B]): IO[B] =
IO(() => f(self.unsafeRun()).unsafeRun())
}
object IO {
def point[A](a: => A): IO[A] = IO(() => a)
implicit val ProgramIO = new Program[IO] {
def finish[A](a: A): IO[A] = IO.point(a)
def chain[A, B](fa: IO[A], afb: A => IO[B]): IO[B] = fa.flatMap(afb)
def map[A, B](fa: IO[A], ab: A => B): IO[B] = fa.map(ab)
}
}
sealed trait ConsoleOut {
def en: String
}
object ConsoleOut {
case class YouGuessedRight(name: String) extends ConsoleOut {
def en = "You guessed right, " + name + "!"
}
case class YouGuessedWrong(name: String, num: Int) extends ConsoleOut {
def en = "You guessed wrong, " + name + "! The number was: " + num
}
case class DoYouWantToContinue(name: String) extends ConsoleOut {
def en = "Do you want to continue, " + name + "?"
}
case class PleaseGuess(name: String) extends ConsoleOut {
def en = "Dear " + name + ", please guess a number from 1 to 5:"
}
case class ThatIsNotValid(name: String) extends ConsoleOut {
def en = "That is not a valid selection, " + name + "!"
}
case object WhatIsYourName extends ConsoleOut {
def en = "What is your name?"
}
case class WelcomeToGame(name: String) extends ConsoleOut {
def en = "Hello, " + name + ", welcome to the game!"
}
}
trait Console[F[_]] {
def putStrLn(line: ConsoleOut): F[Unit]
def getStrLn: F[String]
}
object Console {
def apply[F[_]](implicit F: Console[F]): Console[F] = F
implicit val ConsoleIO = new Console[IO] {
def putStrLn(line: ConsoleOut): IO[Unit] = IO(() => println(line.en))
def getStrLn: IO[String] = IO(() => readLine())
}
}
def putStrLn[F[_]: Console](line: ConsoleOut): F[Unit] = Console[F].putStrLn(line)
def getStrLn[F[_]: Console]: F[String] = Console[F].getStrLn
trait Random[F[_]] {
def nextInt(upper: Int): F[Int]
}
object Random {
def apply[F[_]](implicit F: Random[F]): Random[F] = F
implicit val RandomIO = new Random[IO] {
def nextInt(upper: Int): IO[Int] = IO(() => scala.util.Random.nextInt(upper))
}
}
def nextInt[F[_]: Random](upper: Int): F[Int] = Random[F].nextInt(upper)
}
import stdlib._
case class TestData(input: List[String], output: List[ConsoleOut], nums: List[Int]) {
def showResults = output.reverse.map(_.en).mkString("\n")
def nextInt: (TestData, Int) = (copy(nums = nums.drop(1)), nums.head)
def putStrLn(line: ConsoleOut): (TestData, Unit) = (copy(output = line :: output), ())
def getStrLn: (TestData, String) = (copy(input = input.drop(1)), input.head)
}
case class TestIO[A](run: TestData => (TestData, A)) { self =>
def map[B](f: A => B): TestIO[B] =
TestIO(t => self.run(t) match { case (t, a) => (t, f(a)) })
def flatMap[B](f: A => TestIO[B]): TestIO[B] =
TestIO(t => self.run(t) match { case (t, a) => f(a).run(t) })
def eval(t: TestData): TestData = self.run(t)._1
}
object TestIO {
def point[A](a: => A): TestIO[A] = TestIO(t => (t, a))
implicit val RandomTestIO = new Random[TestIO] {
def nextInt(upper: Int): TestIO[Int] =
TestIO(t => t.nextInt)
}
implicit val ProgramTestIO = new Program[TestIO] {
def finish[A](a: A): TestIO[A] = TestIO.point(a)
def chain[A, B](fa: TestIO[A], afb: A => TestIO[B]): TestIO[B] = fa.flatMap(afb)
def map[A, B](fa: TestIO[A], ab: A => B): TestIO[B] = fa.map(ab)
}
implicit val ConsoleTestIO = new Console[TestIO] {
def putStrLn(line: ConsoleOut): TestIO[Unit] =
TestIO(t => t.putStrLn(line))
def getStrLn: TestIO[String] =
TestIO(t => t.getStrLn)
}
}
def parseInt(s: String): Option[Int] = Try(s.toInt).toOption
def checkAnswer[F[_]: Console](name: String, num: Int, guess: Int): F[Unit] =
if (num == guess) putStrLn(ConsoleOut.YouGuessedRight(name))
else putStrLn(ConsoleOut.YouGuessedWrong(name, num))
def checkContinue[F[_]: Program: Console](name: String): F[Boolean] =
for {
_ <- putStrLn(ConsoleOut.DoYouWantToContinue(name))
choice <- getStrLn.map(_.toLowerCase)
cont <- if (choice == "y") finish(true)
else if (choice == "n") finish(false)
else checkContinue(name)
} yield cont
def gameLoop[F[_]: Program: Console: Random](name: String): F[Unit] =
for {
num <- nextInt(5).map(_ + 1)
_ <- putStrLn(ConsoleOut.PleaseGuess(name))
guess <- getStrLn
_ <- parseInt(guess).fold(
putStrLn(ConsoleOut.ThatIsNotValid(name))
)((guess: Int) => checkAnswer(name, num, guess))
cont <- checkContinue(name)
_ <- if (cont) gameLoop(name) else finish(())
} yield ()
def main[F[_]: Program: Console: Random]: F[Unit] =
for {
_ <- putStrLn(ConsoleOut.WhatIsYourName)
name <- getStrLn
_ <- putStrLn(ConsoleOut.WelcomeToGame(name))
_ <- gameLoop(name)
} yield ()
def mainIO: IO[Unit] = main[IO]
def mainTestIO: TestIO[Unit] = main[TestIO]
val TestExample = TestData(
input = "john" :: "1" :: "n" :: Nil,
output = Nil,
nums = 0 :: Nil)
}
@abdheshkumar
Copy link

This is a really nice and well declarative programming. Thanks, @jdegoes

@pivovarit
Copy link

You should start a Twitch channel ;)

@GMadorell
Copy link

This has been one of the best explanations of FP I've ever seen, congratulations! Would be nice to add some more complex stuff here :P. Something like database access, how to compose different queries to make them transactional, etc.

@vasily802
Copy link

vasily802 commented Mar 18, 2019

Thanks for a great talk, really inspiring and educating!
Question: how to run the snippet correctly?
$ scalac fpmax.scala compiles everything to a new folder 'fpmax', but then when I run $ cd fpmax && scala App0, I get:
Exception in thread "main" java.lang.NoClassDefFoundError: App0 (wrong name: fpmax/App0)

@halyph
Copy link

halyph commented Apr 3, 2019

@vasily802
it should be like this

def main(args: Array[String]): Unit = {
    mainIO
  }

but there is no output.

@jdegoes, could you please provide as a hint how to run these apps (e.g. App1 or App2)?

@kell18
Copy link

kell18 commented Apr 7, 2019

@halyph good try, but to get actual results you need to evaluate an IO - via IO.unsafeRun() method.
It's so-called an "end of the world" when we're interpreting our pure IO values into real-world side effects.

@kell18
Copy link

kell18 commented Apr 7, 2019

@vasily802 it's a JVM specific:
To run it like so you need to compile classes to separate inner folder with -d option for scalac. So it'd be:

  1. mkdir classes
  2. scalac -d classes fpmax.scala
  3. scala -cp classes fpmax.App0
    Or you can simply go one directory up after running scalac and provide it as classpath folder (-cp) instead of classes.

In general, it's simpler to use Intellij for all this stuff :)

@LeaveNhA
Copy link

LeaveNhA commented Dec 1, 2019

This has been one of the best explanations of FP I've ever seen, congratulations! Would be nice to add some more complex stuff here :P. Something like database access, how to compose different queries to make them transactional, etc.

Would be nice.

@ivan-moto
Copy link

ivan-moto commented Dec 7, 2019

@benbartuska
Copy link

@halyph good try, but to get actual results you need to evaluate an IO - via IO.unsafeRun() method. It's so-called an "end of the world" when we're interpreting our pure IO values into real-world side effects.

to expand on this, the correct syntax would be to put this into the two apps you wish to check:

def main(args: Array[String]): Unit = {
    mainIO.unsafeRun()
  }

Similarly, you can print the test results to the console like so:

def main(args: Array[String]): Unit = {
    println(runTest)
  }

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