11
\$\begingroup\$

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)
}
\$\endgroup\$

3 Answers 3

7
\$\begingroup\$

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).

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Great, that looks fantastic! Thanks so much for the quick response. \$\endgroup\$ Commented Mar 25, 2015 at 16:13
  • \$\begingroup\$ Very clean & reusable too. \$\endgroup\$
    – Shipof123
    Commented Nov 27, 2018 at 22:28
2
\$\begingroup\$

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.

\$\endgroup\$
0
\$\begingroup\$

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))
}

}
\$\endgroup\$
1
  • 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
    Commented Jul 7, 2017 at 19:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.