I guess I could sum up my problem like this: Object-orientation is an excellent principle for organizing programs, but a sucky language feature.
The nice thing about all OO languages is that given a parameter or other value, you know exactly what you can do with it. For example, if you have a Java method with the signature:
public void doSomething(Collection String> names);you know exactly what you can do with names. You can add objects to it; you can iterate over it; you can empty it out. And for each object inside it, you can take a substring, concatenate it to another string, etc. If you want to figure out what you can do with each expression, just look in the JavaDocs or use your IDE's autocompletion.
In a structurally-typed language, you have this same ability, except that the method body should be viewed as a series of assertions about the type. For example, in Python:
def do_something(names):
for name in names:
call_something_else(name + 'foobar');
This method asserts that names has a method __iter__ that obeys the iterator protocol, and that each element returned by the iterable can be added to a string, and that the return value of that is a legal parameter for call_something_else. The code is more general than the Java above, but it reveals less information about the values involved. This is why it's so difficult to build a decent autocompletion engine for Python, and perhaps why dynamic languages have a reputation for being difficult to use on large projects.
In both cases, if the objects you have in mind don't support the operation you want to perform, it's immediately obvious what to do. You write a method that performs that operation. This is problematic if you don't control that class, which is why Ruby and C# have moved to open classes. (In Java or Python, the idiomatic solution for that is to subclass it or write an adapter that you control that delegates to the base class.)
And it's usually obvious where this method should go: on the class that you need to manipulate.
In a language with multimethods or pattern-matching, it's still obvious that you ought to write a method. But now, it's no longer obvious where the method should go: do you define it with the first argument type? The second argument type? The generic function declaration, if your language requires that you declare generic functions instead of assuming all functions are generic? A group of related functions? It's own module?
And then every time someone uses the method, they need to remember where it was defined and add the appropriate import statement. If they forget, they might get behavior they don't expect, as the appropriate method for the given generic function won't even have been loaded.
I tried to come up with some patterns for module arrangements when I was first starting this blog, based on my limited experience. But this is inherently problem-domain-specific: there's no one right organizing principle that applies to everyone's programs.
So again, it's a flexibility vs. productivity tradeoff. A multimethod or typeclass system gives you a lot more flexibility in how you arrange your code. But this requires that you make a lot more decisions on how you arrange your code, and each one of those decisions takes time. And if you get it wrong, it's usually not all that easy to change, requiring that you rework a rats-nest of imports and dependencies.
3 comments:
How do you think that in all OO languages »given a parameter or other value, you know exactly what you can do with it«? This sounds like a description of a type system, and according to what follows in the text, of a static type system in particular. But I don't see how it is related to OO.
Yeah, in that paragraph, it's describing a single-dispatch statically-typed OO language. The next paragraph describes a structurally-typed (either duck typing, like in Python or Ruby, or latent static typing, like in C++ templates) single-dispatch library. But I've found that when I program in duck-typed languages, I usually have an idea of what the acceptable types are in my head, so it still gives me an immediate idea of what other operations are available on that value.
When extending it to multiple-dispatch, the issue is really which parameter of the new method might take that value. You can get around this through convention - always put the collection first, for example, or always have the needle then the haystack - but conventions that aren't enforced by the language tend not to be followed.
Last night, I thought about hooking up a global package repository (like CPAN or PyPI) to the equivalent of a massive JavaDoc search engine. Sorta like Hoogle. So you could instantly see what other operations are available on a type, even if they're defined in someone else's third-party library. But something like this would still be too slow for autocompletion, and it doesn't solve anything about remembering the operations available on a type.
To discover what you can do with a value of some type "Hpsoiwmq", you can search all functions where "Hpsoiwmq" is mentioned in arguments or result. No OO is needed.
Post a Comment