-
Star
(117)
You must be signed in to star a gist -
Fork
(32)
You must be signed in to fork a gist
-
-
Save jdegoes/1b43f43e2d1e845201de853815ab3cb9 to your computer and use it in GitHub Desktop.
| 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) | |
| } |
You should start a Twitch channel ;)
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.
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)
@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)?
@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.
@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:
mkdir classesscalac -d classes fpmax.scalascala -cp classes fpmax.App0
Or you can simply go one directory up after running scalac and provide it as classpath folder (-cp) instead ofclasses.
In general, it's simpler to use Intellij for all this stuff :)
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.
Original video: https://www.youtube.com/watch?v=sxudIMiOo68
Here is a Kotlin implementation: https://github.com/enhan/fptothemax/tree/master/src/main/kotlin/eu/enhan/fptothemax
@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)
}
This is a really nice and well declarative programming. Thanks, @jdegoes