top | item 2622166

Types Are Anti-Modular

57 points| swannodette | 15 years ago |gbracha.blogspot.com | reply

45 comments

order
[+] dons|15 years ago|reply
From the Haskell reddit discussion:

> It's a simple truth, indeed a tautology, that if you want the compiler to check the consistency of interactions between modules, then the compiler must know the information that it is checking. That's all that's being said here. It really shouldn't surprise anyone. If you dispose of typing, then someone still needs to know that relevant information in order to write correct code; but it is the programmer, rather than the compiler.

or, with more wit,

> Types are anti-modularity in the same sense stop signs are anti-transportation.

http://www.reddit.com/r/haskell/comments/hs39c/gilad_bracha_...

[+] cageface|15 years ago|reply
Indeed. Types are useful because they span module boundaries. I've been working in Scala and C++ lately after working for ten+ years exclusively in dynamic languages and I can't tell you how happy I am to let the compiler catch my dumb mistakes. IMO, dynamic languages optimize for the rare need for freeform metaprogramming at the expense of the common need for easily maintained code.

Dynamic language enthusiasts will tell you that testing is better than static typing but in my experience static typing eliminates about half my test code and gives me much more immediate and precise feedback on my most common errors.

[+] swannodette|15 years ago|reply
The blog post isn't about correctness - it's about modularity. Lispers and Smalltalkers are used to programming with very small compilation units - individual methods and individual functions. Being able to fix running programs w/o going through the whole compile cycle is a demonstrable productivity gain.

Types and modularity are at odds. The antagonism is readily apparent in languages that support dynamic and static typing - Qi. It's RCEPL (Read-Check-Eval-Print-Loop) has serious restrictions in comparison to its standard REPL.

[+] colanderman|15 years ago|reply
Um... NO. This argument applies equally to functions and constants and is equally wrong.

Types are not anti-modular: the culprit is module systems that don't allow type parameterization. Systems such as Java solve this problem for functions and constants by allowing code to be parameterized over interfaces. Bam, you can compile code using said functions and constants separately from their implementation.

The solution for types is exactly the same: move types into interfaces. Guess what, Objective Caml and Coq do this via module functors and it's glorious. If my airline scheduler module needs to compile separately from my graph module, well I can write:

module Scheduler(Graph: sig type graph val traverse: graph -> (string -> unit) -> unit end) = struct ...blabla... end

and I can compile that code without ever writing a snippet of the graph module -- not even defining the ADT!

TLDR: this problem has been solved 30 years ago by people who actually use typed languages.

[+] vog|15 years ago|reply
Thanks for pointing this out. I was about to believe the post was correct, but it seems it isn't. However, just to be sure I understand you correctly:

Is the module functor compiled only once, or once for every parameter type? I'm asking because if it's compiled only once, the generated code might be significantly less optimized. This might not play a role for complex parameter types, but could miss lots of good optimization opportunities for simple types such as int or char. How is this solved in those languages?

Also, is it just OCaml and Coq, or is this possible with other strongly typed languages (such as Haskell) as well?

[+] omaranto|15 years ago|reply
Oddly enough, I think this functorized style is what Bracha advocates. If I understood him correctly in an interview he gave on FLOSS Weekly, his Newspeak language forces you to program in (the dynamically typed equivalent of) the style you describe: each module takes as parameters all modules which it depends on. (He would point out, I think, that ML doesn't force you to adopt this style of writing modules...)

Maybe he discovered module functors on his own and is unaware of their use in statically typed languages such as ML? I don't know, but it certainly seems that this solution is just as good in a statically typed setting as in the dynamically typed setting.

[+] Confusion|15 years ago|reply

  TLDR: this problem has been solved 30 years ago by people
  who actually use typed languages.
You have no idea who Gilad Bracha is, do you? In the face of people like him, a bit more modesty is appropriate. You may want to consider that maybe he isn't so obviously wrong, because he knows his subject matter very well.
[+] alok-g|15 years ago|reply
Copy-pasting a comment on the original article here, since I have the same comment/question:

Brian said...

Dynamic typing doesn't restore the modularity- it simply delays checking for violations. Say module X depends upon module Y having function foo- this is a statement which is independent of static vr.s dynamic checking. This means that that modularity is lost- you can't have a module Y which doesn't have a function foo. If module Y doesn't have a function foo, the program is going to fail, the only questions are when and how.

What I don't get is why detecting the error early is a disadvantage. It's well known that the earlier a bug is detected, the cheaper it is to fix. And yet, all the arguments in favor of dynamic typing are about delaying the detection of bugs- implicitly increasing their cost.

6/05/2011 6:49 AM

[+] 6ren|15 years ago|reply
The article's just claiming that the modules of dynamic languages can be compiled independently; not that it will be correct.

Just my thoughts: There's a bigger question here about types, though. I think the argument is that the ceremony of types gets in the way more than it helps, and it requires up-front design. Many people like dynamic languages for their flexibility; and when you get bugs, you have to fix them anyway. There's an assumption in typed languages that we can design our types well enough upfront to help solve the problem.

It's interesting how types create a dependency on the interface (i.e. if you use a type, you depend on it). One of the ideas of Abstract Data Types was to reduce dependency on the internal implementation details; but you're still dependent on the external interface. If you change the interface, it creates problems (e.g. unit testing claims to give you confidence to refactor, but if you change interfaces, you also have to change the tests...). EDIT added "claims to"

This seems intrinsic to modularity, and I can't really see any solution to this; except for ways to make it easier to cope with interface change. Some insights may come from webapp API's, where the dependency is more explicit (you have to send and receive serialized messages).

[+] jonnytran|15 years ago|reply
Traditionally, academic research has been funded by NASA and the Department of Defense. Under these contexts (space transport, war, etc.), the cost of a runtime failure is extremely high, counted in number of human lives or billions of dollars. Also, throughout history, most software was written once, shipped, then used. The possibility of a hotfix was practically impossible. This is where the adage that "the earlier a bug is detected, the cheaper it is to fix" came from.

In those contexts, this is absolutely true. If you're in space, and your software decides to vent your oxygen, you're screwed. And so it makes sense to invent all kinds of static compiler checks that try to eliminate all possible bugs. Type-checking, for example, can guarantee the elimination of an entire class of problems.

However, what if your context were different? What if your context was a web startup?

Suddenly, the cost of a runtime failure is not so high. With a simple drop-in plugin like Hoptoad, I can be notified of an error, diagnose, fix, and deploy oftentimes before the user can even email me describing what problem he/she had.

In fact, it's much worse than that. As you said, "dynamic typing doesn't restore the modularity- it simply delays checking for violations". This delayed checking for violations has extreme value in the startup context.

Take, for instance, yesterday's post: Show HN: Hacker News Instant (3 hour project) http://news.ycombinator.com/item?id=2621144 If you were one of the first to try it, it wasn't long before you realized that typing a space in your search threw the app into an infinite loop, making it unusable. But I think a simple comment in the discussion thread summed it up perfectly: "I don't think the bug makes it any less noteworthy".

The fact that the author could create a prototype -- a minimum viable product -- in just 3 hours, meant that he could post it on HN and get feedback on it. He could get an idea of whether the app was worth pursuing... or better off scrapping to pursue something else.

In the startup context, you can think of writing software as sketching. You just write enough code to convey to people what your product is and how it can help them. The code may be completely broken, calling functions that don't even exist. It doesn't matter. If no one ever tries to use some specific edge case of one specific feature (or hell, your entire product), that code will never be needed. And so, the fact that it doesn't make sense, doesn't matter.

By checking for as many kinds of "violations" as possible early in the development process, you're forcing the developers to spend time making it all right from the start -- before release. In the space shuttle context, a runtime failure may have critical consequences. But in the startup context, failure to release on time may have critical consequences. Releasing before your competitor may make or break your entire business. Or maybe it's your personal project and life gets in the way and you just end up never releasing at all. Personally, a lot of the joy I get out of making software, is seeing people I know benefit from using it. But if I don't see that, I'm much more likely to give up on it entirely.

People say that when a bug is found, you have to fix it, regardless of whether you're doing static or dynamic checking. They then conclude that it's a no-brainer that you'd rather have this happen sooner than later. But no one ever talks about the cost of fixing bugs. Fixing everything up front, the way static checking forces you to do, unnecessarily increases your costs if that feature eventually gets scrapped. And in startups, this happens all the time.

In the startup context, there is often an excess of good ideas. The problem is, you don't know which one will be the jackpot and you only have enough time and money to pursue a small handful of them. One approach is to do a rough sketch of as many ideas as you can, see which ones start to get traction, and then flesh out the finer details on the ones that do. The ones that don't get traction are scrapped.

Now, if you were using a tool that said every part of your program must make sense and be free of violations before you can run it, you would have spent all that time fixing bugs in features (and possibly entire projects) that no one ever used. The opportunity cost of this is you were not implementing 10 other great ideas. On the other hand, if you were using tools that allowed bugs to be present along-side working code, you would be free to choose when to polish something when it was a priority to you.

Also, in government-funded projects for the space program, you essentially have all the time and funding you could want from the start. Your goal is to use that funding to eliminate all possible runtime errors, possibly pushing back the release to do as much of this as possible. For the most part, it's okay. You'll just get more funding.

However, in the startup world, it's the exact opposite. If you're a poor developer trying to make it big, you have no money now. But if you can prove your app is valuable and can make lots of money, then and only then will investors consider giving you money for it. Before funding, you're lucky if you have one full-time developer. Only after funding, when the project has to have already proven itself, do you have the funds to pay the developers you need to stomp out all the bugs.

It's not that static is better than dynamic or dynamic is better than static. It all depends on what you're doing. You have to choose what makes sense for what you're doing in the context you're doing it in.

The idealist in me wants to eliminate all bugs in code I write before release. And even more so in the code other people write. I totally get that. But the pragmatist in me (which only developed after having to write real production code for a real startup that pays my actual bills) knows that sometimes it makes sense to choose to not fix bugs. Static checks tell me "no, fix them now". Dynamic checks empower me to choose what I need.

[+] ankrgyl|15 years ago|reply
This article confuses the reader with a weird implicit definition of modularity. The miscommunication is rooted here:

> Separate compilation allows you to compile parts of a program separately from other parts. I would say it was a necessary, but not sufficient, requirement for modularity.

The author goes on to assert that a requirement for modularity is that modules that depend on each other should be able to be compiled independently, without even a specification to glue them together. This just doesn't make sense. This might be a debatable point, but it has nothing to do with modularity.

[+] hxa7241|15 years ago|reply
Lack of types gives the illusion of more modularity.

As was mentioned -- there are still dependencies. The only difference is that, without types, checking whether the parts fit together is deferred until later -- but it will still be done. (All parts of software have to be connected, and all have to be consistent.)

And therefore you could argue that types are pro modular: because they allow you to write separate pieces without needing the whole to test if it is OK -- which you cannot do without types.

[+] baguasquirrel|15 years ago|reply
Existential types? The author alludes to this, and then proceeds in ignorance of what he'd just said for the rest of the post.

In Java, all you have for these is interfaces, so it's no wonder people think that types are problematic.

Types aren't the problem. Crappy types are the problem. Not making the types lightweight enough that people can dish them out at their pleasure, that's the problem. When the math don't work well enough, make better maths.

[+] dkarl|15 years ago|reply
I disagreed strongly with this post and was about to post a rebuttal, but then I realized there actually is a situation where untyped languages enjoy a concrete, practical advantage because of the lack of compilation dependencies. That situation is when you are writing a module (something that would be a compilation unit in a typed language) that calls missing or nonexistent library interfaces, and you want to test parts of the module that do not depend on the missing APIs. In a typed language, you can't do it. You would have to satisfy the dependencies first so your code would compile, or you would have to copy snippets of your code out of the compilation unit into a REPL or another file so they could be compiled separately. In an untyped language, you can experiment with parts of the module and even start writing tests without having to make sure all of the module's dependencies are satisfied.

Aside from that one advantage, though, it's the same with types or without them. For a module as a whole, the compilation dependencies that exist in a typed language are the same as the runtime dependencies that exist in an untyped language. Whether you're using a typed or untyped language, you can write whatever code you want, but you can't do anything with it until you satisfy its dependencies. With that one exception, of course: in an untyped language, you can run code that doesn't depend on the missing APIs but happens to be in the same module as code that does depend on them.

Overall, I don't think that amounts to much, especially compared to a typed language like Scala where you can copy snippets into a REPL. Or even better, Ensime is an Emacs mode that is supposed to let you work with Scala the way Slime lets you work with Common Lisp. (I haven't tried it myself yet, but it exists, and people are using it for real work.)

[+] szany|15 years ago|reply
From p. 3 of the paper "Why Dependent Types Matter":

"We have given a complete program, but Epigram can also typecheck and evaluate incomplete programs with unfinished sections sitting in sheds, [···], where the typechecker is forbidden to tread. Programs can be developed interactively, with the machine showing the available context and the required type, wherever the cursor may be. Moreover, it is Epigram which generates the left-hand sides of programs from type information, each time a problem is simplified with ⇐ on the right."

[+] msie|15 years ago|reply
Yeah I really hated this post. Starts off with a flame-baiting title. Then resorts to very qualified statements to support it. After reading it I'm wondering if it matters at all and I'm feeling dumb all throughout the discussion. :-(

Update: Aarghh, it's just aggravating to read an academic stirring up the pot over some point of theoretical purity. And I love theory too. ;-)

[+] stralep|15 years ago|reply
I do not see a single reason why strongly typed language compiler couldn't have unsafe flag, to compile with undefined symbols, and to make your program fail/rise error/perform some other action when such symbols are evaluated or called.

And I understand why this behavior would be unwanted in production systems.

[+] vog|15 years ago|reply
Note that there is an interesting counter-argument in the following HN comment:

http://news.ycombinator.com/item?id=2622491

This argument is also applicable to your concrete example, and explains how you can achieve modularity in a strongly typed language even in that case. The type system just has to be flexible enough to allow for module functors.

In other words, the original article's argument holds only for strongly typed languages whose type system is too primitive.

[+] Martijn|15 years ago|reply
> (...) in an untyped language, you can run code that doesn't depend on the missing APIs but happens to be in the same module as code that does depend on them.

So this advantage is about writing code that isn't intended to be compiled or run yet. I can do that in a statically typed language too: I just put the code in a comment block.

Or am I missing something here?

[+] prodigal_erik|15 years ago|reply
If g returns an x and f takes an x, I can do f(g()) without knowing anything else about type x. Even type inference will choose an x that makes g compatible with f, or complain if there isn't any. So the system is still modular at the source level, which is important because it keeps the project comprehensible. What you don't have is binary-level modularity, but that's just an efficiency hack that dates back to when computers were almost too slow to run compilers. Now it should only matter to people who obfuscate by refusing to ship source (or source-equivalent intermediate files, which at least one Ada and one C++ implementation used to handle separate compilation of polymorphic code) and living with the lack of whole-program optimization.
[+] riobard|15 years ago|reply
Why not structural typing?