Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

So, there have been a few replies to this post, but as far as I can tell, you're asking about maintaining invariants ("not breaking the structure"), which nobody has addressed so far. Let's have a look how this is handled in different types of languages (^ means I have extensive experience with these, so I might have some of those without a ^ wrong - some languages are also tough to categorise, but bear with me):

1. statically-typed OOP: e.g. C#^, Java^, C++^, F#, OCaml, Scala, etc.; specifying the type of a field is a kind of invariant assertion (e.g. name must be a string not a number), and depending on the expressiveness of the type system, these static assertions can become arbitrarily specific. You might still need to do runtime checks in the mutating methods if certain invariants (e.g. length > 0) can't be expressed statically in the language. If internal state can be set to mutable objects (e.g. mutable strings) from outside, you can still break the invariants by mutating the objects after they've been checked. Some type systems (such as Java's generic types) are even notoriously weak, requiring extra care.

2. dynamically-typed OOP: e.g. JavaScript^, Objective-C^(see note), Ruby, Python, Io, etc.; generally, no invariants are maintained by default, and everything happens at runtime. So unless you specifically check invariants in the object's methods, invariants can often easily be broken by passing in data with unexpected structure. It is often trivial to pass in objects that satisfy the invariants, then change them in arbitrary ways to violate them afterwards. Encapsulation is frequently also easy to break by extending classes or prototypes.

3. statically-typed functional or procedural: C^, Haskell, ML, ATS, etc.; just as with the statically-typed OOPLs, you can explicitly encode many invariants in the type system. You can't set a number-typed field in a record to a string value. To maintain "softer" invariants, you will typically have runtime checks in the mutating functions. The languages with a pure functional bent typically encourage concentrating mutable state into a select few places in the code. Those places can be gatekeepers for enforcing invariants.

4. dynamically-typed functional or procedural: Lisps such as Clojure^, Scheme and CL^, Erlang, etc.; Where mutable state is allowed, or the default it's just as much a free-for-all as the dynamic OOPLs. You need to make your mutating functions aware of all your invariants, or they're trivially easy to break. In the pure-functional or immutable-by-default languages, again, since mutation is concentrated to a few hotspots (transactions in Clojure, messages in Erlang), those hotspots can easily be made to refuse performing mutations that break invariants.

Comparing 1 with 2, and 3 with 4 respectively, it's pretty obvious that the static languages make it easier to maintain invariants. The more expressive the type system, the more you can nail down the permitted structure. The culmination of this are theorem-proving languages. Interestingly, those tend to be functional.

So, if you compare 1 and 3, encapsulation buys you some safety in some cases, but realistically, it's just as easy to make a system that permits violation of invariants as it is in functional languages. Some of the more pure functional languages replace encapsulation with constrained mutability to help maintain invariants.

Comparing 2 and 4, I'd argue the functional languages actually do a better job helping you maintain invariants. The malleability of objects in (2) languages makes them hard nail down. Immutability on the other hand at least provides stability in time, if not in structure.

Note that I'm only evaluating the ability to maintain invariants here, not trying to come up with an overall judgement on languages. In some situations, maintaining invariants is very, very useful. In others, you don't really care that much.

(note on Objective-C): while you declare fields and variables with a specific static type, any object-typed field can point to an object of any run-time type. So the static types are mostly just annotations and affect ARC semantics, they're not actually enforced or safe, it basically relies on duck typing; Key-Value Observing in particular makes some pretty deep changes to the run-time types of objects, which wouldn't be possible in e.g. Java. Unlike some of the other dynamic languages, the non-object C types and mostly-fixed memory layout of objects give some level of structural safety. Still, there's nothing stopping you from creating a number class that returns YES for isKindOfClass:[NSString class], thus making it straightforward to fool most invariant checks.



Hmmmm, well I wasn't asking about invariants in the way that this term is typically used. I was asking about adapability to future changes. The lure of encapsulation has been that it places a firewall (i.e., the "interface" or "signature") between your representation for some information and the client code that needs access to this information so that you are free to change the representation without changing the client code. It is true that having an unvarying interface is a form of invariant. The problem is that adapting to future needs may involve varying this invariant.

Rich Hickey claims that 90% of the time, encapsulation has the opposite of its desired effect, however, due, in part, to breaking the client's ability to use generic functions on the encapsulated data.

I know that I have personally seen quite a bit of OO code that did not live up to the promise of adapability, and so I would like to know more precisely what the alternate approach that Rich Hickey (and I presume other FP advocates) proposes is, and how it would address the issue of adaptability with examples that bear on real-world situations.

Many responses here have been, You can do encapsulation in FP languages. Sure you can, but that's not the alternate approach! That's the OO appraoch! I know that you can take an OO appraoch in functional programming languages, just as you can take a functional approach in OO programming languages, but Hickey and the OP specifically say that encapulation is extremely over-used, and to think twice and then think twice again before doing it.

The talks I have seen by Rich Hickey are inspiring, but they demand a follow-up talk (or maybe book) that provides the gory details, rather than just more potentially dubious promises.


In most of your statically-typed OOP languages, a type declaration really is just an assertion like "the value bound to 'foo' is always a number". This falls far short of asserting that it has the intended properties. For example, "is a number" is miles away from "is the largest prime number less than n."

For most interesting programs, it is simply not enough to have something which is a number. So you must use some supplementary mechanism to verify all the interesting properties. In practice the supplementary mechanism is not as awkward as writing extended type theory proofs in your programs. Now if this supplementary mechanism is good enough to depend on for all the interesting correctness checks, why isn't it good enough for the basic stuff as well?

If you do make the type system expressive enough to verify everything, it is Turing complete, and many assertions are only verified by calculating them in the type system. But if you write a correct implementation in your Turing-complete type system, it is at least as easy to write a correct implementation in the ordinary way. So why not just write the same code twice, and raise an exception if a result does not get a quorum?

This might be a valid strategy for some cases. But in most cases, some kinds of errors are just much more important to cover than others.

Static vs. dynamic typing is not the issue. The first issue is what kind of system you have for annotating code with appropriate assertions - easier is better, assuming that more assertions are always better.

The second issue is how much of this annotation is mandatory. Push it to the limit and you get a "bondage and discipline" language. Pair it with a type system that is less than extraordinary and you spend a significant share of your time wrestling an awkward type system.

Some people seem to enjoy this activity. But unless you do most of your coding under the influence of heavy sedatives, the odds are that the majority of your errors are not simply passing the wrong primitive type. And unless something is really wrong with you, most of your errors are not due to your inability to put an unbreakable padlock on your own code to limit your own intentional malice. So it is certainly possible to emphasize these cases.

But that should be based on some understanding of costs. If a given class of error is infrequent and low-cost, there should be a burden to justify spending 50% more time always handling that class of error.

If the language understands those costs better than the human then that could be helpful (particularly applicable to DSLs). But if it does not then it is better for the human to be able to manage this for themselves.

The real argument seems to be about which classes of assertions should be mandatory, not anything about the internal type accounting. Seems to me that this should be a decision specific to the purpose. If you are teaching programming your requirements might be different from if you are writing missile guidance systems. Moreover, different people might have different tastes!

So I am getting pretty tired of the facile suggestion that static typing (in particular, as implemented in mainstream blub languages) is invariably safer and that this makes it an obvious choice. The only variable at hand is NOT how much you care about maintaining invariants. It is really not that simple. It is also very significant what kinds of invariants you mean to maintain and what amount of cost you are willing to absorb to maintain them.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: