如何以编程方式使方法可链接?

时间:2013-07-17 06:22:16

标签: scala

假设我有一个班级,我想让它的方法可链接,我可以做这样的事情:

class MyClass {

  def methodOne(arg1: Any): MyClass = {
    println("doing stuff")
    this
  }

  def methodTwo(arg1: Any, arg2: Any): MyClass = {
    println("doing other stuff")
    this
  }
}

虽然这样可以实现我正在寻找的功能,但在我看来并不是很优雅。有没有更好的方法呢?

假设有可能,我希望能够做类似以下的事情,但我不确定如何处理makeChainable函数。

class MyClass {

  val methodOne: Any => MyClass = 
    makeChainable((arg1: Any) => println("doing stuff"))

  val methodTwo: (Any, Any) => MyClass = 
    makeChainable((arg1: Any, arg2: Any) => println("doing other stuff"))

}

4 个答案:

答案 0 :(得分:4)

第一个选项是效率最高的选项,另一个选项通过将代码包装到函数对象中来引入开销。但是创建这样的包装器当然是可能的。我们来定义

trait Chainable {
  final def mkChain(f: () => Any): () => this.type =
    () => { f(); this; }
  final def mkChain[A](f: (A) => Any): (A) => this.type =
    (x: A) => { f(x); this; }
  final def mkChain[A,B](f: (A,B) => Any): (A,B) => this.type =
    (x: A, y: B) => { f(x, y); this; }
  // etc. for other arities
}

注意this.type,它说我们函数的结果是它们所定义的类的类型。所以现在当我们将它混合到我们的类中时

class MyClass extends Chainable {
  val methodTwo =
    mkChain((x: Any, y: String) => println("Doing something " + y));
}

methodTwo的结果为MyClass


更新:还有另一种选择,即使用隐式转化:

trait ToChain {
  implicit class AsThis(val _underlying: Any) {
    def chain: ToChain.this.type = ToChain.this
  }
}

class MyClass2 extends ToChain {
  def methodOne(arg1: Any): Unit =
    println("Doing something")
  def methodTwo(arg1: String): Unit =
    println("Doing something else " + arg1)

  methodOne(3).chain.methodTwo("x");
}

致电chain将任何内容转换为this.type。但是它只能在课堂上使用,你不能在外面调用像new MyClass2.methodOne(3).chain.methodTwo("x")这样的东西。


更新:另一种解决方案,基于从Unitthis的隐式转换:

import scala.language.implicitConversions

class Chain[A](val x: A) {
  implicit def unitToThis(unit: Unit): A = x;
}
implicit def unchain[A](c: Chain[A]): A = c.x;

// Usage:

val r: MyClass = new Chain(new MyClass) {
  x.methodOne(1).methodTwo(2,3);
}

答案 1 :(得分:2)

为一元函数实现makeChainable很容易,但如果你想支持更高的arity,它就会变得毛茸茸。除非你想为每个arity写一个单独的makeChainable,否则我可以看到做方法二的唯一方法是对方法进行元组,将其传递给makeChainable,然后将其解包。

class MyClass {

  def methodOne: Any => MyClass = makeChainable {
    (arg1: Any) => println("doing stuff")
  }

  def methodTwo: (Any, Any) => MyClass = Function untupled makeChainable {(
    (arg1: Any, arg2: Any) => println("doing other stuff")
  ).tupled}

  def makeChainable[A](f: (A) => Unit): (A => MyClass) = { a: A => f(a); this }

}

new MyClass().methodOne("a").methodTwo("b", "c")

但是 - 请原谅我的观点 - 调用链通常是你在其他语言中采用的快捷方式,这些语言的表达力不如Scala。除非您这样做是为Java用户创建API,否则我认为这是一个非常糟糕的主意。

这是一种替代方案,我仍然永远不会这样做,以一种较少侵入性的方式大致实现你所追求的风格:

class MyClass {
  def methodOne(a: Any) { println("doing stuff") }
  def methodTwo(a: Any, b: Any) { println("doing other stuff") }
  def apply(fs: (MyClass => Unit)*) { fs.foreach(f => f(this)) }
}

new MyClass()(_.methodOne("a"), _.methodTwo("b", "c"))

编辑:

更优雅的方式是定义“红隼组合子”。我确实认为这种方法是合法的:)

class MyClass {
  def methodOne(a: Any) { println("doing stuff") }
  def methodTwo(a: Any, b: Any) { println("doing other stuff") }
}

implicit class Kestrel[A](x: A) {
  def ~(f: A => Unit): A = { f(x); x }
}

new MyClass() ~ (_.methodOne("a")) ~ (_.methodTwo("b", "c"))

答案 2 :(得分:1)

我知道这可能不是你想要的,但你的描述让我想起了很多doto construct in Clojure

我找到了几个讨论将doto移植到Scala的不同方法的线程:

something like Clojure's "doto"?

Re: something like Clojure's "doto"?(我认为这实际上是对第一个线程的回复,不知何故最终成为一个单独的线程)

通过这些线程,看起来最简单的方法就是使用短名称生成val并将其用作重复语句的接收者。

或者创建隐式值类(在Scala 2.10中可用):

implicit class Doto[A](val value: A) extends AnyVal {
  def doto(statements: (A => Any)*): A = {
    statements.foreach((f: A => Any) => f(value))
    value
  }
}
new MyClass2().doto(_.methodOne(3), _.methodTwo("x"));

其他答案更多的是你正在寻找的,但我只是想指出另一种语言用来解决非可链接方法调用的替代方法。

答案 3 :(得分:0)

暂且不考虑这一点的明智之处,使用Shapeless以类型安全和无样板的方式实现它是非常容易的:

import shapeless._

trait ChainableUtils {
  def makeChainable[F, Args <: HList](f: F)(implicit
    in: FnHListerAux[F, Args => Unit],
    out: FnUnHLister[Args => this.type]
  ) = out((a: Args) => { in(f)(a); this })
}

然后:

scala> class MyClass extends ChainableUtils {
     |   def func1 = makeChainable((i: Int) => println("Doing stuff."))
     |   def func2 = makeChainable((a: Any, b: Any) => 
     |     println("Doing other stuff."))
     | }
defined class MyClass

scala> val myInstance = new MyClass
myInstance: MyClass = MyClass@6c86b570

scala> myInstance.func1(1).func2('a, "a").func1(42)
Doing stuff.
Doing other stuff.
Doing stuff.
res0: myInstance.type = MyClass@6c86b570

这适用于任何FunctionN