Search code examples
scalalistoperator-overloadingbuilder-pattern

How to overload two-colon operator in Scala?


I was wishing to implement builder pattern in Scala by overloading list operator and Nil. But apparently it didn't work.

class SomeBuilder {

  val sb : java.lang.StringBuffer = new java.lang.StringBuffer

  def ::(str : java.lang.String): SomeBuilder = {
    sb.append(str)
    this
  }

  def Nil(): java.lang.String = {
    sb.toString
  }

}

object Hello extends App {
  println( new SomeBuilder :: "aaa" :: "bbb" :: Nil )
}

Why and how to succeed?


Solution

  • As you may find in the Scala spec

    The associativity of an operator is determined by the operator's last character. Operators ending in a colon ‘:’ are right-associative. All other operators are left-associative.

    and a bit later:

    If there are consecutive infix operations e0;op1;e1;op2…opn;en with operators op1,…,opn of the same precedence, then all these operators must have the same associativity. If all operators are left-associative, the sequence is interpreted as (…(e0;op1;e1);op2…);opn. Otherwise, if all operators are right-associative, the sequence is interpreted as e0;op1;(e1;op2;(…opn;en)…).

    It means that your syntax

    new SomeBuilder :: "aaa" :: "bbb" :: Nil 
    

    is actually interpreted as

    Nil.::("bbb").::("aaa").::(new SomeBuilder)
    

    This is done that way because :: is an operator traditionally used in functional programming to build immutable List and this is exactly the semantics required there. So if you really want to use :: you should make a code like this:

    object Nil {
    
     class RealBuilder(val sb: java.lang.StringBuilder) {
        def ::(str: java.lang.String): RealBuilder = {
          sb.append(str.reverse)
          this
        }
    
        def ::(terminal: SomeBuilder): String = {
          sb.reverse.toString
        }
    
        override def toString = sb.toString
      }
    
    
        override def toString = sb.toString
      }
    
      def ::(str: java.lang.String): RealBuilder = {
        new RealBuilder(new java.lang.StringBuilder(str))
      }
    
      override def toString = ""
    }
    
    sealed trait SomeBuilder
    object SomeBuilder extends SomeBuilder
    

    and then

    println(SomeBuilder :: "aaa" :: "bbb" :: Nil)
    

    will work. But note how dreadfully inefficient this is: effectively you rotate every string twice. You have to do this because :: is right-associative and there is no efficient perpend method.

    Sum up you can make this syntax work but this is a really bad idea to use :: for that. You will be better if you use literally almost anything else.

    Sidenote #1: Using Nil is also a rather bad idea because it will clash with the scala.collection.immutable.Nil (i.e. an empty List).

    Sidenote #2: Although modern JVM implementation can optimize synchronized out it many cases you better use java.lang.StringBuilder instead of java.lang.StringBuffer in such non-multithread environment


    Update the same but with ++

    So the main problem was using ::. If you use other operator like ++, it should work find.

    If syntax like that

    println(new SomeBuilder ++ "aaa1" ++ "bbb2" build)
    

    or

    println((new SomeBuilder ++ "aaa1" ++ "bbb2").build)
    

    is OK with you, you may use code like this:

    class SomeBuilder {
      val sb: StringBuilder = new StringBuilder
    
      def ++(s: String): SomeBuilder = {
        sb.append(s)
        this
      }
    
      def build: String = sb.toString()
    
      override def toString = sb.toString
    }
    

    if you for some reason prefer something closer to your example such as

    println(new SomeBuilder ++ "aaa1" ++ "bbb2" ++ BuildString)
    

    you may use code like this:

    class SomeBuilder {
      val sb: StringBuilder = new StringBuilder
    
      def ++(s: String): SomeBuilder = {
        sb.append(s)
        this
      }
    
      def ++(terminator: BuildString): String = sb.toString()
    
      override def toString = sb.toString
    }
    
    sealed trait BuildString
    
    object BuildString extends BuildString