《Kotlin 核心编程》阅读笔记
第四章 代数数据类型和模式匹配
代数数据类型(ADT)
在计算机编程中,特别是函数式编程与类型理论中,ADT 是一种组合类型(composite type)。一个类型有其他类型组合而成。
可以将一些简单的类型通过某种“操作符”而抽象成比较复杂而且功能强大的类型。
计数
每种类型在实例化的时候,都会有对应的取值,比如Boolean 类型存在两种可能取值,如果将数字2 与 Boolean 取值种类相关联,这种方式就叫做计数。
积类型
在ADT中,积类型的表现形式和乘法非常相似,可以将其理解为一种组合(combination)。
Boolean 类型对应的是2,Unit类型对应的是1(Unit表示只有一个实例,也就是说只有一种取值,按上面计数的概念就为1)。他们组合之后产生的积类型应该就是 :2*1 = 2
使用代码表示这两种类型的组合:
class BooleanProductUnit(a:Boolean,b:Unit){}
val a = BooleanProductUnit(false,Unit)
val b = BooleanProductUnit(true,Unit)
它的实例取值只能为上面两种。利用类进行组合的时候,实际上就是一种product 操作,积类型可以被看做同时持有某些类型的类型。
由于我们可以根据计数来判断某种类型或者某种类的取值,所以计数还能用于在编译时期对when之类的语句做分支检查。
和类型和密封类
和类型就对应于代数中的加法。枚举类可以算是一种和类型。
enum class Day {SUN,MON,TUE,WED,THU,FRI,SAT}
和类型的特点:
- 和类型是类型安全的。 如上述的枚举类中总共有7种可能的取值,不可能出现其他取值。在使用它的时候就不用担心出现非法情况。
- 和类型是一种“OR”的关系。 和类型一次只能是其中的某一种类型。不能同时拥有两种类型。
使用密封类再实现一个与上述例子不同的版本:
sealed class Day1{
class SUN :Day1()
class MON :Day1()
class TUE :Day1()
class WED :Day1()
class THU :Day1()
class FRI :Day1()
class SAT :Day1()
}
密封类会通过一个sealed修饰符将其创建的子类进行限制,即该类的子类只能定义在父类或者与父类的同一个文件。
使用密封类最大的好处就是,当我们使用when表达式时,不用去考虑非法的情况了。
构造代数数据类型
例:根据一些条件来计算下面几种图形的面积:
圆形(给定半径);长方形(给定长和宽);三角形(给定底和高);
sealed class Shape{
class Circle(val radius:Double):Shape()
class Rectangle(val width:Double,val height:Double):Shape()
class Triangle(val base:Double,val height: Double):Shape()
}
fun getArea(shape: Shape):Double = when(shape){
is Shape.Circle -> PI* shape.radius*shape.radius
is Shape.Rectangle -> shape.height*shape.width
is Shape.Triangle -> shape.base*shape.height/2.0
}
这里我的理解应该是构造了一个和类型。
类型匹配
大部分情况下,那些复杂的数据结构并没有给我们事先提供好那么多的方法,而且有时我们甚至很难或者无法再向这些复杂的数据结构中添加新的方法。
模式匹配中匹配的不仅有正则表达式,还可以有其他表达式。 这里的表达式就是模式。
凡是能够求出特定值的组合我们都能称其为表达式。模式匹配所匹配的内容其实就是表达式。
常见的模式
- 常量模式:和if -else 或者 switch -case 语句相同。
fun constantPattern(a:Int)=when(a){ 1 -> "It is 1" 2 -> "It is 2" else -> "It is other number" }
- 类型模式 :类似于Java中的instanceof 方法
sealed class Shape{ class Circle(val radius:Double):Shape() class Rectangle(val width:Double,val height:Double):Shape() class Triangle(val base:Double,val height: Double):Shape() } fun getArea(shape: Shape):Double = when(shape){ is Shape.Circle -> PI* shape.radius*shape.radius is Shape.Rectangle -> shape.height*shape.width is Shape.Triangle -> shape.base*shape.height/2.0 }
- 逻辑表达式模式
fun logicPattern(a:Int) = when{ a in 2..11 -> ("$a is samller than 10 and bigger than 1") else -> "Maybe $a is bigger than 10,ir smaller than 1" } fun logicPattern1(a:String) = when { a.contains("Yison") -> "Something is about Yison" else -> "It`s none of Yison`s business" }
处理嵌套表达式
sealed class Expr {
//表示某个整数的值
data class Num(val value:Int):Expr()
// Operate 是一个树形结构,用来表示一些复杂的表达式
// opName 属性表示常见的操作符 + - * /
data class Operate(val opName:String,val left:Expr,val right:Expr):Expr()
}
示例实现一个需求:将“0+x”或者“0+x" 化简为x,其他情况返回表达式本身;
利用if else 来实现:
fun simplifyExpr(expr:Expr):Expr = if(expr is Expr.Num){
expr
}else if(expr is Expr.Operate && expr.opName=="+" && expr.left is Expr.Num && expr.left.value==0){
expr.right
}else if(expr is Expr.Operate && expr.opName=="+" && expr.right is Expr.Num && expr.right.value==0){
expr.left
}else{
expr
}
when 表达式来进行实现。
fun simplifyExpr1(expr: Expr):Expr = when{
(expr is Expr.Operate) &&(expr.opName=="+") && (expr.right is Expr.Num) &&(expr.right.value==0) -> expr.left
(expr is Expr.Operate) &&(expr.opName=="+") && (expr.left is Expr.Num) &&(expr.left.value==0) -> expr.right
else -> expr
}
上面这两种方式没有什么区别。
模式匹配的核心就是解构,也就是反向构造表达式——给定一个复杂的数据结构,然后将之前用来构成该复杂结构的参数抽取出来。
fun simplifyExpr2(expr: Expr):Expr = when(expr){
is Expr.Operate ->when(expr) {
Expr.Operate("+",Expr.Num(0),expr.right) -> expr.right
Expr.Operate("+",expr.left,Expr.Num(0)) -> expr.left
else -> expr
}
is Expr.Num -> expr
}
解决多重的嵌套,可以使用递归方式进行实现
//
fun simplifyExpr3(expr: Expr):Expr = when(expr){
is Expr.Num -> expr
is Expr.Operate -> when(expr){
Expr.Operate("+",Expr.Num(0),expr.right) -> simplifyExpr3(expr.right)
Expr.Operate("+",expr.left,Expr.Num(0)) -> simplifyExpr3(expr.left)
else -> expr
}
}
增强Kotlin 的模式匹配
面向对象的分解
面向对象的分解就是采用这种思路来做的:我们通过在父类中定义一系列的测试方法,然后在子类实现这些方法,就可以在不同的子类中来做相应的操作了。
sealed class Expr {
abstract fun isZero():Boolean
abstract fun isAddZero():Boolean
abstract fun left():Expr
abstract fun right():Expr
//表示某个整数的值
data class Num(val value:Int):Expr(){
override fun isZero() = this.value==0
override fun isAddZero() = false
override fun left(): Expr = throw Throwable("no element")
override fun right(): Expr = throw Throwable("no element")
}
// Operate 是一个树形结构,用来表示一些复杂的表达式
// opName 属性表示常见的操作符 + - * /
data class Operate(val opName:String,val left:Expr,val right:Expr):Expr() {
override fun isZero() = false
override fun isAddZero() =this.opName=="+" && (left.isZero()||right.isZero())
override fun left(): Expr = left
override fun right() = right
}
}
fun simplifyExpr4(expr: Expr):Expr = when{
expr.isAddZero() && expr.right().isAddZero() && expr.right().left().isZero() -> expr.right().right()
else -> expr
}
采用面向对象分解方式确实简化了上面需求的实现,但是随之带来的是更多的方法,这也出现了较高的耦合,添加新类进入将会需要重写很多代码。
这适用于我们业务比较简单,并且后期数据结构变化小。这种方式和when 表达式结合使用,可以比较方便的简化逻辑。
访问者设计模式
通过面向对象分解的方式来对Koltin 现有的模式匹配进行增强,但是这种方式存在一个问题,就是类中写了太多的方法,这样会使的类很繁重。使用访问者设计模式能实现:既能够不在类中实现方法,又能够较为方便的访问特定类中的数据。
访问者设计模式表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
sealed class Expre {
abstract fun isZero(v: Visitor): Boolean
abstract fun isAddZero(v: Visitor): Boolean
abstract fun simplifyExpre(v: Visitor): Expre
class Num(val value: Int) : Expre() {
override fun simplifyExpre(v: Visitor) = v.doSimplifyExpre(this)
override fun isZero(v: Visitor) = v.matchZero(this)
override fun isAddZero(v: Visitor) = v.matchAddZero(this)
}
class Operate(val opName: String, val left: Expre, val right: Expre) : Expre() {
override fun simplifyExpre(v: Visitor) = this
override fun isZero(v: Visitor) = v.matchZero(this)
override fun isAddZero(v: Visitor) = v.matchAddZero(this)
}
}
class Visitor {
fun matchAddZero(expre: Expre.Num) = false
fun matchAddZero(expre: Expre.Operate) = when(expre){
Expre.Operate("+",Expre.Num(0),expre.right) ->true
Expre.Operate("+",expre.left,Expre.Num(0)) ->true
else -> false
}
fun matchZero(expre: Expre.Num) =expre.value == 0
fun matchZero(expre: Expre.Operate) = false
fun doSimplifyExpre(expre: Expre.Num) = expre
fun doSimplifyExpre(expre: Expre.Operate,v:Visitor) = when{
(expre.right is Expre.Num && v.matchAddZero(expre) && v.matchAddZero(expre.right)) &&
(expre.right is Expre.Operate && expre.right.left is Expre.Num) && v.matchZero(expre.right.left) -> expre.right.left
else -> expre
}
}
采用访问者设计模式 的好处就是,我们将方法的实现放到了外部,这样就使得类的结构看上去比较单纯。
其他坏处,也基本和面向对象分解一样。