CLI StdIn Scala - scala

I have created a console-interface application as todo-list in Scala. My data access layer works with Slick 3 and my interface works using simple StdIn methods. But I have some troubles with reading lines. My main menu works fine while inner menu acts weirdly sometimes. In particular, when I enter a command for the first time I do not get any result just the same menu is displayed again. Then I enter any command and I get the result. And if I try to enter some command for the 3d time my program just stops with System.exit.
Here is the code for my interface:
object UserInterface {
def displayMainMenu(): Unit ={
println("Main menu:" + " \n1 - Login" + "\n2 - Exit")
println("\nChoose the operation you want to perform:")
val inputMainMenu = readInt()
buildMainMenu(inputMainMenu)
}
def buildMainMenu(inputNumber: Int) = inputNumber match {
case 1 => enterSystem()
case 2 => System.exit(0)
case _ => println("Your input was wrong. Try again"); displayMainMenu()
}
def enterSystem(): Unit ={
println("Input you login, please:")
val inputLogin = readLine()
println("Input you password, please:")
val inputPassword = readLine()
val checkLogin = Await.result(DAO.checkUserLogin(inputLogin, inputPassword), Duration.Inf).toString
val userId = DAO.selectUserId(inputLogin)
def changeOutputs(checkLogin: String):Unit = checkLogin match {
case "true" => println("You have successfully entered"); displayInnerMenu(); buildMenu(userId)
case "false" => println("Your input for login or password is wrong. Please, try again"); displayMainMenu()
case _ => println("Your input is wrong"); displayMainMenu()
}
changeOutputs(checkLogin)
}
def buildMenu(userId: Long): Unit ={
def chooseOption(number: Int):Unit = number match {
case 1 => displayFinishedTasks(userId)
case 2 => displayUnfinishedTasks(userId)
case 3 => addTask(userId)
case 4 => deleteTask()
case 5 => markTaskAsFinished(userId)
case 6 => displayMainMenu()
case _ => println("Your input is wrong"); displayMainMenu()
}
val inputNum = displayInnerMenu()
chooseOption(inputNum)
}
def displayInnerMenu():Int ={
println("TODO List:" + "\n1 - Display finished tasks" + "\n2 - Display unfinished tasks"
+ "\n3 - Add task" + "\n4 - Delete task" + "\n5 - Mark task as finished" + "\n6 - Get back to the main menu")
println("\nChoose the operation you want to perform:")
val inputNum = readInt()
inputNum
}
def displayAllTasks(id: Long) = {
println()
println("User's tasks:\n" + Await.result(DAO.selectTasksByUser(id), Duration.Inf).toList.toString)
displayInnerMenu()
}
def displayFinishedTasks(id: Long) = {
println()
println("User's finished tasks:\n" + Await.result(DAO.selectFinishedTasks(id), Duration.Inf).toList.toString)
displayInnerMenu()
}
def displayUnfinishedTasks(id: Long) = {
println()
println("User's unfinished tasks:\n" + Await.result(DAO.selectUnfinishedTasks(id), Duration.Inf).toList.toString)
displayInnerMenu()
}
def addTask(id: Long) = {
println()
println("Input the task name you want to create, please:")
val taskName = readLine()
Await.result(DAO.addTask(taskName, id), Duration.Inf)
displayInnerMenu()
}
def deleteTask() = {
println()
println("Choose the task you want to delete, please:")
val taskId = readLong()
Await.result(DAO.deleteTask(Some(taskId)), Duration.Inf)
displayInnerMenu()
}
def markTaskAsFinished(id: Long) = {
println()
println("Choose the task you want to mark as finished, please:")
val taskId = readLong()
Await.result(DAO.finishTask(Some(taskId), id), Duration.Inf)
displayInnerMenu()
}
}
What I want is some kind of infinite cycle so I could perform my commands as many times as I need or set the limit. So what changes I can introduce in this code? I would be very grateful for some help!

Your particular troubles seem to come from the fact that changeOutputs in
enterSystem calls displayInnerMenu which reads an Int from input but does nothing useful with it. Probably you should have called buildMenu in most of the places where displayInnerMenu is called.
Also it seems that you should improve your debugging skills. This is a crucial skill and this code is not that hard to debug.
Taken more broadly this is a complicated topic with no simple best answer. But there are certainly bad ones and unfortunately yours is one of those. The thing I don't like most in your code is big separation in the code between the menu item title and menu item action. (Just imagine what it takes to add new menu item in the middle. Or what would it take to create a deeper menu with some items shared between levels.) So I would re-write most of the code. Being more of an OOP-guy than a FP-guy, I would do something like this:
object UserInterface {
// should be non-generic for simplicity of the rest of the code
trait MenuAndStateNG {
def runMenu(): MenuAndStateNG
}
trait MenuItem[S] {
val name: String
def doAction(state: S, curMenu: MenuAndStateNG): MenuAndStateNG
}
case class Menu[S](header: String, items: Seq[MenuItem[S]]) {}
case class MenuAndState[S](menu: Menu[S], state: S) extends MenuAndStateNG {
def runMenu(): MenuAndStateNG = {
var inputNum: Int = -1
var isFirstRun = true
// we use 1-based indices in the menu
while (inputNum <= 0 || inputNum > menu.items.length) {
if (!isFirstRun) {
println("Your input was wrong. Try again")
}
isFirstRun = false
println(menu.header + ":")
println(menu.items.zipWithIndex.map({ case (item, index) => s"${index + 1} - ${item.name}" }).mkString("\n"))
println("Choose the operation you want to perform:")
inputNum = StdIn.readInt()
}
println()
val nextMenu = menu.items(inputNum - 1).doAction(state, this)
nextMenu
}
}
// most of menu items doesn't change current menu
// let's make it easier to implement
trait SimpleMenuItem[S] extends MenuItem[S] {
override def doAction(state: S, curMenu: MenuAndStateNG): MenuAndStateNG = {
doSimpleAction(state)
curMenu
}
def doSimpleAction(state: S): Unit
}
def start(): Unit = {
var curMenu: MenuAndStateNG = MenuAndState(mainMenu, ())
var isFirstRun = true
while (true) {
if (!isFirstRun) {
println
}
isFirstRun = false
curMenu = curMenu.runMenu()
}
}
private val loginItem = new MenuItem[Unit] {
override val name = "Login"
override def doAction(state: Unit, curMenu: MenuAndStateNG): MenuAndStateNG = {
println("Input you login, please:")
val inputLogin = StdIn.readLine()
println("Input you password, please:")
val inputPassword = StdIn.readLine()
val checkLogin = Await.result(DAO.checkUserLogin(inputLogin, inputPassword), Duration.Inf).toString
val userId = DAO.selectUserId(inputLogin)
checkLogin match {
case "true" =>
println("You have successfully entered")
MenuAndState(userMenu, userId)
case "false" =>
println("Your input for login or password is wrong. Please, try again")
curMenu
case _ =>
println("Your input is wrong")
curMenu
}
}
}
private val exitItem = new MenuItem[Unit] {
override val name = "Exit"
override def doAction(state: Unit, curMenu: MenuAndStateNG): MenuAndStateNG = {
System.exit(0)
null // null is bad but it doesn't matter by now
}
}
private val displayFinishedTasks = new SimpleMenuItem[Int] {
override val name: String = "Display finished tasks"
override def doSimpleAction(state: Int): Unit = {
println("User's finished tasks:\n" + Await.result(DAO.selectFinishedTasks(id), Duration.Inf).toList.toString)
}
}
private val displayUnfinishedTasks = new SimpleMenuItem[Int] {
override val name: String = "Display unfinished tasks"
override def doSimpleAction(state: Int): Unit = {
println("User's unfinished tasks:\n" + Await.result(DAO.selectUnfinishedTasks(id), Duration.Inf).toList.toString)
}
}
private val displayAllTasks = new SimpleMenuItem[Int] {
override val name: String = "Display all tasks"
override def doSimpleAction(state: Int): Unit = {
println("User's tasks:\n" + Await.result(DAO.selectTasksByUser(id), Duration.Inf).toList.toString)
}
}
private val addTask = new SimpleMenuItem[Int] {
override val name: String = "Add task"
override def doSimpleAction(state: Int): Unit = {
println("Input the task name you want to create, please:")
val taskName = readLine()
Await.result(DAO.addTask(taskName, id), Duration.Inf)
}
}
private val deleteTask = new SimpleMenuItem[Int] {
override val name: String = "Delete task"
override def doSimpleAction(state: Int): Unit = {
println("Choose the task you want to delete, please:")
val taskId = readLong()
Await.result(DAO.deleteTask(Some(taskId)), Duration.Inf)
}
}
private val markTaskFinished = new SimpleMenuItem[Int] {
override val name: String = "Mark task as finished"
override def doSimpleAction(state: Int): Unit = {
println("Choose the task you want to mark as finished, please:")
val taskId = readLong()
Await.result(DAO.finishTask(Some(taskId), id), Duration.Inf)
}
}
private val logoutTask = new MenuItem[Int] {
override val name = "Get back to the main menu"
override def doAction(state: Int, curMenu: MenuAndStateNG): MenuAndState[Unit] = {
MenuAndState(mainMenu, ())
}
}
val mainMenu: Menu[Unit] = Menu("Main menu", List(loginItem, exitItem))
val userMenu: Menu[Int] = Menu("User menu", List(
displayAllTasks,
displayFinishedTasks,
displayUnfinishedTasks,
addTask,
deleteTask,
markTaskFinished,
logoutTask))
}
The main ideas are following:
Join menu action and title into a single MenuItem
Let the MenuItem select next "menu state" (MenuAndState)
MenuAndState from the outside looks like MenuAndStateNG - i.e. something that just can be run to get next MenuAndStateNG. From the inside it is split into a "fixed part" (Menu) = header + list of items and "variable part" = state. By introducing this separation I was able to make userMenu actually a constant rather than def
Most of the menu items doesn't change menu and just return to their parent. To simplify code for this scenario curMenu is passed as an argument to the doAction and there is a SimpleMenuItem that just always returns it\
Given such design all you need is:
create a val for each menu item
create mainMenu and userMenu vals effectively as lists of those menu items
run an infinite loop starting from the mainMenu (done in start)
Note that because MenuAndStateNG returns the next MenuAndStateNG from its runMenu I can use an infinite loop instead of deepening the stack on each menu iteration (which is generally a bad idea).

Related

Scala stream and ExecutionContext issue

I'm new in Scala and i'm facing a few problems in my assignment :
I want to build a stream class that can do 3 main tasks : filter,map,and forEach.
My streams data is an array of elements. Each of the 3 main tasks should run in 2 different threads on my streams array.
In addition, I need to divde the logic of the action and its actual run to two different parts. First declare all tasks in stream and only when I run stream.run() I want the actual actions to happen.
My code :
class LearningStream[A]() {
val es: ExecutorService = Executors.newFixedThreadPool(2)
val ec = ExecutionContext.fromExecutorService(es)
var streamValues: ArrayBuffer[A] = ArrayBuffer[A]()
var r: Runnable = () => "";
def setValues(streamv: ArrayBuffer[A]) = {
streamValues = streamv;
}
def filter(p: A => Boolean): LearningStream[A] = {
var ls_filtered: LearningStream[A] = new LearningStream[A]()
r = () => {
println("running real filter..")
val (l,r) = streamValues.splitAt(streamValues.length/2)
val a:ArrayBuffer[A]=es.submit(()=>l.filter(p)).get()
val b:ArrayBuffer[A]=es.submit(()=>r.filter(p)).get()
ms_filtered.setValues(a++b)
}
return ls_filtered
}
def map[B](f: A => B): LearningStream[B] = {
var ls_map: LearningStream[B] = new LearningStream[B]()
r = () => {
println("running real map..")
val (l,r) = streamValues.splitAt(streamValues.length/2)
val a:ArrayBuffer[B]=es.submit(()=>l.map(f)).get()
val b:ArrayBuffer[B]=es.submit(()=>r.map(f)).get()
ls_map.setValues(a++b)
}
return ls_map
}
def forEach(c: A => Unit): Unit = {
r=()=>{
println("running real forEach")
streamValues.foreach(c)}
}
def insert(a: A): Unit = {
streamValues += a
}
def start(): Unit = {
ec.submit(r)
}
def shutdown(): Unit = {
ec.shutdown()
}
}
my main :
def main(args: Array[String]): Unit = {
var factorial=0
val s = new LearningStream[String]
s.filter(str=>str.startsWith("-")).map(s=>s.toInt*(-1)).forEach(i=>factorial=factorial*i)
for(i <- -5 to 5){
s.insert(i.toString)
}
println(s.streamValues)
s.start()
println(factorial)
}
The main prints only the filter`s output and the factorial isnt changed (still 1).
What am I missing here ?
My solution: #Levi Ramsey left a few good hints in the comments if you want to get hints and not the real solution.
First problem: Only one command (filter) run and the other didn't. solution: insert to the runnable of each command a call for the next stream via:
ec.submit(ms_map.r)
In order to be able to close all sessions, we need to add another LearningStream data member to the class. However we can't add just a regular LearningStream object because it depends on parameter [A]. Therefore, I implemented a trait that has the close function and my data member was of that trait type.

For comprehension not composing steps fully in unit test using Reader

I have a tagless final implementation with unit test, when I run the unit test only the first step is invoked not the rest.
Here is the test target:
class NameThing[F[_]: Monad](implicit console: Console[F]) {
def program: F[Unit] = for {
_ <- console.prompt
rawName <- console.read
fullName = parse(rawName)
_ <- console.display(fullName)
} yield ()
def parse(rawName:String):FullName = {
val parts = rawName.split(" ")
FullName(parts(0), parts(1))
}
}
The unit test is:
implicit object TestConsole extends Console[Test] {
override def prompt: Test[Unit] = {
println("ok1")
Reader(TestEnv => TestEnv.prompt)
}
override def read: Test[String] = {
println("ok2")
Reader(TestEnv => TestEnv.read)
}
override def display(fullName: FullName): Test[Unit] = {
println("ok3")
Reader(TestEnv => TestEnv.display(fullName.toString))
}
}
val result = new NameThing[Test]().program.run
I only see ok1 displayed.
Complete code here: https://bitbucket.org/jameskingconsulting/scala-effects
Try
new NameThing[Test]().program.run(TestEnv())
new NameThing[Test]().program.run is just a TestEnv => Unit (where .run is Kleisli's run), you should call it on a TestEnv to actually run the program.

Why does Future.firstCompletedOf not invoke callback on timeout?

I am doing Exercises from Learning Concurrent Programming in Scala.
For an exercise question in code comment.
Program prints proper output of HTML contents for proper URL and timeout sufficiently enough.
Program prints "Error occured" for proper URL and low timeout.
However for invalid URL "Error occured" is not printed. What is the problem with the code below?
/*
* Implement a command-line program that asks the user to input a URL of some website,
* and displays the HTML of that website. Between the time that the user hits ENTER and
* the time that the HTML is retrieved, the program should repetitively print a . to the
* standard output every 50 milliseconds, with a two seconds timeout. Use only futures
* and promises, and avoid the synchronization primitives from the previous chapters.
* You may reuse the timeout method defined in this chapter.
*/
object Excersices extends App {
val timer = new Timer()
def timeout(t: Long = 1000): Future[Unit] = {
val p = Promise[Unit]
val timer = new Timer(true)
timer.schedule(new TimerTask() {
override def run() = {
p success ()
timer cancel()
}
}, t)
p future
}
def printDot = println(".")
val taskOfPrintingDot = new TimerTask {
override def run() = printDot
}
println("Enter a URL")
val lines = io.Source.stdin.getLines()
val url = if (lines hasNext) Some(lines next) else None
timer.schedule(taskOfPrintingDot, 0L, 50.millisecond.toMillis)
val timeOut2Sec = timeout(2.second.toMillis)
val htmlContents = Future {
url map { x =>
blocking {
Source fromURL (x) mkString
}
}
}
Future.firstCompletedOf(Seq(timeOut2Sec, htmlContents)) map { x =>
timer cancel ()
x match {
case Some(x) =>
println(x)
case _ =>
println("Error occured")
}
}
Thread sleep 5000
}
As #Gábor Bakos said exception produces Failure which doesn't handled by map:
val fut = Future { Some(Source fromURL ("hhhttp://google.com")) }
scala> fut map { x => println(x) } //nothing printed
res12: scala.concurrent.Future[Unit] = scala.concurrent.impl.Promise$DefaultPromise#5e025724
To process failure - use recover method :
scala> fut recover { case failure => None } map { x => println(x) }
None
res13: scala.concurrent.Future[Unit] = scala.concurrent.impl.Promise$DefaultPromise#578afc83
In your context it's something like:
Future.firstCompletedOf(Seq(timeOut2Sec, htmlContents)) recover {case x => println("Error:" + x); None} map { x => ...}
The Complete Code after using recover as advised by #dk14:
object Exercises extends App {
val timer = new Timer()
def timeout(t: Long = 1000): Future[Unit] = {
val p = Promise[Unit]
val timer = new Timer(true)
timer.schedule(new TimerTask() {
override def run() = {
p success ()
timer cancel ()
}
}, t)
p future
}
def printDot = println(".")
val taskOfPrintingDot = new TimerTask {
override def run() = {
printDot
}
}
println("Enter a URL")
val lines = io.Source.stdin.getLines()
val url = if (lines hasNext) Some(lines next) else None
timer.schedule(taskOfPrintingDot, 0L, 50.millisecond.toMillis)
val timeOut2Sec = timeout(2.second.toMillis)
val htmlContents = Future {
url map { x =>
blocking {
Source fromURL (x) mkString
}
}
}
Future.firstCompletedOf(Seq(timeOut2Sec, htmlContents))
.recover { case x => println("Error:" + x); None }
.map { x =>
timer cancel ()
x match {
case Some(x) =>
println(x)
case _ =>
println("Timeout occurred")
}
}
Thread sleep 5000
}

How do I get an item in a list that matches a certain condition?

I have the following sample code :
package models
import java.util.concurrent.atomic.AtomicInteger
import scala.collection.mutable.ArrayBuffer
case class Task(id: Int, label: String)
object Task {
private val buffer = new ArrayBuffer[Task]
private val incrementer = new AtomicInteger()
def all(): List[Task] = buffer.toList
def create(label: String): Int = {
val newId = incrementer.incrementAndGet()
buffer += new Task(newId, label)
newId
}
def delete(id: Int): Boolean = {
// TODO : add code
}
}
In method delete I need to find a Task that has id equal to the parameter id and if one is found I need to remove it from the collection and return true from the method. Otherwise (if none is found) I should just return false.
I know how to do this in an imperative language such as C# or Java but Scala stumps me..
PS : The code is strictly used to understand the language and the platform, it sucks too much to be pushed in production. Don't worry.
This is one possible solution, however in this case I think it's also possible to switch to var + immutable ArrayBuffer and use filter. Also note that this code is not thread safe
import java.util.concurrent.atomic.AtomicInteger
import scala.collection.mutable.ArrayBuffer
case class Task(id: Int, label: String)
object Task {
private val buffer = new ArrayBuffer[Task]
private val incrementer = new AtomicInteger()
def all(): List[Task] = buffer.toList
def create(label: String): Int = {
val newId = incrementer.incrementAndGet()
buffer.append(Task(newId, label))
newId
}
def delete(id: Int): Boolean = {
buffer.
find(_.id == id). // find task by id
map(buffer -= _). // remove it from buffer
exists(_ => true) // the same as: map(_ => true).getOrElse(false)
}
}
val id1 = Task.create("aaa")
val id2 = Task.create("bbb")
println(s"Id1 = $id1 Id2 = $id2")
println(s"All = ${Task.all()}")
val deleted = Task.delete(id1)
println(s"Deleted = $deleted")
println(s"All = ${Task.all()}")
println(s"Not Deleted = ${Task.delete(123)}")

Making a while-loop more scala-esqe

It is quite clear what the loop below accomplishes. I somewhat think that it could be made more scala-esque, but I can't quite see it. I'm posting this to see if anyone has more inspiration than me.
var bunnies: List[Bunny] = List.fill(nBunniesAtStart)(generateInitialBunny)
var doContinue = true
while (doContinue) {
val prevBunnies = bunnies
bunnies = evolveOneYear(bunnies)
print(bunnies, prevBunnies)
if (bunnies.isEmpty) {
println("\n\n\n No more bunnies...")
doContinue = false
} else {
println("Hit Enter to continue, or q + Enter to quit.\n")
doContinue = readLine.isEmpty()
}
}
I wrote this code as an answer to a codereview post.
EDIT:
thanks to #wingedsubmariner and #Kigyo, I have this alternative:
val startBunnies: List[Bunny] = List.fill(nBunniesAtStart)(generateInitialBunny)
userInputLoop(startBunnies, "")
#tailrec
def userInputLoop(bunnies: List[Bunny], userInput: String): Unit = {
if (userInput.nonEmpty) println("Exiting")
else evolveOneYear(bunnies) match {
case Nil =>
print(Nil, bunnies)
println("No more bunnies...")
case newBunnies =>
print(newBunnies, bunnies)
userInputLoop(newBunnies, readLine())
}
}
Or
val startBunnies: List[Bunny] = List.fill(nBunniesAtStart)(generateInitialBunny)
userInputLoop(startBunnies)
#tailrec
def userInputLoop(prevBunnies: List[Bunny]): Unit = {
evolveOneYear(prevBunnies) match {
case Nil =>
print(Nil, prevBunnies)
println("No more bunnies...")
case bunnies =>
print(bunnies, prevBunnies)
if (readLine().nonEmpty) println("Exiting.")
else userInputLoop(bunnies)
}
}
EDIT 2:
Another attempt, build from some ideas of Chris Martin and Ben Kovitz:
class NoMoreBunniesException extends Exception("No more bunnies...")
class UserStoppageException extends Exception("Exiting at your request.")
def doesUserWantToContinue(): Try[_] = {
println("Hit Enter to continue, or q + Enter to quit.\n");
if (readLine().isEmpty) Success() else Failure(new UserStoppageException)
}
def validateBunnies(bunnies: List[Bunny]): Try[_] = {
if (bunnies.isEmpty) Failure(new NoMoreBunniesException)
else Success()
}
def checkNotEmptyAndUserContinuation(bunnies: List[Bunny]): Try[_] =
validateBunnies(bunnies).flatMap(_ => doesUserWantToContinue)
val firstBunnies = List.fill(nBunniesAtStart)(generateInitialBunny)
println(s"${buildBunniesReport(firstBunnies).toString}\n\n")
val timeline = Stream.iterate(firstBunnies)(evolveOneYear)
val timelineWithPrev = timeline.tail.zip(timeline)
val statusAndReportTimeline = timelineWithPrev.map {
case (bunnies, prevBunnies) =>
(checkNotEmptyAndUserContinuation(bunnies), buildFullReport(bunnies, prevBunnies))
}
// main loop including user interaction
statusAndReportTimeline.takeWhile {
case (Success(_), _) => true
case (Failure(e), report) => { println(s"${report}\n\n${e.getMessage}"); false }
}.foreach { case (_, report) => println(report) }
Here's a solution that's more functional (perhaps also more abstruse):
val firstBunnies = List.fill(nBunniesAtStart)(generateInitialBunny)
val timeline = Stream.iterate(firstBunnies)(evolveOneYear)
val inputLines = Source.fromInputStream(System.in).getLines()
timeline.zip(timeline.tail).iterator
.takeWhile({ case (previousBunnies, bunnies) => previousBunnies.nonEmpty })
.zip(Iterator.single("") ++ inputLines)
.takeWhile({ case (_, input) => input.isEmpty })
.map({ case ((previousBunnies, bunnies), _) =>
(bunnies, previousBunnies) + (
if (bunnies.isEmpty) "No more bunnies..."
else "Hit Enter to continue, or q + Enter to quit."
)
})
.foreach(println)
You can make it more idiomatic scala by using a tail-recursive function instead of a while loop and eliminiating the vars:
import scala.annotation.tailrec
val startBunnies = List.fill(nBunniesAtStart)(generateInitialBunny)
#tailrec
def loop(prevBunnies: List[Bunny]): Unit = {
val bunnies = evolveOneYear(prevBunnies)
print(bunnies, prevBunnies)
if (bunnies.isEmpty) {
println("\n\n\n No more bunnies...")
} else {
println("Hit Enter to continue, or q + Enter to quit.\n")
if (readLine.isEmpty)
loop(bunnies)
}
}
loop(startBunnies)
// Separate the functional logic ...
val firstBunnies = List.fill(nBunniesAtStart)(generateInitialBunny)
val timeline = Stream.iterate(firstBunnies)(evolveOneYear)
for ((previousBunnies, bunnies) <- timeline zip timeline.tail) {
// ... from the I/O.
print(bunnies, previousBunnies)
if (bunnies.isEmpty) {
println("No more bunnies...")
return
} else {
println("Hit Enter to continue, or q + Enter to quit.")
}
if (readLine().nonEmpty) return
}
Here's yet another way to do it, aiming for clarity and simplicity.
import util.control.Breaks._
val bunniesStream: Stream[List[String]] = firstBunnies #:: bunniesStream.map(evolveOneYear)
breakable {
for (bunnies <- bunniesStream) {
println(bunnies)
if (bunnies.isEmpty) {
println("No more bunnies...");
break
} else {
println("Hit Enter to continue, or q + Enter to quit.\n");
if (!readLine().isEmpty) break
}
}
}
The use of breakable suggests that this is not idiomatic Scala code.
The more I consider this, the more I'm bothered by the idea of blocking on the InputStream over a human timescale. So here's an approach using Akka!
The setup:
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
// The actor system
val system = ActorSystem()
// The actors: a bunny farm, and a console to interact with it
val farm = system.actorOf(Props[BunnyFarm])
system.actorOf(Props(classOf[BunnyConsole], farm))
// Keep alive until the actor system terminates
system.awaitTermination()
The bunny farm:
object BunnyFarm {
// Define the messages that a bunny farm uses
case object Advance
case class Result(bunnies: Seq[Bunny], previousBunnies: Seq[Bunny])
}
class BunnyFarm extends Actor {
import BunnyFarm._
// A bunny farm's state consists of a list of its bunnies
var bunnies = List.fill(nBunniesAtStart)(generateInitialBunny)
def receive = {
case Advance =>
// Advance the state of the farm one year
val previousBunnies = bunnies
bunnies = evolveOneYear(bunnies)
// Reply to the sender with the result
sender ! Result(bunnies = bunnies, previousBunnies = previousBunnies)
}
}
The console interface:
class BunnyConsole(farm: ActorRef) extends Actor with akka.camel.Consumer {
// Read from stdin
def endpointUri = "stream:in"
// Initially advance the farm once
farm ! BunnyFarm.Advance
def receive = {
case m: akka.camel.CamelMessage => self forward m.bodyAs[String]
// Each string message represents a line of user input
case s: String => s match {
case "" => farm ! BunnyFarm.Advance
case _ => quit()
}
// When the bunny farm sends a result...
case r: BunnyFarm.Result =>
println(s"Previous bunnies: ${r.previousBunnies}")
println(s"New bunnies: ${r.bunnies}")
if (r.bunnies.nonEmpty) {
println("Hit Enter to continue, or q + Enter to quit.")
} else {
println("No more bunnies...")
quit()
}
}
// Terminate the actor system, thus halting the program
def quit() = context.system.shutdown()
}
Dependencies:
com.typesafe.akka:akka-actor
com.typesafe.akka:akka-camel
org.apache.camel:camel-stream
Edit - The same solution refactored for brevity.
Setup:
import akka.actor.{Actor, ActorSystem, Props}
val system = ActorSystem()
system.actorOf(Props(classOf[BunnyConsole]))
system.awaitTermination()
Console:
class BunnyConsole extends Actor with akka.camel.Consumer {
def endpointUri = "stream:in"
var bunnies = List.fill(nBunniesAtStart)(generateInitialBunny)
advance()
def receive = {
case m: akka.camel.CamelMessage => m.bodyAs[String] match {
case "" => advance()
case _ => quit()
}
}
def advance() {
val previousBunnies = bunnies
bunnies = evolveOneYear(bunnies)
print(bunnies, previousBunnies)
if (bunnies.nonEmpty) {
println("Hit Enter to continue, or q + Enter to quit.")
} else {
println("No more bunnies...")
quit()
}
}
def quit() = context.system.shutdown()
}
Here's yet another approach. It's horrible. I post it here in the hope that it will inspire someone to re-do its essential idea in a more readable or at least conventional way.
val bunniesStream = Stream.iterate(firstBunnies)(evolveOneYear)
case class Interaction(bunnies: List[String]) {
lazy val print: Unit = println(bunnies)
lazy val continue: Boolean = {
print
if (bunnies.isEmpty) { println("No more bunnies..."); false }
else userOK
}
lazy val userOK: Boolean = {
println("Hit Enter to continue, or q + Enter to quit.\n");
readLine().isEmpty
}
}
bunniesStream.map(Interaction).takeWhile(_.continue).force
The idea that I'm trying to implement here is to get the user's input by lazy evaluation. What makes it hard to do the loop in a functional style is the fact that you don't know when to stop the loop until after you've read the user's input, but you might need to stop the loop before reading the user's input.
The last line bundles all the input and output into an expression. Without .force, that expression should evaluate to an object which you can then pass to other functions as you like. This seems to follow the spirit of functional programming. No input or output should happen until you do a .force. Except it does, because there's something fundamentally wrong with my approach. I don't quite know what it is. Maybe the error has something to do with my mixing decision-making with input/output in the Interaction class.