Static typing inhibits interoperability. It does so between class libraries and their clients, and also between programming languages.
The reason is simply because static typing violates encapsulation in general, and improperly propagates constraints that aren’t justifiable as architectural, design or implementation requirements in particular. It does so either by binding too early, or by binding (requiring) more than it should.
The reason that static typing violates encapsulation is because it fails to permit the object itself from being the sole arbiter of its own behavior, and the sole arbiter of the semantics of any operations that may be applied to it. It does that by forcing the programmer to break the veil of encapsulation of the object by directly referencing its class or type as the static type constraint of variables that will be permitted to contain objects of that type, instead of respecting the privacy of that information, which should remain hidden behind the wall of encapsulation that objects are supposed to provide. And then that information is used by the compiler to force the behavior of objects based on the static type constraint of the variables that reference them, as well as the semantics of the operations applied to them, thus violating encapsulation even more profoundly.
As usually implemented by widely-used programming languages, and therefore as typically used by program developers, static typing confuses the distinction between a class and the type that the class implements. And even in an ideal implementation in some language that almost no one will ever actually use in anger, it violates encapsulation by confusing the variables or parameters that reference a value with the class of the referenced value, the type of the referenced value, or (usually) both.
A class is not a type. It’s an implementation of a type. Therefore, any system built on that false assumption is intrinsically broken at a very deep level. If you write code based on the idea that classes are types, you need to transform your thinking.
And a variable is not the value it references. Therefore, any system built on that false assumption is intrinsically broken at a very deep level. The semantics of a variable is the architectural role(s) it plays in the algorithm that uses it. It is not the class of the value of the variable that the variable may be referencing at any particular time, nor the type that that class may be implementing at that particular time (the type that a class implements is not necessarily a static property of the class.)
As a concrete example, consider the usage of an array object in a statically-typed language which must hold objects of different types (or different classes, it makes no difference in this case,) and therefore must use Object as the static type constraint for the array’s element type (assuming there is no less-general common superclass.) Putting elements into such an array is easy, and raises no issues. However, although it is equally easy to fetch elements from such an array, using the retrieved values for other than the most trivial purposes typically requires the programmer to over-constrain the retrieved value: The programmer is forced to constrain such values to being instances of some predefined type (which often also means constraining them to be instances of some predefined class,) before such values can be used in application-specific or domain-specific ways. The programmer must impose such constraints by casting the value to a specific type or class–which will fail if the programmer misjudges the type of the object, even though there would have been no failure had the programmer been allowed to use the value in the operations that were needed without having to cast the value to some specific type (because those operations would be valid for all the elements of the array, regardless of their type or class.)
Typically, one only needs to send one or two messages to values retrieved from a variable or from a collection. One usually does not need to send every message (or apply every operation) defined by a particular type or class. Therefore, requiring that a value must be an instance of a particular type or class is almost always requiring more than is necessary–and usually, far more. Imposing such unnecessary constraints inhibits interoperability because it reduces the generality of the code, reduces the generality of the entities the code defines, and reduces the generality of the the entities that the code can make use of.
And the violation of encapsulation bubbles up to higher-level entities: Classes, modules, packages, assemblies, class libraries and entire frameworks–which is how it inhibits interoperability, not only between class libraries or application frameworks and their clients, but even between different programming languages.
Static typing dramatically increases the coupling between components, because it not only exposes the specific entities that define and/or implement all the behavior of a value, it also requires that clients use precisely and only those specific defining or implementing entities, and typically prohibits the use of functionally/semantically equivalent alternatives that would have worked as desired. This is true even when interfaces are pervasively used as static type constraints (which is rarely the case in practice, and so should in all fairness be dismissed as a moot point,) and because clients must in any case use only the specific interfaces used and/or defined by the component providing the service they’d like to use.
Pervasively using interfaces as static type constraints, although it definitely helps, nevertheless remains a problem a) because interfaces typically require behavior that just is not needed every time one of their instances is used (which is almost always the case,) b) because interfaces defined by third parties–and the classes and methods that use them–generally cannot be changed in any way by their clients, and c) because it is possible (and even likely) that the various providers of “reusable” (ahem) components will separately define what are conceptually “the same” interfaces for the same purposes, which nevertheless won’t be type-compatible even if they have exactly the same names and require precisely all the same operations (messages, operators, etc.)
Why? Because “reusable” components from independent sources that use what are conceptually “the same” interfaces nevertheless cannot easily interoperate with each other, since two (or more!) interfaces that aren’t defined in the same namespace will be classified by the static type system as different, incompatible types, no matter how identical they are otherwise. That means that, for example, the function provided by class library A from source Alpha that transforms a video stream by applying a user-supplied function to the color of each pixel cannot be used with the function that performs the desired color transformation provided by class library B from source Beta, because the two class libraries each use their own “Color” interface, which is not type compatible in spite of having the exact same name and defining the exact same set of required operations with the exact same semantics.
And that’s a short–and incomplete–example of why and how static typing substantially reduces a programmer’s degree of freedom in combining and reusing code components from different, independent sources, relative to what it could be if programming languages used the open-world assumption instead of the closed-world assumption regarding which operations are valid. Operations should be assumed valid until that assumption is proven false, for reasons that are analogous to those that justify the assumption that a person accused of a crime is innocent until proven guilty, or those that motivate the assumption that the null hypothesis is true until proven false by actual experimental observation.
Most of human progress over the past few centuries is directly attributable to discarding the prior obsession with being able to present absolute proofs, and replacing that paradigm with what we now know and love as the scientific method, which is based on falsifiability instead of being based on provability. Our modern technology only exists because we gave up the idea that our models of the world must be provably correct, and instead started using the paradigm of falsifiability, as embodied by the scientific method.
The programming world is in severe need of a Copernican Revolution so that we can discard the ever-more complicated epicycles and contortions that the static typists go through to try to match the generality, reusability and productivity of dynamic programming languages.
Note: You can vote on this essay on Reddit