Scala 模式:管道操作符

原文发表于2015-04-16。

问题

我们写代码是喜欢从左到右一路写过去的,多流畅,多顺手啊!

考虑这种代码:返回某些数据的json形式。
大概会这么写:

def getyou() = {
  Json(Map("a"->1, "b"->2))
}

我写出了获取数据的表达式Map("a"->1, "b"->2),想起来要返回json,于是移动到行首,写上Json(,再移动到行尾,写上)

不能从左到右一路写过去,不流畅!

假如数据对象有toJson方法,可以直接从左到右写data.toJson,流畅!但是一般的对象没有toJson方法怎么办?

简单管道

我在昨夜想到了UNIX shell的管道: a | b | c。如果Scala内置这种语法多好!

其实,利用Scala的DSL能力,也可以自行实现。看代码:

object Json {
  def |:(obj: Map[_, _]) = {
    val conc = obj.map{p => "  " + p._1 + ": " + p._2}.mkString(",\n")
    "{\n" + conc + "\n}"
  }
}

Map("a"->1, "b"->2) |: Json

自定义了一个|:操作符。Scala允许用符号作为方法名,用起来像操作符,冒号后缀表示该方法的this参数在右侧。
实际上被编译器翻译成Json.|:(Map("a"->1, "b"->2))
于是就能从左到右流畅书写代码了

可惜的是不能轻松地连接多个管道,不能data |: Json |: Println,因为这种操作符是右结合的,调用顺序是从右到左,而不是从左到右。除非加上括号:

object Println {
  def |:(obj: Any) = println(obj)
}

(Map("a"->1, "b"->2) |: Json) |: Println

你就免不了要跑到行首加一个括号,就不流畅了。

连续管道

办法还是有的,要想办法运用正常的左结合操作符。如果我们能扩展任意的对象,就好了。那就给任意对象扩展一个管道操作符吧!
终极实现 看代码:

implicit def pipify[T](t: T) = new {
  def |[R](f: T=>R): R = f(t)
}

def json(obj: Map[_, _]) = obj |: Json

Map("a"->1, "b"->2) | json | println

实际上被编译器翻译成pipify(pipify(Map("a"->1, "b"->2)).|(json)).|(println)
哈哈,晕了没有?

完整示例

完整代码提供在这里,各位可以拿去用REPL或IDE的Scala WorkSheet运行一下。

val data = Map("a"->1, "b"->2)

object Json {
  def |:(obj: Map[_, _]) = {
    val conc = obj.map{p => "  " + p._1 + ": " + p._2}.mkString(",\n")
    "{\n" + conc + "\n}"
  }
}

def json(obj: Map[_, _]) = obj |: Json

object Println {
  def |:(obj: Any) = println(obj)
}

implicit def piping[T](t: T) = new {
  def |[R](f: T=>R): R = f(t)
}

(data |: Json) |: Println

data | json | println

(Update: 发现有写|>的流派,总之用者自选)

Ruby之类的动态语言也能类似地扩展,但可能做不到这么简洁。大概能写成:

#a为任意类型的对象
#简单管道
a.pipe b
#连续管道
a.pipe(b).pipe(c)

如果谁能用Ruby实现得更简洁,还请告诉我!

posted @ 2020-12-25 17:28  计算法  阅读(55)  评论(0编辑  收藏  举报