February 11, 2008Structural typing vs. Duck typingA while ago, I explained why I thought that Duck Typing was dangerous. More recently, Scala popularized a different type of typing called Structural Typing, which is often described as being "type safe Duck Typing". Let's take a closer look at Structural Typing and see if it delivers on this promise. With Duck Typing, you send a message to an object in absolute darkness. You don't know whether this object knows this message and you just trust the caller to have passed you an object that does: def test(o) log o.getName # Let's hope this will work endThe code above might be broken, but you will only find out at runtime. Structural typing allows you to be a bit more explicit about your expectations on this object. In the following Scala example, I declare that the object passed in parameter should have at least one method, called getName, that should return a String:
def test(f: { def getName(): String }) {
log(f.getName)
}
This doesn't seem like much, but it does buy us a lot of type safety. For example, if I try to pass an object that doesn't define a getName method, the Scala compiler complains:
type mismatch;
found : Test
required: AnyRef{def getName(): String}
I get the same error message if I try to pass an object that has a getName method that returns an int instead of a String.
From that standpoint, Structural Typing is indeed superior to Duck Typing and one of my major objections to Duck Typing ("it's not type safe") goes away. Structural Typing doesn't solve everything, though. Here is a quick side by side comparison of the main techniques available:
Bad:
def test(f: { def getName(): String }) {
log(f.getName)
}
def toXml(f: { def getName(): String }) {
log("
The repetition notwithstanding, the problem with the code above is that renaming one of the getName methods will not cause the other one to be automatically renamed as well since the compiler has no way of knowing that these methods are identical. In some way, it's ironic that the so-called Structural Typing doesn't preserve... the structure of the type we're actually passing.
Better:
trait HasName {
def getName : String = { ... }
}
def test(HasName f) = {
log(f.getName)
}
def toXml(HasName f) = {
log("<log>" + f.getName + "</log>");
}
So, Structural Typing... good or bad?
I have mixed feelings. Structural typings can be handy for occasional pieces of code that might not be worth creating a Class/Trait/Interface for, but as soon as you want to reuse the signature more than once or if that signature contains more than one method, you are better off creating a carefully named type that captures that signature and use it instead. What do you think? Comments
What about recursion? Things will get tricky for structural typing whenever there's a method that returns an object of the same type.
I'd like to have structural typing for interfaces. So I can pass a Person object to printName(HasName name) when Person implements a getName() method. Along the lines of traits, but distinct because I don't want to mix mixins (puh!) and interfaces. Structural typing with traits does mix the mixing concept (code) with the interface concept (contract) Peace It is probably just a difference in definitions being used, but duck typing can be and often is *type safe*; it just may not be known if the method calls will succeed until run time. But I think I know what you intended. Structural typing is of course nothing new, although its introduction in Scala means it may get some renewed attention, which is a good thing. I does seem to deliver a potentially happy middle ground, as you've observed, between full out strict static typing and no guardrails duck typing. However there is a trade off you've not mentioned; structural typing doesn't just add improved type safety to duck typing, it also reduces some of the power and flexibility. Risking a contrived example, consider where you want to accept any object which has either a walk() or a slither() method (but not necessarily both), and you determine which to call by invoking the number_of_legs() method on it first. The set of signatures of valid objects is now no longer a simple enumeration of must-have methods, but a more complex functional description. It may be possible to define a static structural type signature (Haskell?), but not with the simplistic signature system you're describing, or without getting into mixins or interfaces or all that other static type system cruft that you're trying to avoid. That loss of flexibility may be acceptable, but it is there. The other thing to note is that structural type "safety", as you're describing as being static (compile time), could also be used dynamically as well. In many dynamic languages this could take on some form of introspection, occurring at the time of the function invocation but before the body of the function is executed. In Python for example, you could easily define a similar HasName decorator, and define your function such: @HasName It might not be quite as "safe" because it would be a run-time type check and not compile-time, but it could still reduce the number of test paths needed before discovering the type inconsistency, and it would provide the code documentation that I think you're really trying to achieve. You may also want to look at Python's proposed optional type system called "functional annotations" as another related solution - see PEP 3107. [BTW, your comment submission form will not accept a valid URL in the URL field] Posted by: Deron Meranda at February 12, 2008 01:14 AMI would do this: trait Fun{ // by extending fun, needed has the getName type available Hi Deron, It seems to me your walk()/slither() example would be better captured by simple polymorphism: make the contract move() and have this method invoke internally either walk() or slither() depending on the object... And you are right: I disabled the submission of http address in comments for spam reasons... Thanks for the comment! Posted by: Cedric at February 12, 2008 04:35 AMYou don't even need to Extends Fun; you can import the type alias from the trait (or object) to be in scope. Posted by: Alex Blewitt at February 12, 2008 05:22 AMAs Hendrik said already, Scala allows structural typing to respect the DRY principle as well, by letting you define a type alias for the structural type: type HasName = { def getName : String } def test(f: HasName) = { def toXml(f: HasName) = { ... or, you could define a trait, as you did, and an implicit conversion from a structural type to the trait: trait HasName { def getName : String } implicit def named2hasName(named: {def getName: String}) = new HasName { def getName = named.getName } Deron, implicits combined with traits would solve your problem as well: trait Mover { def move } implicit def walking2moving(walker: {def walk}) = new Mover { def move = walker.walk } implicit def slithering2moving(slitherer: {def slither}) = new Mover { def move = slitherer.slither } def acceptMover(m: Mover) = { ... } Sure, this requires some extra typing (pun intended), but if you're writing a reusable component, the couple of extra lines of code for this are probably entirely justified. PS. I don't think there's any reason to mix a leg count into this any more, but I guess you could do that if you expected a class that has both 'slither' and 'walk' methods. implicit def slithering2moving(slitherer: {def slither; def walk; def legCount: Int}) = new Mover { def move = ... /* based on leg count */ } Posted by: Erkki Lindpere at February 12, 2008 05:50 AMType safety is a false sense of security for people who don't know how to write unit tests. Posted by: Paul Barry at February 12, 2008 06:15 AMPaul, your comment sounds silly when its about an article by someone who authored a very popular unit testing framework. I think the point is that if you have type safety, it removes the need for some of the tests. Posted by: Joshua at February 12, 2008 06:28 AMJoshua, First, I never said the author of this blog doesn't know how to write unit tests. Second, I don't think you need to write additional unit tests to do type-checking stuff. Those kinds of errors get tested in the process of testing the behavior of the application. Basically, I disagree with the theory that you have to write more tests when using a dynamically typed language, which is based on the idea that the compiler is giving you some testing for free. Posted by: Paul Barry at February 12, 2008 08:54 AMHi Cedric, any possibility of a "throw-down" of sorts between you and fellow googler Alex Martelli? You would argue against Python and Alex would argue for it. Posted by: rreed at February 13, 2008 01:38 AMRefreshing to see that the new fangled languages are catching up with C++ template facilities. I must be getting old, but maybe there's a place for us dinosours in this new era too. Posted by: Torbjörn Gyllebring at February 13, 2008 02:29 AMAlex, you made a good point. Importing is a lot more sane way to do that; trait extending was just what first came to my mind. Torbjörn: see D language, my friend who's very familiar with it says it has much better designed template system than C++ ... Template Haskell might be of interest to you also (I think newest GHC supports it). I really hope Scala gets some kind of macrosystem. Posted by: Henrik at February 13, 2008 03:57 AMStructural/nominative typing is orthogonal to dynamic/static typing. A language can have dynamic, structural types (e.g. Smalltalk); static, structural types (e.g. Modula, OCaml); dynamic, nominative types (e.g. CLOS); or static, nominative types (e.g. Java, Haskell). But doesn't Structural typing suffer from the same malady as Duck typing for the worst possible scenario: just because two objects have the same method, it doesn't mean the *contract* for the method is the same. Would you blindly call the "shoot()" method on an object, regardless of if it's a camera or a gun? Isn't structural typing perfectly supported in the concept oriented programming (a new but very promising term for me)? Posted by: at February 15, 2008 12:34 AMHi Cedric, I've looked at Scala, and in general I think it tries to solve the wrong problem. By the time you've extended the expressiveness of your type system to make things fully OO you end up with a bunch of meta-data that obfuscates your original code. Some posters have spoken about enforcing contracts. Contracts have both syntax and semantics. Semantics are runtime behaviour which you will never be able to enforce via a static check. This is why Eiffel has all those runtime asserts. Betrand Meyer got this right. My view is that you are better off removing all this meta-data from your code and placing it within dedicated contract specifications. This is what TDD/BDD does. A far more expressive type system then Scalas is available for Strongtalk: http//www.strongtalk.org/ I like Strongtalk and the fact that the type annotations are optional. But comparing Strongtalks type annotations with well written BDD specifications, I would say that the BDD specs are a lot more readable and much better at expressing intent and enforcing contracts. Trying to force all this on to the compiler in a manifest way is barking up the wrong tree IMO. A compiler will never be able to fully address runtime semantics. Paul. Posted by: Paul at February 27, 2008 11:41 PMPost a comment
|