美文网首页Scala编程与实践
【Scala编程】格式化算术表达式程序

【Scala编程】格式化算术表达式程序

作者: JasonDing | 来源:发表于2015-06-28 21:53 被阅读286次

    格式化算术表达式程序

    为了练习模式匹配的使用,该博文介绍编写格式化算术表达式的程序,最终的呈现结果如下面这个二维布局的数学表达式所示,这里除法运算被垂直打印出来:

    1          
    - * (x + 1)
    2          
    -----------
      x   1.5  
      - + ---  
      2    x   
    

    为了实现这个程序,我们需要做一下工作:

    1. 编写一个二维布局库来创建和渲染二维布局元素。这里主要应用Scala面向对象编程的一些方法,通过组合与继承来构建简单部件,进而实现库的设计。
    2. 编写一个表达式的格式化类,利用二维布局库来渲染二维字符图形,从而实现一个完整的呈现效果。
    

    二维布局库

    在二维布局库中,我们将定义类使得元素对象能够由简单部件,包括数组,行,以及长方形进行构造。我们还将定义组合操作符abovebeside。这种组合操作符能把某些区域的元素组合成新的元素。

    实现above和beside

    Element类中的above方法把一个元素放在另一个上面,实际上是连接两个元素的contents的值。使用++操作符连接两个数组。我们这里还要考虑两个元素的宽度。
    beside方法,把两个元素靠在一起,创建一个新的元素,新元素的每一行都来子两个原始元素的串联。这里还要考虑两个元素不同的高度。

    实现widen和heighten

    widen方法被above调用以确保Element堆叠在一起后有相同的宽度,heighten方法被beside调用以确保靠在一起的元素具有同样的高度。

    定义工厂对象

    工厂对象包含了构建其他对象的方法。客户可以通过使用工厂方法构建对象而不是直接使用new构造对象。这种方式的好处是可以将对象的创建集中化并且隐藏对象实际代表的类的细节。
    这里我们创建Element类的伴生对象并把它作为布局元素的工厂方法。使用这种方式,你唯一暴露给客户的就是Element的类/对象的组合,隐藏ArrayElement,LineElement和UniformElement三个类的实现。
    三个elem工厂方法来创建不同的类,这里以参数来去区别不同的类。

    完整程序

    package com.jason.expr
    import Element.elem
    
    abstract class Element {
      def contents: Array[String]
    
      def width: Int = contents(0).length
      def height: Int = contents.length
    
      def above(that: Element): Element = {
        val this1 = this widen that.width
        val that1 = that widen this.width
        elem(this1.contents ++ that1.contents)
      }
    
      def beside(that: Element): Element = {
        val this1 = this heighten that.height
        val that1 = that heighten this.height
        elem(
          for((line1, line2) <- this1.contents zip that1.contents)
            yield line1+line2
        )
      }
    
      def widen(w: Int): Element = {
        if(w <= width) this
        else{
          val left = elem(' ', (w-width)/2, height)
          val right = elem(' ', w-width-left.width, height)
          left beside this beside right
        }
      }
    
      def heighten(h: Int): Element = {
        if(h <= height) this
        else {
          val top = elem(' ', width, (h-height)/2)
          val bot = elem(' ', width, h-height-top.height)
          top above this above bot
        }
      }
    
      override def toString = contents mkString "\n"
    }
    
    object Element {
      private class ArrayElement(val contents: Array[String]) extends Element
      private class LineElement(s: String) extends Element {
        val contents = Array(s)
        override def width = s.length
        override def height = 1
      }
      private class UniformElement(
                                  ch: Char,
                                  override val width: Int,
                                  override val height: Int
                                    ) extends Element {
        private val line = ch.toString * width
        def contents = Array.fill(height){line}
      }
    
      def elem(contents: Array[String]): Element =
        new ArrayElement(contents)
    
      def elem(chr: Char, width: Int, height: Int): Element =
        new UniformElement(chr, width, height)
    
      def elem(line: String): Element =
        new LineElement(line)
    }
    

    格式化表达式类

    操作符优先级

    首先在水平布局上,结构化的表达式,比如:

    BinOp("+",
          BinOp("+",
                BinOp("+", Var("x"), Var("y")),
                Var("z")),
          Number(1))
    

    应该打印成(x+y)*z+1。注意x+y两边的小括号是必加的,不过(x+y)*z的两边就是可选的。为了保证布局最好的可读性,目标是应当去掉冗余的括号,并保留所有必须存在的括号。

    在操作符优先级可以采用只定义递增优先级的操作符组,然后通过计算每个操作符的优先级的方式。这样省去了大量对优先级的预计算。
    precedence变量是从操作符到它们优先级的映射,从0开始的整数。它是通过带有两个生成器的for表达式计算出来的。第一个生成器产生opGroups数组的每个索引i。第二个生成器产生opGroups(i)里每个操作符op。for表达式对每个这样的操作符创造一个从op操作符到它的索引i的关联。因此,操作符在数组里的相对位置就被作为它的优先级取出来。
    操作符优先级的处理是这样的:

    val opGroups =
    Array(
      Set("|", "||"),
      Set("&", "&&"),
      Set("^"),
      Set("==", "!="),
      Set("<", "<=", ">", ">="),
      Set("+", "-"),
      Set("*", "%")
    )
    
    val precedence = {
        val assocs =
          for{
            i <- 0 until opGroups.length
            op <- opGroups(i)
          } yield op -> i
        Map() ++ assocs
    }
    

    现在还需要考虑两个操作符的优先级,分别是一元操作符和除法操作符的优先级。一元操作符的优先级高于任何二元操作符,其优先级比*和%的优先级大1,设unaryPrecedence为opGroups的长度,设除法操作符的优先级fractionPrecedence为-1,这样方面进行另外的处理和操作。

    格式化操作中的模式匹配

    主方法format用来产生表达字符的二维数组的布局元素,该方法接受表达式和紧贴表达式的操作符优先级。format方法通过对表达式的类型执行模式匹配来完成它的工作。
    我们重点看一下一元操作符和二元操作符所对应的样本。

    case UnOp(op, arg) =>
      elem(op) beside format(arg, unaryPrecedence)
    

    解释:一元操作,结果就由操作op和最高环境优先级格式化参数arg的结果组成。

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

    解释:如果表达式是分数,中间结果frac就由格式化了的操作元left和right垂直放置,中间用一条水平线元素分隔构成。水平线的宽度是格式化的操作元宽度的最大值。
    最后在frac两边各加一个空格,是为了考虑“(a/b) / c”,如果显示的结果是这样的:

    a
    _
    b
    _
    c
    

    这种方式既可能是“(a/b) / c”,也可能是“a/(b/c)”。为了消除含糊的语义,内嵌的分数a/b的布局两边应该各加一个空格。变成了这样子:

      a
      _
      b
    _ _ _ 
      c
    
    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(")")
    }
    

    解释:对于普通的二元操作符来说,首先格式化它的操作元left和right,格式化左操作元的优先级是操作符op的opPrec,而格式化右操作元是这个优先级加1。这样设计保证了括号也同样反映正确的联合性。
    比如:
    BinOp("-", Var("a"), BinOp("-", Var("b"), Var("c")))
    将被正确划分为“a-(b-c)”,之后中间结果oper由格式化了的左右操作元依次放好,中间加操作符构成。如果当前操作符的优先级小于外围操作符的优先级,那么oper就被放在括号当中,否则就按原样返回。

    完整程序

    package com.jason.expr
    import 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 {
      private val opGroups =
        Array(
          Set("|", "||"),
          Set("&", "&&"),
          Set("^"),
          Set("==", "!="),
          Set("<", "<=", ">", ">="),
          Set("+", "-"),
          Set("*", "%")
        )
    
      private val precedence = {
        val assocs =
          for{
            i <- 0 until opGroups.length
            op <- opGroups(i)
          } yield op -> i
        Map() ++ assocs
      }
    
      private val unaryPrecedence = opGroups.length
      private val fractionPrecedence = -1
    
      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)
    }
    

    测试结果

    package com.jason.expr
    
    object Expression extends  App{
      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)
    }
    

    最终打印效果:

    1          
    - * (x + 1)
    2          
    
    
    x   1.5
    - + ---
    2    x 
    
    
    1          
    - * (x + 1)
    2          
    -----------
      x   1.5  
      - + ---  
      2    x   
    

    转载请注明作者Jason Ding及其出处
    GitCafe博客主页(http://jasonding1354.gitcafe.io/)
    Github博客主页(http://jasonding1354.github.io/)
    CSDN博客(http://blog.csdn.net/jasonding1354)
    简书主页(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
    Google搜索jasonding1354进入我的博客主页

    相关文章

      网友评论

        本文标题:【Scala编程】格式化算术表达式程序

        本文链接:https://www.haomeiwen.com/subject/ftveqttx.html