ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 02.01.2026

Просмотров: 3455

Скачиваний: 0

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

Section 15.8

Chapter 15 · Case Classes and Pattern Matching

337

package org.stairwaybook.expr

import org.stairwaybook.layout.Element.elem

sealed abstract class Expr

case class Var(name: String) extends Expr case class Number(num: Double) extends Expr

case class UnOp(operator: String, arg: Expr) extends Expr case class BinOp(operator: String,

left: Expr, right: Expr) extends Expr

class ExprFormatter {

//Contains operators in groups of increasing precedence private val opGroups =

Array(

Set("|", "||"), Set("&", "&&"), Set("ˆ"), Set("==", "!="),

Set("<", "<=", ">", ">="), Set("+", "-"),

Set("*", "%")

)

//A mapping from operators to their precedence

private val precedence = { val assocs =

for {

i <- 0 until opGroups.length op <- opGroups(i)

} yield op -> i assocs.toMap

}

private val unaryPrecedence = opGroups.length private val fractionPrecedence = -1

// continued in Listing 15.21...

Listing 15.20 · The top half of the expression formatter.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index

Section 15.8

Chapter 15 · Case Classes and Pattern Matching

338

// ...continued from Listing 15.20

private def format(e: Expr, enclPrec: Int): Element =

e match {

case Var(name) => elem(name)

case Number(num) =>

def stripDot(s: String) =

if (s endsWith ".0") s.substring(0, s.length - 2) else s

elem(stripDot(num.toString))

case UnOp(op, arg) =>

elem(op) beside format(arg, unaryPrecedence)

case BinOp("/", left, right) =>

val top = format(left, fractionPrecedence) val bot = format(right, fractionPrecedence)

val line = elem('-', top.width max bot.width, 1) val frac = top above line above bot

if (enclPrec != fractionPrecedence) frac else elem(" ") beside frac beside elem(" ")

case BinOp(op, left, right) => val opPrec = precedence(op) val l = format(left, opPrec)

val r = format(right, opPrec + 1)

val oper = l beside elem(" "+ op +" ") beside r if (enclPrec <= opPrec) oper

else elem("(") beside oper beside elem(")")

}

def format(e: Expr): Element = format(e, 0)

}

Listing 15.21 · The bottom half of the expression formatter.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 15.8

Chapter 15 · Case Classes and Pattern Matching

339

Listing 15.21 shows the remainder of class ExprFormatter, which includes three methods. The first method, stripDot, is a helper method.The next method, the private format method, does most of the work to format expressions. The last method, also named format, is the lone public method in the library, which takes an expression to format.

The private format method does its work by performing a pattern match on the kind of expression. The match expression has five cases. We’ll discuss each case individually. The first case is:

case Var(name) => elem(name)

If the expression is a variable, the result is an element formed from the variable’s name.

The second case is:

case Number(num) =>

def stripDot(s: String) =

if (s endsWith ".0") s.substring(0, s.length - 2) else s

elem(stripDot(num.toString))

If the expression is a number, the result is an element formed from the number’s value. The stripDot function cleans up the display of a floating-point number by stripping any ".0" suffix from a string.

The third case is:

case UnOp(op, arg) =>

elem(op) beside format(arg, unaryPrecedence)

If the expression is a unary operation UnOp(op, arg) the result is formed from the operation op and the result of formatting the argument arg with the highest-possible environment precedence.3 This means that if arg is a binary operation (but not a fraction) it will always be displayed in parentheses.

The fourth case is:

3The value of unaryPrecedence is the highest possible precedence, because it was initialized to one more than the precedence of the * and % operators.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index

Section 15.8

Chapter 15 · Case Classes and Pattern Matching

340

case BinOp("/", left, right) =>

val top = format(left, fractionPrecedence) val bot = format(right, fractionPrecedence)

val line = elem('-', top.width max bot.width, 1) val frac = top above line above bot

if (enclPrec != fractionPrecedence) frac else elem(" ") beside frac beside elem(" ")

If the expression is a fraction, an intermediate result frac is formed by placing the formatted operands left and right on top of each other, separated by an horizontal line element. The width of the horizontal line is the maximum of the widths of the formatted operands. This intermediate result is also the final result unless the fraction appears itself as an argument of another fraction. In the latter case, a space is added on each side of frac. To see the reason why, consider the expression “(a / b) / c”. Without the widening correction, formatting this expression would give:

a

-

b

-

c

The problem with this layout is evident—it’s not clear where the top-level fractional bar is. The expression above could mean either “(a / b) / c” or “a / (b / c)”. To disambiguate, a space should be added on each side to the layout of the nested fraction “a / b”. Then the layout becomes unambiguous:

a

-

b

---

c

The fifth and last case is:

case BinOp(op, left, right) => val opPrec = precedence(op) val l = format(left, opPrec)

val r = format(right, opPrec + 1)

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 15.8

Chapter 15 · Case Classes and Pattern Matching

341

val oper = l beside elem(" "+ op +" ") beside r if (enclPrec <= opPrec) oper

else elem("(") beside oper beside elem(")")

This case applies for all other binary operations. Since it comes after the case starting with:

case BinOp("/", left, right) => ...

you know that the operator op in the pattern BinOp(op, left, right) cannot be a division. To format such a binary operation, one needs to format first its operands left and right. The precedence parameter for formatting the left operand is the precedence opPrec of the operator op, while for the right operand it is one more than that. This scheme ensures that parentheses also reflect the correct associativity. For instance, the operation:

BinOp("-", Var("a"), BinOp("-", Var("b"), Var("c")))

would be correctly parenthesized as “a - (b - c)”. The intermediate result oper is then formed by placing the formatted left and right operands side- by-side, separated by the operator. If the precedence of the current operator is smaller than the precedence of the enclosing operator, r is placed between parentheses, otherwise it is returned as is.

import org.stairwaybook.expr._

object Express extends Application {

val f = new ExprFormatter

val e1 = BinOp("*", BinOp("/", Number(1), Number(2)), BinOp("+", Var("x"), Number(1)))

val e2 = BinOp("+", BinOp("/", Var("x"), Number(2)), BinOp("/", Number(1.5), Var("x")))

val e3 = BinOp("/", e1, e2)

def show(e: Expr) = println(f.format(e)+ "\n\n")

for (e <- Array(e1, e2, e3)) show(e)

}

Listing 15.22 · An application that prints formatted expressions.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 15.8

Chapter 15 · Case Classes and Pattern Matching

342

This finishes the design of the private format function. The only remaining method is the public format method, which allows client programmers to format a top-level expression without passing a precedence argument. Listing 15.22 shows a demo program that exercises ExprFormatter.

Note that, even though this program does not define a main method, it is still a runnable application because it inherits from the Application trait. As mentioned in Section 4.5, trait Application simply defines an empty main method that gets inherited by the Express object. The actual work in the Express object gets done as part of the object’s initialization, before the main method is run. That’s why you can apply this trick only if your program does not take any command-line arguments. Once there are arguments, you need to write the main method explicitly. You can run the Express program with the command:

scala Express

This will give the following output:

1

- * (x + 1) 2

x

1.5

-

+ ---

2x

1

- * (x + 1) 2

-----------

x

1.5

-

+ ---

2x

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index


Section 15.9

Chapter 15 · Case Classes and Pattern Matching

343

15.9 Conclusion

In this chapter, you learned about Scala’s case classes and pattern matching in detail. Using them, you can take advantage of several concise idioms not normally available in object-oriented languages. Scala’s pattern matching goes further than this chapter describes, however. If you want to use pattern matching on one of your classes, but you do not want to open access to your classes the way case classes do, then you can use the extractors described in Chapter 26. In the next chapter, however, we’ll turn our attention to lists.

Cover · Overview · Contents · Discuss · Suggest · Glossary · Index