Syntactic sugar in Scala

Versions: Scala 2.12.1

When a programming language provides operator overloading, the learning curve increases most of the time because of the syntactic sugar it brings. After all more of operations will be expressed as not meaningful (at least in first approach) symbols. Scala also comes with its own syntactic sugar that can be applied in a lot of places: sequences, functions or conversion.

This post presents the syntactic sugar in Scala. It's divided in 5 parts where each one presents use cases hidden behind the symboles. The syntactic sugar for underscore is deliberately omitted in this post. If you're interested about it, please read the post about Underscore role in Scala.

Syntactic sugar in sequences

Probably the most visible place using syntactic sugar are sequences. they're often an intrinsic part of the most of applications. In Scala the operations on them can be done in "classical" manner, through explicitly named methods. But it's also possible to use the following operators:

In the example of interchangeable operations as :\ and /: or :+ and +: we can observe that the manipulated sequence is either on the left or the right side of the expression. Considering these operators as the synonyms of methods would be misleading since the parameters should be always on the same place. It's worth to know that it's not the case.

*fix notation

Methods calls in Scala are made with the dot notation, known from Java:

val concatenatedLetters = letters.mkString(", ")

But other and less verbose solutions exist. The first one is the infinix notation. Thanks to it we can ommit the dots in the methods/fields calls:

it("should be called on symbolic method name") {
  val letters = Set("a", "b")

  val newLetters = letters + "c"

  newLetters should have size 3
  newLetters should contain only("a", "b", "c")
}
it("should be called on alphanumerical method name") {
  val numbers = 1 to 4

  val evenNumbers = numbers filter (_%2 == 0)

  evenNumbers should have size 2
  evenNumbers should contain only(2, 4)
}

Another kind of notation shortcuts is the postfix notation. It lets us to invoke the parameterless methods without dots and parentheses:

it("should call parameterless method with postfix notation") {
  val letters = Seq("a", "b", "c")

  val firstLetter = letters head

  firstLetter shouldEqual "a"
}

Even though infinix notation brings some space savings, it's not advised to write the whole applications with it. Normally it's a good candidate for operators (aka symbolic method names, see the examples from the previous section) or internal DSL languages (e.g. Scalatest's matchers). But in other places (alphanumeric notes in production code, boolean expressions) it's good to keep things simple and use dot notation. Why it's a bad idea to use the infinix expressions everywhere is shown in the following code where we must wrap the conditional expression with additional parentheses to avoid the compilation error:

it("should wrap the conditional expression with parenthesis") {
  val letters = Seq("a", "b", "c", "d")
  // Here we want to check if given letter is in the sequence
  // We use for that an if-else expression to show that sometimes
  // we may need to wrap the infinix expressions that not always will
  // lead to the improved readability
  val containChecker: (String) => Boolean = (letter: String) => {
    if (!(letters contains letter)) {
      false
    } else {
      true
    }
  }

  val resultForA = containChecker("a")
  val resultForZ = containChecker("z")

  resultForA shouldBe true
  resultForZ shouldBe false
}

Regarding to postfix notation it's discouraged by the compiler and its use must be explicitly enabled with scala.language.postfixOps feature. Otherwise a warning similar to the following one is returned at the compilation time:

Warning:(169, 33) postfix operator head should be enabled
by making the implicit value scala.language.postfixOps visible.
This can be achieved by adding the import clause 'import scala.language.postfixOps'
or by setting the compiler option -language:postfixOps.
See the Scaladoc for value scala.language.postfixOps for a discussion
why the feature should be explicitly enabled.
      val firstLetter = letters head

Moreover postfix notation doesn't guarantee the correct interpretation of the code by the compiler. Sometimes the compiler can consider the line following the postfix notation as the integrate part of it:

val letters = Seq("a", "b", "c")
var flag = false

val firstLetter = letters head
flag = true

Such code won't compile because of the error:

Error:(170, 33) value update is not a member of String
      val firstLetter = letters head

Apply method

Another syntax shortcut in Scala is the invocation of the apply(...) method. It can be invoked either in classical dot notation or in less usual way, considering the object directly as a function:

describe("apply syntactic sugar") {
  it("should be called without the object") {
    class Letter(letter: String) {
      def apply(introduction: String): String = s"${introduction} ${letter}"
    }
    val aLetter = new Letter("a")

    val introduction = aLetter("This is")

    introduction shouldEqual "This is a"
  }
  it("should be used to create a new instance from the companion object") {
    class Letter(val letter: String) {}
    object Letter {
      def apply(number: Int): Letter = new Letter(s"${number}")
    }

    val letterFromInt = Letter(1)

    letterFromInt.letter shouldEqual "1"
  }
}

Functions and tuples definition

Another kind of syntactic sugar can be found with the function and tuples definition. For the former ones we could define their types as FunctionX where X is the supported arity. However a less verbose and more commonly used way exists:

describe("functions definitions") {
  it("should define a shorthand version of function type") {
    def convertToString(number: Int): String = s"${number}"

    val intToStringConverter: (Int) => String = convertToString _
    val oneAsString = intToStringConverter(1)

    oneAsString shouldEqual "1"
  }
  it("should define a verbose function type") {
    def convertToString(number: Int): String = s"${number}"

    val intToStringConverter: Function1[Int, String] = convertToString _
    val oneAsString = intToStringConverter(1)

    oneAsString shouldEqual "1"
  }
  it("should define an inlined function") {
    val intToStringConverter: (Int) => String = (number: Int) => s"${number}"
    
    val oneAsString = intToStringConverter(1)

    oneAsString shouldEqual "1"
  }
  it("should define a shorthand version of function with no parameters") {
    def returnIdleState(): String = "idle"

    val idleStateGenerator: () => String = returnIdleState _
    val idleState = idleStateGenerator()

    idleState shouldEqual "idle"
  }
}

Another similar use concerns the destructing bind of tuples that can be directly extracted from the underlying sequence:

describe("tuple destruction bind") {
  it("should assign the tuple content to new vars") {
    val (letter1, letter2, _) = ("a", "b", "c")

    letter1 shouldEqual "a"
    letter2 shouldEqual "b"
  }
}

Symbols

Another less common use of Scala's syntactic sugar is the apostrophe (') sign that can be used to create the instances of Symbol class:

describe("Symbol instance") {
  it("should be created from the apostrophe") {
    val symbol = 'A

    symbol.name shouldEqual "A"
  }
}

As shown in this post, Scala, by implementing the operator overriding, provides a lot of syntactic sugar that can sometimes replace the original methods (the first section talks about it) or reduce the verbosity of the code base (an example can be found in the last section with the function shortening). However, as usual, you should be careful about the use of the syntactic sugar. After all the .foldLeft(...) invocation is immediately more meaningful than /: symbols. Moreover, sometimes the syntactic sugar expressions may lead to compilation or execution problems as it was proven in the part about the *fix notations.