6 ms·
The real problem is abstraction leaks. When an abstraction leaks, the user of the abstraction is forced to think both about the abstraction and its internal imp
by catnaroek 10y ago
The real problem is abstraction leaks. When an abstraction leaks, the user of the abstraction is forced to think both about the abstraction and its internal implementation, which results in a net increase in the cognitive burden of understanding the code.
However, Java doesn't lend itself to designing very tight abstractions, since all but the most elementary arithmetic operations can fail in utterly miserable ways. If you read a line of code like:
someMap.get(someList.length()).someMethod()
In how many ways can it throw an exception? Using temporary variables doesn't help: it only makes less things go wrong per expression or statement, at the price of making each individual expression or statement do less work for you.
Fortunately, there exist languages that offer less room for things to go wrong.
- cbsmith 10y agoMuch as with generics in C++, the correct approach generally tend to be too assume exceptions can be thrown at any time. Java's try-with clauses make that a bit more natural than it used to be. Ironically, I think a lot of Java's abstraction pain comes from having much more fixed behavior. The static type system, the late introduction of generics, the large base class hierarchy, and the concern with backwards comparability means you've got a ton of behavior and interfaces that are locked down with specific (often poor) abstractions.
- catnaroek 10y ago> Ironically, I think a lot of Java's abstraction pain comes from having much more fixed behavior. Fixed behavior isn't necessarily a bad thing. What is always a bad thing is when types don't accurately capture what your program really means. Java's own collections documentation says: “To keep the number of core interfaces small, the interfaces do not attempt to capture such subtle distinctions as mutability, modifiability, and resizability. Instead, certain calls in the core interfaces are optional, enabling implementations to throw an `UnsupportedOperationException` to indicate that they do not support a specified optional operation. Collection implementers must clearly document which optional operations are supported by an implementation.” https://docs.oracle.com/javase/8/docs/technotes/guides/collections/overview.html https://docs.oracle.com/javase/8/docs/technotes/guides/colle... In Java, this API design style is the norm rather than the exception, which validates the criticism coming form dynamic language advocates: “You still need to use tests, so why bother with types?” The only way forward is to use types in a way that actually eliminates the need for (some) nontrivial tests. At the core, the fundamental problems with Java are: (0) Its type system is too nominal. Every notion, primitive or derived, needs a separate name. The result is that making crisp distinctions takes more effort than programmers are willing to make. In a structurally typed system, a derived notion is just a combination or more basic ones, whether you give the combination a name or not. (1) It doesn't offer a principled way to express that an operation can naturally have multiple kinds of results. And such operations abound in practice! For example, if you try to get the value associated with a key in a container, the key might or might not be there. Or, if you try to get the i-th element of a list (assume 0-based indexing and nonnegative i for the sake of the example), then i is either a valid index or j positions after the end of the list. None of these possible results is intrinsically “wrong”, but Java can only express operations with one kind of correct result.
- cbsmith 10y ago> Fixed behavior isn't necessarily a bad thing. What is always a bad thing is when types don't accurately capture what your program really means. That's kind of what I meant by having too much fixed behaviour. > In Java, this API design style is the norm rather than the exception, which validates the criticism coming form dynamic language advocates: “You still need to use tests, so why bother with types?” The obvious answer is that while you can't solve everything with types, you can still succinctly represent what would otherwise be a ton of tests with a single type name. > The only way forward is to use types in a way that actually eliminates the need for (some) nontrivial tests. Well, one man's trivial is another man's non-trivial, but I'm happy having a machine automate all my trivial tests. > Its type system is too nominal Yes. It has become more sophisticated over time, but it is still pretty weak, and more importantly the built-in behaviour carries a lot of the consequences of the ridiculously limited nature of the type system in the first place. > It doesn't offer a principled way to express that an operation can naturally have multiple kinds of results. You could make that charge against most of the BCPL languages. C++ gets around it with templates, but even then has all kinds of limitations on how the output type is a function of the input type. > For example, if you try to get the value associated with a key in a container, the key might or might not be there. Java has Optional<T> which is exactly the way to express that case. > None of these possible results is intrinsically “wrong”, but Java can only express operations with one kind of correct result. Exceptions are a reasonable way of expressing alternative results, as is Optional<T>, as is returning a common supertype/interface, as is null (much as I despise it), as is having an object that serves as a discriminated union. That may not all sound great to you, but those are actually very common strategies found in a variety of other languages.
- catnaroek 10y ago> The obvious answer is that while you can't solve everything with types, you can still succinctly represent what would otherwise be a ton of tests with a single type name. Except a name is just a declaration of intentions. A Python class name is just as good a declaration of intentions as a Java class name. But why should I trust an unverified declaration? > Well, one man's trivial is another man's non-trivial, but I'm happy having a machine automate all my trivial tests. Finite discriminate unions are pretty trivial by any reasonable standards, and Java can only handle them by very cumbersome means (visitors), which people who value their time reasonably avoid. > You could make that charge against most of the BCPL languages. C++ gets around it with templates, but even then has all kinds of limitations on how the output type is a function of the input type. Somehow Rust and Swift manage it. Even C++ and D fare better than Java, since finite discriminated unions can be implemented using templates, although pattern matching using variant_visitors is a truly maddening experience. > Java has Optional<T> which is exactly the way to express that case. But `null` still exists, and it's used in several parts of the Java standard library. > Exceptions are a reasonable way of expressing alternative results, as is Optional<T>, as is returning a common supertype/interface, as is null (much as I despise it), as is having an object that serves as a discriminated union. (0) No, exceptions aren't reasonable. They don't always show up in types, so you don't know what kinds of exceptions a particular procedure might throw. (1) Java's `Optional<T>` is hardly more satisfying: how do I simultaneously pattern-match over two or more optionals? (2) In any case, there exist many more interesting discriminated unions than can be expressed using only optionals and recursive types.