Scala's type inference could be unpleasant, creating problems that did not exist in Java (no inference) or dynamically typed languages.
To be clear, Scala's type inference is awesome! Yet with great powers come great responsibility and the library writer, and to some extent the consumer too, should know what to beware of the rough edges.
Examine this library method which returns a Map
class MyLib {
def getMap = Map("a"->"b")
}
And the client code that reads the map
class MyClient {
def useLib {
val lib = new MyLib
val map = lib.getMap
useMap(map)
}
def useMap(map: Map[String, String]) = println("useMap " + map)
}Looks nice and definitely works. After a while the library is using a mutable
HashMap for some imperative reason
import scala.collection.mutable.HashMap
class MyLib {
def getMap = {
var map = new HashMap[String, String]()
map += ("a"->"b")
map += ("c"->"d")
map
}
}
Looks harmless enough, but when we'll run the client we'll get
java.lang.NoSuchMethodError: MyLib.getMap()Lscala/collection/immutable/Map;
MyClient.useLib(MyClient.scala)
What happened? The client used the Map trait but the code compiled with a dependency on the underlying implementation. Here is the library java representation using
javappublic class MyLib extends java.lang.Object implements scala.ScalaObject{
public test1.MyLib();
public scala.collection.mutable.HashMap getMap();
public int $tag() throws java.rmi.RemoteException;
}While if we'll decompiling with
JAD gives us
import java.rmi.RemoteException;
import scala.*;
import scala.collection.Map;
public class MyClient implements ScalaObject{
public MyClient(){}
public void useMap(Map map){
Predef$.MODULE$.println((new StringBuilder()).append("useMap ").append(map).toString());
}
public void useLib() {
MyLib lib = new MyLib();
scala.collection.immutable.Map map = lib.getMap();
useMap(map);
}
public int $tag() throws RemoteException{
return scala.ScalaObject.class.$tag(this);
}
}
So although the useMap method use scala.collection.Map the compiler in useLib references scala.collection.immutable.Map. It seems that it it could use scala.collection.Map and by that dodging some of the problem.
If we'll change the library to
import scala.collection.mutable.HashMap
import scala.collection.Map
class MyLib {
def getMap: Map[String, String] = {
var map = new HashMap[String, String]()
map += ("a"->"b")
map += ("c"->"d")
map
}
}
Which provides the interface (using javap)
public class test1.MyLib extends java.lang.Object implements scala.ScalaObject{
public test1.MyLib();
public scala.collection.Map getMap();
public int $tag() throws java.rmi.RemoteException;
}which gives the library writer flexibility to change the Map implementation without breaking the client's code. Another solution could be a defensive client
import scala.collection.Map
class MyClient {
def useLib {
val lib = new MyLib
val map : Map[String, String] = lib.getMap
useMap(map)
}
def useMap(map: Map[String, String]) = println("useMap " + map)
}
It looks ugly but it would not break the client when the library changes the map implementation.
Coding conventions should mandate explicit return value for external API. Obviously it's also a good practice for readability and self documented code. This issue is especially relevant for large projects that break up to binary dependent modules.
AddendumIn view of the comments I need to add a clarification:
Java will also fail at build time (and runtime). To be more clear, if in Java you change the source one class it does not changes the compilation of other classes (though it could break their compilation). I don't know much about dynamic languages, but I believe the case is similar there (please correct me if I'm wrong). On the other hand, if in Scala you use implicit all around then changing one the source code of a library class does change the compilation output of the consumer class and this is the dangerous part.
In many projects there are binary dependencies between libraries / modules. Actually most projects are depending on some sort of external library, linking to its jar. Compiled versions of the modules are kept in a repository and unless you change their code they are will not be rebuilt. Lets say I wish to upgrade a version of a module v1.0 to v2.0 used by a client v1.0. I will build it with all the others and run some tests, and if all goes well then I assume that the module is backwards compatible and add it to the binary repository. Not the folks that build the product are taking together all the jar files and they break on runtime!
Now, this is not a big deal as itself. The bigger deal is that the module is not really backward compatible and you may not have all the sources handy to rebuild them from scratch as in the unfortunate case of using not open source library, but even if you do have the sources its a pain.