🎓 A1Learner
🔍
0 of 48 lessons completed

Scala Project: Command-Line Tool

A command-line tool is a program that takes fixed arguments and prints a report. This lesson builds one from arguments held in code.

Parsing builds a typed options value

The parser turns words into an Options value. A good limit gives a Right result.

case class Options(topic: String, limit: Int, verbose: Boolean)

def parse(args: List[String]): Either[String, Options] =
  args match
    case topic :: limitText :: _ =>
      limitText.toIntOption match
        case Some(n) if n > 0 => Right(Options(topic, n, false))
        case _ => Left("limit must be a positive number")
    case _ => Left("need a topic and a limit")

@main def main(): Unit =
  println(parse(List("sales", "2")))

A bad argument gives a Left error

The parser returns a Left for a bad limit. Fold turns the Left into an error message.

case class Options(topic: String, limit: Int, verbose: Boolean)

def parse(args: List[String]): Either[String, Options] =
  args match
    case topic :: limitText :: _ =>
      limitText.toIntOption match
        case Some(n) if n > 0 => Right(Options(topic, n, false))
        case _ => Left("limit must be a positive number")
    case _ => Left("need a topic and a limit")

@main def main(): Unit =
  val result = parse(List("sales", "zero"))
  println(result.fold(err => "Error: " + err, opts => "Topic: " + opts.topic))

A given formats each type its own way

Display uses the given instance for the value type. The Int instance adds a word, and the String instance uppercases the text.

trait Show[T]:
  def show(v: T): String

given Show[Int] with
  def show(v: Int): String = v.toString + " items"

given Show[String] with
  def show(v: String): String = v.toUpperCase

def display[T](v: T)(using s: Show[T]): String = s.show(v)

@main def main(): Unit =
  println(display(42))
  println(display("sales"))

Transforms filter, rename, and total the data

Filter keeps only large sales. Map takes the names, and sum totals every amount.

case class Sale(item: String, amount: Int)

@main def main(): Unit =
  val sales = List(Sale("apples", 30), Sale("pears", 20), Sale("plums", 50))
  println(sales.filter(s => s.amount >= 25).map(s => s.item).sorted.mkString(", "))
  println(sales.map(s => s.amount).sum)

Parsing plus formatting makes a report line

Parse first, then format the result. A good parse prints one clean report line.

case class Options(topic: String, limit: Int, verbose: Boolean)

def parse(args: List[String]): Either[String, Options] =
  args match
    case topic :: limitText :: _ =>
      limitText.toIntOption match
        case Some(n) if n > 0 => Right(Options(topic, n, false))
        case _ => Left("limit must be a positive number")
    case _ => Left("need a topic and a limit")

trait Show[T]:
  def show(v: T): String

given Show[Options] with
  def show(o: Options): String = "Topic: " + o.topic + ", limit: " + o.limit.toString

def display[T](v: T)(using s: Show[T]): String = s.show(v)

@main def main(): Unit =
  parse(List("sales", "2")) match
    case Left(err) => println("Error: " + err)
    case Right(opts) => println(display(opts))

Example

case class Options(topic: String, limit: Int)

case class Sale(item: String, amount: Int)

def parse(args: List[String]): Either[String, Options] =
  args match
    case topic :: limitText :: _ =>
      limitText.toIntOption match
        case Some(n) if n > 0 => Right(Options(topic, n))
        case _ => Left("limit must be a positive number")
    case _ => Left("need a topic and a limit")

trait Show[T]:
  def show(v: T): String

given Show[Sale] with
  def show(s: Sale): String = s.item + ": " + s.amount.toString

def display[T](v: T)(using s: Show[T]): String = s.show(v)

@main def main(): Unit =
  val sales = List(Sale("apples", 30), Sale("pears", 20), Sale("plums", 50), Sale("peaches", 40))
  parse(List("sales", "2")) match
    case Left(err) => println("Error: " + err)
    case Right(opts) =>
      val top = sales.sortWith((a, b) => a.amount > b.amount).take(opts.limit)
      top.foreach(s => println(display(s)))
      println("Total: " + top.map(s => s.amount).sum.toString)

Key points

  • Options: the parsed arguments held in one case class.
  • Parsing: turning raw words into typed options, with an error for bad input.
  • Either: a type that holds a result on the right side or a reason on the left side.
  • Type class: a trait that adds one shared ability to many types.
  • Formatting: a given instance decides how one type prints in the report.

In this example, the fixed arguments parse into Options. The sales sort from largest to smallest, and the top two print through the Show instance. Their amounts sum to 90.

Note

Scala prints a case class with no space after each comma.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Scastie

Exercise

The fixed arguments are List("sales", "zero"). What does parse return?