Friday, August 28, 2015

String Interpolation

Scala 2.10 introduced a nice feature for creating instances of types which can be defined using a String. It's called String Interpolation and its most common application is those strings that you see sprinkled through code like the following:

    throw new Exception(s"$x is too big for Int")

The expression beginning with "$" is replaced in the string by its value (in this case the evaluating x, whether that is a function or an identifier). Expressions including spaces require curly braces. If you follow the link referenced at the top, you will see how to customize string interpolation for your own purposes. If it is all completely clear to you by the time you get back here, then just ignore the rest of this blog post.

But if, like me, you find that comments like "A simple (buggy) implementation of this method could be:..." less than super-helpful, then stay right here and see a real example.

The example I'm going to present is the creation of a Rational number using the string form r"n/d" where n and d represent the numerator and denominator respectively. What Rational, I hear you saythere is no Rational object in Scala. Well, that's true (today at least). But we are going to suppose that we do have the following class (where most of the code in the middle has been replaced by ellipsis).

  case class Rational(n: Long, d: Long) extends Numeric[Rational] {
    ...  
  }

  object Rational {
    def apply(x: Long): Rational = new Rational(x,1)
    def apply(x: String): Rational = {
      val rRat = """^\s*(\d+)\s*(\/\s*(\d+)\s*)?$""".r
      x match {
        case rRat(n,_,d) => Rational(n.toLong,d.toLong)
        case rRat(n) => Rational(n.toLong)
        case _ => throw new Exception(s"invalid rational expression: $x")
      }
    }
    ...
  }

We will add the code described in the linked document to the object definition above. However, we will have to work the actual implementation a little better.

  object Rational {
    implicit class RationalHelper(val sc: StringContext) extends AnyVal {
      def r(args: Any*): Rational = {
        val strings = sc.parts.iterator
        val expressions = args.iterator
        val sb = new StringBuffer()
        while(strings.hasNext) {
          val s = strings.next
          if (s.isEmpty) {
            if(expressions.hasNext)
              sb.append(expressions.next)
            else
              throw new Exception("r: logic error: missing expression")
          }
          else
            sb.append(s)
        }
        if(expressions.hasNext)
          throw new Exception(s"r: logic error: ignored: ${expressions.next}")
        else
          Rational(sb.toString)
      }
   }
  ...
  }

You can probably eliminate the rather ugly logic errors because I don't think they can ever happen (although the mechanism which requires you to evaluate the next expression when you get an empty argument string is rather hokey and not well explained).

That's all there is to it. If you actually want to use it in another class file, then you will need to import Rational.RationalHelper.