Moje zdjęcie
Software Craftsman's Blog by Marcin Pieciukiewicz
Java and Scala development

Monday, July 8, 2013

Named and Default parameters inconsistency in Scala

While I was reading Scala in Depth I've run into an interesting problem related to named method parameters and class inheritance. What will happen, when in child class we'll override method and we will change parameters names in this method? How will it change the way that this method should be called? And what about default parameters, will those behave simillary to named parameters? It turns out that they will not behave the same. For this reason inheritance of default parameters may result in difficult to detect errors in your application behavior.

I will start by summarizing ways that Scala gives us to pass parameters to method calls. How we can use named and default parameters which were introduced in Scala 2.8.

Positional parameters (likewise in Java).
Usually parameters are passed to function by passing multiple parameters in the order they have been declared in a function declaration. ie.:

scala> def process(x1:Int, x2:Int) = x1 * 2 + x2

scala> process(2, 5)
res0: Int = 9

Named parameters.
If we need to change the order of parameters in function call, or we want to improve readability of function call, or we want to pass only some of the parameters (if function allows that), we can use named parameters. ie.:

scala> def process(x1:Int, x2:Int) = x1 * 2 + x2

// passing parameters without change in ordering
scala> process(x1 = 2, x2 = 5) 
res1: Int = 9 

// passing parameters with change in ordering
scala> process(x2 = 5, x1 = 2) 
res2: Int = 9

It is also possible to call a function by combining positional parameters with named parameters. But in this case it is required that named parameters (if we change ordering of passed parameters) should be passed after positional parameters. ie.:

// some parameters with name
scala> process(2, x2 = 5) 
res3: Int = 9 

// first parameter named, BUT ordering was not changed
scala> process(x1 = 2, 5) 
res4: Int = 9 

// first parameter named, AND ordering was changed
scala> process(x2 = 5, 2) 
<console>:9: error: positional after named argument.
              process(x2 = 5, 2)

Default parameters.
If we want allow a user of our function, not to pass all of the parameters, we can define, in a function declaration, a value that will be assigned to a parameter, if that parameter won't be passed in a function call. ie.:

scala> def sum(x1:Int, x2:Int, x3:Int = 0) = x1 + x2 + x3 
scala> sum(1, 2, 3)
res5: Int = 6 
scala> sum(1, 2)
res6: Int = 3

It is worth to notice, that default parameters don't have to be declared at the end of the parameters list. In that case the only way to call a function without passing theirs value is to use named parameters:

scala> def sum(x1:Int, x2:Int = 0, x3:Int) = x1 + x2 + x3 
// only to parameters have been passed
scala> sum(1, 3) 
<console>:9: error: not enough arguments for method sum: (x1: Int, x2: Int, x3: Int)Int.
Unspecified value parameter x3.
              sum(1, 3) 
// defined parameters were passed by name
scala> sum(x1 = 1, x3 = 3) 
res7: Int = 4

Named parameters and class inheritance.
Let's check how named parameters behave in the case of class inheritance and method overwriting. Let us look onto those example classes:

class Calculator {
  def process(x1:Int, x2:Int) = x1 * 2 + x2

class BrokenCalculator extends Calculator {
  override def process(a:Int, b:Int) = super.process(a, b) + 1

It's important to notice that parameter names of method process has been channged from x1 and x2 to a and b.
In the next step let's create instances of those classes, but in the case of BrokenCalculator instances we will assign them to references of different types:
val calculator = new Calculator
val brokenCalculator = new BrokenCalculator
val brokenCalculatorAsCalculator:Calculator = new BrokenCalculator

Does assigning of BrokenCalculator to reference of type Calculator have an influance on method behavior? It turns out that it depends on the way how the parameters are passed:

scala> calculator.process(2, 5)
res8: Int = 9
scala> brokenCalculator.process(2, 5)
res9: Int = 10
scala> brokenCalculatorAsCalculator.process(2, 5)
res10: Int = 10

In the case when we passed parameters as positional parameters all of the calls depends on the class of the instatiated object, therefore brokenCalculator and brokenCalculatorAsCalculator have behaved exacly the same.
What if we want to use named parameters in method call?

scala> calculator.process(x2 = 5, x1 = 2)
res11: Int = 9 
// parameter passed with the names from Calculator class
scala> brokenCalculator.sum(x2 = 5, x1 = 2)
<console>:11: error: value sum is not a member of BrokenCalculator
              brokenCalculator.sum(x2 = 5, x1 = 2)
scala> brokenCalculator.process(b = 5, a = 2)
res12: Int = 10 
// parameter passed with the names from BrokenCalculator class
scala> brokenCalculatorAsCalculator.sum(b = 5, a = 2)
<console>:11: error: value sum is not a member of Calculator
              brokenCalculatorAsCalculator.sum(b = 5, a = 2)
scala> brokenCalculatorAsCalculator.process(x2 = 5, x1 = 2)
res13: Int = 10

In this case, as you can see, it is important what reference type we are using. In our example objects calculator and brokenCalculatorAsCalculator required parameter names x1 and x2. When we've tried to pass the names a and b we've got compilation error. Analogous in the case of brokenCalculator object we had to use names a and b, and when we've tried to use x1 and x2 we've also got compilation error.

I think that for proper use of named parameters we have to assume that method's contract (the way how method can be called) is determined by the type of reference we use, and this allows compiler to verify the correctness of a method call.

Default parameters and class inheritance.
Let's now change our classes, by defining a default parameter values. In the case of BrokenCalculator we'll change default value of x2 parameter:

class Calculator {
  def process(x1:Int, x2:Int = 0) = x1 * 2 + x2

class BrokenCalculator extends Calculator {
  override def process(x1:Int, x2:Int = 1) = super.process(x1, x2)
Now let's check how method behavior depends on object type and reference type that it is assigned to:

scala> calculator.process(2)
res14: Int = 4 // default value 0
scala> brokenCalculator.process(2)
res15: Int = 5 // default value 1
scala> brokenCalculatorAsCalculator.process(2)
res16: Int = 5 // default value 1

Those results are quite surprising (!), it seems that if a child class changes the default value of a parameter than default values of parameters are indendent of the reference type. During the call of brokenCalculatorAsCalculator parameter x2 had been assigned default value of 1, although the contract from Calculator class defined a default value of 0. I think that this might cause problems, especially because IDE (in my case IntelliJ IDEA) shows to a developer that default value of x2 in the call of brokenCalculatorAsCalculator is 0!

One more case left. What if BrokenCalculator class won't declare any default value for x2 parameter?

class BrokenCalculator extends Calculator {
  override def process(x1:Int, x2:Int) = super.process(x1, x2)

Can we now call the process method and pass it only 1 parameter?

scala> calculator.process(2)
res17: Int = 4
scala> brokenCalculator.process(2)
res18: Int = 4
scala> brokenCalculatorAsCalculator.process(2)
res19: Int = 4

Yes we can! In this case BrokenCalculator has inherited default value of x2 parameter, so if child class doesn't define default parameter value, this value is inherited from parent class.

By this example you can see huge inconsistency in Scala design. What if some programmer will use the Calculator reference? By looking at method defined in Calculator class he cannot know the default parameter value! He has to look into concrete implementation of object he will have. But this is not always possible.

So you have to remember that child class always inherits default values of parameters and it can always change them. And when you call the method you need to know the default values defined in concrete class, not only in reference type you are using.

Next article: Scala - Parentheses and Curly Brackets in Anonymous Functions
Previous article: List sorting in Scala

No comments:

Post a Comment