How can I programmatically make methods chainable?

前端 未结 4 1524
栀梦
栀梦 2021-02-09 05:30

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 = {
             


        
相关标签:
4条回答
  • 2021-02-09 06:07

    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);
    }
    
    0 讨论(0)
  • 2021-02-09 06:15

    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.

    0 讨论(0)
  • 2021-02-09 06:17

    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.

    0 讨论(0)
  • 2021-02-09 06:19

    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"))
    
    0 讨论(0)
提交回复
热议问题