monnef
monnef

Reputation: 4053

How to create a custom operator simulating dot (without parentheses)?

Let's have this code:

scala> case class Num(n:Int){def inc = Num(n+1)}
defined class Num

scala> implicit class Pipe(n:Num){ def | = n }
defined class Pipe

This works:

scala> (Num(0) |) inc
res7: Num = Num(1)

But is it possible to somehow (maybe implicits or macros?) make Scala to run sample bellow in a same way as the code with parentheses without modifying Num class?

scala> Num(0) | inc
<console>:11: error: Num does not take parameters
              Num(0) | inc
                     ^

Wanted result is:

scala> Num(0) | inc | inc
res8: Num = Num(2)

EDIT:
Here's code that's much closer to the real thing. I hope this is more understandable.

object ApplyTroubles2 extends App {

  import GrepOption.GrepOption

  abstract class Builder {
    var parent: Builder = null

    def getOutput: String

    def append(ch: Builder) = { ch.parent = this; ch }

    def echo(s: String) = append(new Echo(s))

    def wc() = append(new Wc())

    def grep(s: String, opts: Set[GrepOption]) = append(new Grep(s, opts))

    def grep(s: String) = append(new Grep(s))
  }

  object MainBuilder extends Builder {
    def getOutput: String = ""

    override def append(ch: Builder): Builder = ch
  }

  class Echo(data: String) extends Builder {
    def getOutput = data
  }

  class Wc() extends Builder {
    def getOutput = parent.getOutput.size.toString
  }

  class Grep(var pattern: String, options: Set[GrepOption]) extends Builder {
    def this(pattern: String) = this(pattern, Set.empty)

    val isCaseInsensitive = options.contains(GrepOption.CASE_INSENSITIVE)

    if (isCaseInsensitive) pattern = pattern.toLowerCase

    def getOutput = {
      val input = if (isCaseInsensitive) parent.getOutput.toLowerCase else parent.getOutput
      if (input.contains(pattern)) input
      else ""
    }
  }

  object GrepOption extends Enumeration {
    type GrepOption = Value
    val CASE_INSENSITIVE = Value
  }

  object BuilderPimps {
    //    val wc: Builder => Builder = x => x.wc()
    //    def echo(msg: String): Builder => Builder = x => x.echo(msg)
  }

  implicit class BuilderPimps(b: Builder) {
    // as suggested in one answer, should solve calling an apply method
    //    def |(fn: Builder => Builder): Builder = fn(b)

    def | : Builder = b

    def getStringOutput: String = b.getOutput

    def >> : String = getStringOutput
  }

  import MainBuilder._

  // working
  println(echo("xxx").wc().getOutput)
  println(echo("str") getStringOutput)
  println((echo("y") |) wc() getStringOutput)
  println(((((echo("y") |) echo ("zz")) |) wc()) >>)
  println(((echo("abc") |) grep ("b")) >>)
  println((echo("aBc") |) grep("AbC", Set(GrepOption.CASE_INSENSITIVE)) getStringOutput)

  // not working
  println((echo("yyyy") | wc()) getStringOutput)
  println(echo("yyyy") | wc() getStringOutput)
  println((echo("y")|) grep("y") >>)
  println(echo("x") | grep("x") | wc() >>)
}

I realize that wanted operator adds no extra value in terms of functionality, it's supposed to be just a syntactic sugar to make things look nicer (in this case I'm trying to mimic the shell piping).

Upvotes: 0

Views: 596

Answers (2)

monnef
monnef

Reputation: 4053

The answer from Akos Krivachy is pretty close, but since I can't append my complete solution to it I have to create a new separate answer (this feature of SO seems a bit weird to me).

object ApplyTroubles2 extends App {

  import GrepOption.GrepOption

  abstract class Builder {
    var parent: Builder = null

    def getOutput: String

    def append(ch: Builder) = { ch.parent = this; ch }

    def echo(s: String) = append(new Echo(s))

    def wc() = append(new Wc())

    def grep(s: String, opts: Set[GrepOption]) = append(new Grep(s, opts))

    def grep(s: String) = append(new Grep(s))
  }

  object MainBuilder extends Builder {
    def getOutput: String = ""

    override def append(ch: Builder): Builder = ch
  }

  class Echo(data: String) extends Builder {
    def getOutput = data
  }

  class Wc() extends Builder {
    def getOutput = parent.getOutput.size.toString
  }

  class Grep(var pattern: String, options: Set[GrepOption]) extends Builder {
    def this(pattern: String) = this(pattern, Set.empty)

    val isCaseInsensitive = options.contains(GrepOption.CASE_INSENSITIVE)

    if (isCaseInsensitive) pattern = pattern.toLowerCase

    def getOutput = {
      val input = if (isCaseInsensitive) parent.getOutput.toLowerCase else parent.getOutput
      if (input.contains(pattern)) input
      else ""
    }
  }

  object GrepOption extends Enumeration {
    type GrepOption = Value
    val CASE_INSENSITIVE = Value
  }

  // all above is un-touchable (e.g. code of a library I want to pimp out)

  // all bellow are the pimps I wanted
  // (
  //   based on this answer [https://stackoverflow.com/a/20181011/1017211]
  //   from Akos Krivachy [https://stackoverflow.com/users/1697985/akos-krivachy]
  // )

  object MyBuilder {
    type MyBuilderTransformer = MyBuilder => MyBuilder

    def builderFunc(func: Builder => Builder): MyBuilderTransformer =
      (x: MyBuilder) => {func(x.builder).wrap}

    // methods in original library without parameters can be represented as vals
    val wc: MyBuilderTransformer = builderFunc(_.wc())

    // when it has parameters it must be def, we need to pack params
    def grep(s: String): MyBuilderTransformer = builderFunc(_.grep(s))

    def grep(s: String, ss: Set[GrepOption]): MyBuilderTransformer = builderFunc(_.grep(s, ss))

    // root expression, differs a bit from original, but in this case it's good enough
    def fromString(msg: String): MyBuilder = MyBuilder(MainBuilder.echo(msg))
  }

  // wrapper class
  case class MyBuilder(builder: Builder)

  implicit class BuilderPimps(b: Builder) {
    def wrap = MyBuilder(b)
  }

  implicit class MyBuilderPimps(b: MyBuilder) {
    def |(fn: MyBuilder => MyBuilder): MyBuilder = fn(b)

    def getStringOutput: String = b.builder.getOutput

    def >> : String = getStringOutput
  }

  // this all works (shows how an end user would use this pimps)

  import MyBuilder._

  println(fromString("abc") | wc getStringOutput)
  println(fromString("abc") | wc >>)

  println(fromString("abc") | grep("b") | wc getStringOutput)
  println(fromString("abc") | grep("b") | wc >>)

  println(fromString("abc") | grep("B", Set(GrepOption.CASE_INSENSITIVE)) | wc getStringOutput)
  println(fromString("abc") | grep("B", Set(GrepOption.CASE_INSENSITIVE)) | wc >>)

  println(fromString("abc") | grep("y", Set(GrepOption.CASE_INSENSITIVE)) | wc getStringOutput)
  println(fromString("abc") | grep("y", Set(GrepOption.CASE_INSENSITIVE)) | wc >>)
}

Upvotes: 0

Akos Krivachy
Akos Krivachy

Reputation: 4966

Postfix vs Infix

Let's first look at how infix and postfix notation relate to each other. Given your case when you write Num(0) | inc then that is equivalent to Num(0).|(inc) in postfix notation.

So looking at your desired syntax:

Num(0) | inc | inc

This will be equivalent to the following in postfix notation:

Num(0).|(inc).|(inc)

Ok, now that that's clear let's do this!

The solution

The def | needs to take a parameter, that holds the function it should do. There are two solutions here, either we define a function over Num or we define a function over the Int the Num is actually holding:

implicit class Pipe(n:Num){
  def |(fn: (Num) => Num) = fn(n)
  def |(fn: (Int) => Int) = Num(fn(n.n))
}

Both would work - you need to choose which one will work best for you.

Now that we have this, we need to define this function. You could put this in Num's companion object (also providing the two differing implementations):

object Num {
  val incNum: Num => Num = n => Num(n.n + 1)
  val inc = (i: Int) => i + 1
}

Looks like we're done. Now we just need to import these functions from the object and use them. The whole code:

case class Num(n:Int)
object Num {
  val incNum: Num => Num = n => Num(n.n + 1)
  val inc = (i: Int) => i + 1
}

implicit class Pipe(n:Num){
  def |(fn: (Num) => Num) = fn(n)
  def |(fn: (Int) => Int) = Num(fn(n.n))
}

import Num._
Num(0) | inc | inc         // Num(2)
Num(0) | incNum | incNum   // Num(2)

Upvotes: 3

Related Questions