可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Let's say I have a class and I want to make its methods chainable, I could do something like this:
class MyClass {
def methodOne(arg1: Any): MyClass = {
println("doing stuff")
this
}
def methodTwo(arg1: Any, arg2: Any): MyClass = {
println("doing other stuff")
this
}
}
While that would achieve the functionality I'm looking for, it's not very elegant in my opinion. Is there a better way of doing this?
Assuming it's possible, I'd like to be able to do something like the following, but I'm not sure how to approach the makeChainable
function.
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"))
}
回答1:
The first option is the most efficient one, the other one introduces overhead by wrapping code into function object. But it's certainly possible to create such a wrapper. Let's define
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
}
Notice this.type
, it says the result of our functions is the type of the class they're defined in. So now when we mix it into our class
class MyClass extends Chainable {
val methodTwo =
mkChain((x: Any, y: String) => println("Doing something " + y));
}
the result of methodTwo
will be MyClass
.
Update: There is another option, to use implicit conversions:
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");
}
Calling chain
converts anything to this.type
. However it only works inside the class, you can't call something like new MyClass2.methodOne(3).chain.methodTwo("x")
outside.
Update: Yet another solution, based on implicit conversion from Unit
to this
:
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);
}
回答2:
It's easy to implement makeChainable
for unary function, but it gets hairy if you want to support higher arity. The only way I can see to do method two, unless you want to write a separate makeChainable
for every arity, is to tuple the method, pass it through makeChainable
, and then untuple it.
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")
But - and please forgive me for opining - invocation chaining is generally a shortcut you take in other languages that are less expressive than Scala. Unless you're doing this to make an API for Java users, I think this is a really bad idea.
Here's one alternative, which I still would never do, to accomplish roughly the style you're going for in a way that's less invasive:
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"))
Edit:
A more elegant way would be to define a "kestrel combinator". I do think this approach is legit :)
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"))
回答3:
I know this isn't probably exactly what you're looking for, but your description reminds me a lot of the doto
construct in Clojure.
I found a couple of threads discussing the different ways of porting doto
to Scala:
something like Clojure's "doto"?
Re: something like Clojure's "doto"? (I think this was actually a reply to the first thread that somehow ended up as a separate thread)
Looking through those threads, it looks like the easiest way is just to make a val
with a short name and use that as the receiver of repeated statements.
Or create an implicit value class (available in 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"));
The other answers are much more what you're looking for, but I just wanted to point out an alternate approach another language took for working around non-chainable method calls.
回答4:
Leaving aside the question of how wise this is in the first place, it's pretty easy to implement in a type-safe and boilerplate-free way with 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 })
}
And then:
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
This will work for any FunctionN
.