I am currently trying to teach myself scala by rewriting projects from my intro CS class, which was taught in C++. In addition to just learning the syntax I am trying to gain understanding of how functional programming works and what is considered good functional programming style.
Below I've included both the original code, which is a simple while loop that continues looping until the user selects the 'quit' option, and my attempt at a functional equivalent. Have I done this correctly? What could be improved?
Original imperative version in C++:
int main() { int option = -1; while (option != 3) { cout << fixed; cout << "Please select one of the following:" << "\n1 - one" << "\n2 - two" << "\n3 - quit" << endl; cin >> option; if (option == 1) { cout << "selected 1" << endl; } else if (option == 2) { cout << "selected 2" << endl; } else if (option == 3) { cout << "selected quit.\n"; } else { cout << "Sorry, that command is not recognized. "; } } return 0; }
My attempt at a functional version in scala:
def menu(option: Int) {
println("""|Please select one of the following:
| 1 - one
| 2 - two
| 3 - quit""".stripMargin)
if (option == 1) {
println("selected 1")
val opt = StdIn.readInt
menu(opt)
}
else if (option == 2) {
println("selected 2")
val opt = StdIn.readInt
menu(opt)
}
else if (option == 3) {
println("selected quit")
}
else {
println("Sorry, that command is not recognized")
}
}
def main(args: Array[String]) {
println("""|Please select one of the following:
| 1 - one
| 2 - two
| 3 - quit""".stripMargin)
val opt = StdIn.readInt
menu(opt)
}
3 Answers 3
One of the Scala features you need to get used to using is match
- it is your friend. One of your very best friends.
Also, the way you have defined menu
is recursive, with no bounding. In this particular case, it's probably not an issue, but it is, in general a Really Bad Idea.
Finally, you have repeated the printing of the menu and reading of the option. In general, one should follow Don't Repeat Yourself.
So how about something like the code below. Note, one of the rare cases tailor made for do...while
.
def menu(option: Int): Boolean = {
option match {
case 1 =>
println("selected 1")
true
case 2 =>
println("selected 2")
true
case 3 =>
println("selected quit")
false
case _ => // the else case
println("Sorry, that command is not recognized")
true
}
}
def readOption: Int = {
println("""|Please select one of the following:
| 1 - one
| 2 - two
| 3 - quit""".stripMargin)
StdIn.readInt()
}
def main(args: Array[String]) {
var opt = 0
do {
opt = readOption
} while (menu(opt))
}
Alternately, if you wanted to something more functional, you could change menu
to something like this:
val actionMap = Map[Int, () => Boolean](1 -> handleOne, 2 -> handleTwo, 3 ->handleThree)
def handleOne(): Boolean = {
println("selected 1")
true
}
def handleTwo(): Boolean = {
println("selected 2")
true
}
def handleThree(): Boolean = {
println("selected quit")
false
}
def menu(option: Int): Boolean = {
actionMap.get(option) match {
case Some(f) => f()
case None =>
println("Sorry, that command is not recognized")
false
}
}
Where actionMap
maps each Int
to a function that takes no parameters and returns a Boolean
. In menu, we use option
as the key into the map and then call the result (if there is one).
-
1\$\begingroup\$ Great, that looks fantastic! Thanks so much for the quick response. \$\endgroup\$jwmortensen– jwmortensen2015年03月25日 16:13:00 +00:00Commented Mar 25, 2015 at 16:13
-
\$\begingroup\$ Very clean & reusable too. \$\endgroup\$Shipof123– Shipof1232018年11月27日 22:28:01 +00:00Commented Nov 27, 2018 at 22:28
A similar approach to the accepted answer, yet emphasizing encapsulation, as follows,
object OptMenu extends App {
sealed abstract class Menu {
def menu()
}
case object OptOne extends Menu {
def menu() = println("selected 1") // do other stuff as well
override def toString = "1 - one"
}
case object OptTwo extends Menu {
def menu() = println("selected 2") // do other stuff as well
override def toString = "2 - two"
}
case object OptQuit extends Menu {
def menu() = println("selected quit")
override def toString = "3 - quit"
}
def help() = "Please select one of the following:\n" +
Seq(OptOne, OptTwo, OptQuit).mkString("\n")
println(help())
io.StdIn.readInt match {
case 1 => OptOne.menu()
case 2 => OptTwo.menu()
case 3 => OptQuit.menu()
case _ => sys.error("Unrecognized option.")
}
}
Each menu action in wrapped in a dedicated case object, each menu can be specialised. Encapsulation here facilitates scalability as well as specializing desired menus. Every case object that extends Menu
must implement a function menu()
with specialized logic. In this illustration, the abstract menu()
(and all the specializations) deliver a Unit
value.
Idiomatic in Scala is to override the toString
method, here used for showing help text by putting the case objects into a collection and then making a string out of it (mkString
).
Pattern-matching as already suggested, follows. The call to StdInt
has been prefixed with io
; standard is also to import scala.io.StdInt
especially for calling StdIn functions several times.
Note this code has no main
function, instead OptMenu
object overrides App
. Thus for invoking this code from REPL consider
OptMenu.main(Array.empty)
For unrecognized options, sys.error
will print a given error message and terminate the execution.
The match block could be refactored into this (similarly as in Donald's answer),
val opts = Map ( 1 -> OptOne, 2 -> OptTwo, 3 -> OptQuit)
opts.getOrElse( io.StdIn.readInt, sys.error("Unrecognised option") ).menu()
yet suggested here is the use of getOrElse
together with sys.error. In contrast with the initial answer here, we type menu()
once only, not three times, one for each possible match. This illustrates how encapsulation aids in scalability: we can add new options and menu()
will be invoked anyway, recall this function is implemented by each case object that extends Menu
.
Neater than throwing an exception, in order to discard console input that are not Int
, consider this code,
util.Try(io.StdIn.readInt).toOption.getOrElse(sys.error("no int input"))
Here we invoke readInt
to deliver an integer value, yet we convert it first into an Option
, and in case we cannot extract its integer value, a sys.error termination is enforced.
Using @Donald.McLean method but using one handle function being called with partial partially.
package scalaproj
import scala.io.StdIn
object MenuProg {
def handleInput(continue: Boolean)(op:Int): Boolean = {
println("selected " + op)
if (!continue) println("Quiting ...")
continue
}
val handleLoop = handleInput(true) _
val handleQuit = handleInput(false) _
val actionMap = Map[Int, (Int) => Boolean](1 -> handleLoop , 2 -> handleLoop, 3 ->handleQuit)
def menu(option: Int): Boolean = {
actionMap.get(option) match {
case Some(f) => f(option)
case None =>
println("Sorry, that command is not recognized")
false
}
}
def readOption: Int = {
println("""|Please select one of the following:
| 1 - one
| 2 - two
| 3 - quit""".stripMargin)
StdIn.readInt()
}
def main(args: Array[String]) {
var opt = 0
do {
opt = readOption
} while (menu(opt))
}
}
-
1\$\begingroup\$ I find this is not a review but an alternative implementation of one of the answers without much explanation so I downvoted it. \$\endgroup\$t3chb0t– t3chb0t2017年07月07日 19:42:24 +00:00Commented Jul 7, 2017 at 19:42
Explore related questions
See similar questions with these tags.