Friday, September 01, 2017

Uniform Access Principle in Scala

The Uniform Access Principle basically states that the notation used to access the object's member should be the same independent of whether the member is a field or a method. Thus the developer can remain ignorant of the actual implementation.

The Scala compiler automatically adds the getter/setter methods for constructor arguments depending on the type qualifier.  

class Person(var name: String)

For the above class, the Scala compiler generates the following Java class which can be obtained after disassembling the Person.scala. You can clearly see that getter & setter methods have been added automatically 

$ javap -p Person.class 
Compiled from "Person.scala"
public class Person {
  private java.lang.String name;
  public java.lang.String name();
  public void name_$eq(java.lang.String);
  public Person(java.lang.String);
}

Note the special syntax for the setter which is suffixed with "_$eq". This allows the setter to be invoked when an assignment style syntax is employed:
  • If type = var, both getter/setter methods are added
  • if type = val, only getter is added
  • if no type is specified, neither getter/setter methods are added

val person1 = new Person("Alice")

println(person1.name) // The getter is invoked here implicitly here

person1.name = "Bob"  // The setter is invoked implicitly here

As is evident, the notation employed to access name field does NOT indicate whether the field is being accessed directly or the getter/setter method is being invoked. This is the Uniform Access Principle in action.

Pipeline operations - Scala v/s Java

In both Java & Scala, the pipeline operations are functional and do not modify the original collection.

In the below examples in the case of Java, the pipeline operations are working on streams which are evaluated lazily. 

Intermediate operations like map/filter do not actually execute immediately but just create a new stream which when traversed give the mapped/filtered output of the source stream.

Terminal operations like reduce/forEach cause the stream to be traversed to produce a result or side-effect.

In the below examples for Scala, the pipeline operations are operating on plain collections and hence execute eagerly unlike in the case of Java. However, in Scala, there are ways to run pipeline operations on streams.

(1) Foreach

Java

List<String> fruits = Arrays.asList("pear", "apple", "strawberry", "mango", "banana", "orange")
fruits.forEach(System.out::println)

Scala

val fruits = List("pear", "apple", "strawberry", "mango", "banana", "orange")
fruits.foreach(println)


(2) Map

Java

List<String> ufruits = fruits.stream().map(s -> s.toUpperCase()).collect(Collectors.toList())

or

List<String> ufruits = fruits.stream().map(String::toUpperCase).collect(Collectors.toList())

Scala

val ufruits = fruits.map(_.toUpperCase)


(3) Reduce

Java

Optional<String> allfruits = fruits.stream().reduce((s1,s2) -> s1 + "-" + s2)

Scala

val ufruits = fruits.reduce(_ + "-" + _)


(4) Filter

Java

List<String> selected = fruits.stream().filter(s -> s.startsWith("a")).collect(Collectors.toList())


Scala

val ufruits = fruits.filter(_.startsWith("a"))