"The C compiler made me feel adequate. Unlike Ada, I regularly got my programs to compile. And so, I made my choice and went with C."
I think this dynamic plays out in a lot of technology evaluation decisions. It's probably why techs like Javascript, MongoDB, Ruby on Rails, Java, PHP, MySQL, Wordpress, etc. have gotten widespread adoption despite numerous technical flaws. They have a very low barrier to getting something useful up on screen, and feed peoples' need for instant gratification. When you feel good about yourself while using the language, you feel good about the language.
The interesting thing about the software industry is that network effects are often so strong that it's rational for a disciplined, expert developer to use one of these technologies rather than something more niche that plays to their expertise. You may hate Java, but if you need to use Hadoop/Storm/SparkML/OpenNLP and the myriad of well-tested libraries, it may be a lot better choice than building your own distributed big-data stack.
I guess this is the idea behind "Worse is Better" [1]. It makes me wonder if "better" will ever be "better" (perhaps after the pace of adoption in the software industry slows down, and we start spending time getting known product architectures right rather than finding the next big unknown product architecture), or whether by that time the entrenched base written on top of ad-hoc technologies will be too big to change.
> It's probably why techs like Javascript, MongoDB, Ruby on Rails, Java, PHP, MySQL, Wordpress, etc. have gotten widespread adoption despite numerous technical flaws. They have a very low barrier to getting something useful up on screen, and feed peoples' need for instant gratification.
It might not be just about that. Sometimes you don't know what you're doing, and you have to find out first. What use is it if you painstakingly design and craft a perfectly engineered program, only to find out that you actually needed something else? Being able to experiment is precious. Being forced to handle everything before the thing even compiles is not all that useful at times. There's a reason why interactive environments like Lisp and Smalltalk don't force you to define all functions that could plausibly get called (and when they do get called, you can even add them on the fly and continue).
This doesn't feel consistent with my experience. Significantly restructuring a large and complex piece of infrastructure is never going to be easy, but I don't think a strong static type system makes it any harder. If anything, it makes it significantly easier, just like a strong battery of unit tests would.
A sufficiently expressive type system will guide you and hold your hand over the course of the refactoring. It will determine for you the precise cascading effect of any code changes you make. And if you forgot to write a unit test to handle some edge case, the chances are pretty decent that the type system will detect it for you anyway.
In terms of writing a new piece of software whose design you haven't fleshed out yet, my approach in a language like Haskell is in fact very experimental, but quite different from something like Python. Rather than writing lots of little tests and trying to get bare-bones functionality implemented passing those tests, I often completely ignore tests and let the type system guide me. I try to flesh out the types that my program will need at a high level and just go from there. I call it "type-driven development", and it's extremely useful for early-stage exploratory programming, because it lets you play around with the structure of the program without worrying about any details. This is only possible in a language those type system is powerful enough to actually encode nontrivial aspects of a program's high-level structure.
I make sure at every point that I have a codebase that passes the type checker. It may not be interesting to actually run it at an early stage, but in many ways that actually makes exploratory problem solving even easier.
In many cases, this approach will reveal architectural issues early on that you may not have realized existed until you'd already written thousands of lines of code in something like Python. The real point is that either way you will have to face those issues if you want to end up with a working piece of software. But it's a lot easier to face them early on, when little code depends on them, rather than later on.
I think there's a distinction in exploratory programming between code with known inputs & outputs but unknown data structures & algorithms, vs. code with known algorithms but unknown inputs and outputs.
The former encompasses situations like contract development, signal processing, control & embedded systems, compilers, maintenance programming, etc. I've found that strictly typed languages like Haskell, Ada, or Rust can be very helpful here, even for exploratory programming. In this situation, you know (and have little control over) the data that goes into your program and the requirements that it must meet, and if it doesn't do so, it's because you made a mistake somewhere.
But many types of exploratory programming involve unknown inputs or unknown outputs. Think of data mining or scientific computing, where the issue is often that you have unclean data and your task is to figure out how it's messed up and how to fix it as quickly as possible. Or UI software (both web and mobile) and tech entrepreneurship, where the big unknown is often how the user will react to the software and where they will find value in it. I've found that dynamic languages are great for this, because the whole point is you don't know what the data will look like, let alone how it will be structured.
It was interesting to me that the two alternatives you mentioned were "write lots of little tests" or "let the type system guide what needs to be fixed". For a lot of the exploratory programming I've been doing lately, the most efficient approach is just "let it break". Go make your changes, and if some part of the system doesn't work anymore, just take note of that. Because the act of letting things break gives you important information. If you don't care, then obviously that feature wasn't very important to the system to begin with, and you should rip it out entirely. If you do care, you should fix it, and then write some tests to make sure it doesn't break again. This way, you can build a rough rank ordering of how important features are, so that when you're faced with engineering trade-offs (and you always are), you have a solid, experience-based intuition about what you really need to support and what you can let slide.
To some extent, "let it break" is not all that different from "let the type system guide what needs to be fixed." The difference is in what's breaking. The type-driven approach is basically "let the type coherence of the program break." In other words, compile the program, knowing fully well that it won't successfully compile. You see exactly where the types break down, and you can then figure out what needs to be changed.
With all that said, I like your distinction at the beginning of your comment a lot. A lot of my recent work has focused very much on a problem domain where the input data is very well understood, but the transformations applied to that data are complex and not all that well understood. I've found that Haskell has worked extremely well for me in this case, but I think you make a compelling point that it may not work quite so well in cases where the input data is the unknown, but the transformations are fairly well understood already. Perhaps this is why languages like Python have dominated the machine learning space, and strictly-typed languages just never really took off there.
The advantage of "let it break" is that you can continue to run the portion of the program that remains coherent. It's letting the user (who is often just yourself, or a small group of beta testers) into the system; if the user continues to find value in the program even when it's broken, the part that's broken must not have been that important. It's not unusual for programmers to overspecify the requirements when the requirements are unknown; you have to make a number of (often arbitrary) decisions just to get a program working end-to-end, and there's no guarantee that those decisions align with how a user will get the most value from the system.
And the disadvantage of that is that you possibly don't even know what's broken! It will come down entirely to how vigilant you've been with testing. Your argument is that if it's broken but you don't notice, then it must not be all that important. But I think there are tons of exceptions to that, and that's putting a huge amount of faith in yourself and beta testers to reliably test all important aspects of the software. Besides, if you do more or less figure out what's broken and what's not important, you still have to engage in a flimsy surgery of the codebase to excise the broken pieces.
I'm not arguing universally against what you're proposing by any means, but it doesn't seem like a clearly superior approach to me. The bottom line is that "let it break" takes many forms. There's your version, there's my type coherence version, there's Erlang's version with supervisor trees, etc. I think it's hard to argue that any of these is universally superior to the others.
It's superior for a certain problem domain, one in which the desired feature set is unknown, the desired feature set is simple, and the consequences of failure are limited. Consumer web & mobile startups, basically, as well as some limited small-business B2B software where the alternative is an error-prone manual process. It's probably not appropriate for enterprise software, and is certainly not appropriate for embedded, regulated, or safety-critical software.
This is far from the whole software industry, but it's a segment that tends to be of disproportionate interest to HN readers, given the startup-focused origins of this site.
Even if it fails to typecheck? Has the Haskell compiler gotten smart enough that it can stub out portions of the code with type errors and fail at runtime?
Actually yes, you can pass a compiler flag, -fdefer-type-errors, which makes type errors turn from compilation errors to simply warnings. That said, any code that does not type check will also not run and instead will throw an exception at runtime if called (laziness is key here). But if Part B was giving type errors and you only cared about testing Part A, then you are good to go.
Yep. It's easier to implement and makes more sense in a lazy language than what you're describing though. I don't use it in my work but I do use a related technique to isolate what I am focused on fixing.
In a strict language, a type error would mean the bricked off expression that doesn't type-check would spread like an infection to any expression that incorporated it.
In Haskell, I can dance around sub-expressions that don't type-check at runtime and evaluate things optionally.
Values still wouldn't work. You'd have to change their types to `() -> whatever` and that means a lot of churn.
Even strict-by-default languages that let you easily make values explicitly lazy would mean type level churn, if not term level.
The value part is important because it means you can't really talk about partially implemented hypothetical results where you know how to handle one branch of a function, but not another. This is a super common divide-and-conquer technique in Haskell.
Could possibly half-ass it if you have some kind of panic functionality, but it'll fail unconditionally in a strict language when the result of the function is forced. In Haskell, if you don't use the bottom, it doesn't blow up.
Big +1 to this. The takeaway for me is to develop languages and tools that scale with the domain-expertise curve. That is, as you learn more about both the problem and solution, you can evolve your implementation without having to start over or get many things right from the start. The goal should be to both tighten the feedback loop and provide a smooth path from tinkering towards production grade.
Static languages, in particular Haskell, PureScript, etc, are unparalleled when it comes to fast prototyping or design space exploration. The compiler can show you whether your idea even makes sense before writing big chunks of business code.
How? By building the structure/hard stuff first and letting the compiler verify it while leaving out the uninteresting parts.
A very simple example. Let's say you have some data model with people, addresses, whatever. You want to see what you need to do to get a list of addresses for some group of people.
You start by defining the structure of your data model (this can depend on DB schema, business requirements, whatever):
data Person = Person { name :: String, city :: String }
data Address = Address { city :: String, zipcode :: String, street :: String }
Now, go straight to the hard part. No need to fuck around with DB connections, mock data, whatever:
Leave out the implementation for now. So probably we are gonna need a way to get an Address for a Person. Ok:
addressForPerson :: Person -> Address
addressForPerson = undefined
Again, leave out the implementation. Now go back to `getAddresses` and see if we can implement it now:
getAddresses = map addressForPerson
Cool, the compiler doesn't complain. So far we've verified that if we have a function like `addressForPerson` we can safely assume that our design is going to work.
Now we can go back to `addressForPerson` and implement that. But turns out, we only have a city stored in Person, so the best we can do is get back a list of potential addresses for each person. So we need to adjust that type signature:
addressForPerson :: Person -> [Address]
addressForPerson = undefined
Leave out implementation, it's trivial so no need to spend time on it now. But now the compiler complains that `getAddresses` doesn't make sense anymore! We are saying that, having a list of people, we can get a list of addresses, but that's not true anymore. We found out that we can only get a list of potential addresses, so we have to adjust the type signature.
getAddresses :: [Person] -> [[Address]]
Now every use of `getAddresses` will have to be adjusted to the new requirements. Etc etc.
Granted, this is a very simple example, but I hope it illustrates how static languages facilitate extremely fast prototyping. Sure, a Lisp REPL is very very nice, but in my personal experience it's no match for a type system. We didn't even have to mock data to start experimenting!
(And yes, Haskell, OCaml, PureScript, Elm etc all have a REPL too).
I don't see this example particularly enlightening. I'm not really ever seeing problems where I'm stuffing an integer into where I wanted a string in dynamic languages. Or stuffing a Person into a Gorilla. These problems seem important and they tickle programmers warm fuzzies (unless you're like me and you've been programming static and dynamic for more than 20 years).
The small amount of scientific study around what type checking and static typing gives you is that you avoid around 3% of bugs but even those 3% are easier to fix than the effort in writing them correctly in the first place. While I agree that the subject needs more study but what we know now: the real win would be to have a dev process that lowers criticality of failure (like TDD, CI and other Agile processes) and dispense with static typing for most projects.
While I disagree (static typing is much more than catching type conversion errors), my point was that it makes experimentation and design exploration much easier and faster. Without types, you'd need mock data for even the simplest of programs, and changing mock data structure is much more painful than changing a type somewhere and getting the places (all the places) where your program turns then out to be incoherent. Not to mention that types are much more than Persons or Gorillas, they can be higher order functions too (Person -> Gorilla) that are passed around.
Sure. But does your static typing system allow you to skip writing automated tests? Pretty sure everyone writes tests these days unless they're spiking something. If you're writing tests, you will have to come up with the mock data anyway. And the kind of exploration you're doing I tend to do from a test regardless. Once my exploration is done I also have a test that captures my work. You still have to write one. The question of whether to static or dynamic is moot in this case. It no longer matters much.
Maybe I need to spend some time in Haskell and come over to your way of seeing it. But the studies show that constructing the type system for your problem is very difficult and remains error prone. People routinely make mistakes with types systems of all kinds. I've found I spend more time making the compiler happy that it does making me happy.
The sibling comment points out property based testing (see QuickCheck), which is a good idea in any language. And yes - you don't need to write that many unit tests (but still some) when using a type system, because you can encode a lot of invariants in the types.
But still, my point stands - writing tests during early experimentation is a lot harder than just altering some type and letting the compiler infere what needs to be done or what you broke.
I've spent some time writing Clojure and still use dynamic languages a lot for smaller tasks, so I know both sides. This doesn't mean much, but I'd really encourage you to try some ML language and spend some time with a nice type system. Haskell is probably too big a time investment, so maybe try Elm [0] - it's very beginner friendly and a lot of the people using it come from JavaScript.
I don't think I've ever seen a bug that shouldn't've been a type error. When I encounter a bug that wasn't a type error, it's usually a sign that my model is subtly incorrect. (E.g. I recently found a bug where one codepath wasn't filtering a tree that it should have been. This clued me in that filtered and unfiltered trees need to be different types, and actually making that change simplified a lot of code, since it forced me to figure out the correct point at which to do the filtering so that I could do it in one place)
The data shows that you see 3% bugs that could have been fixed with static typing. 97% are from other things. Mind you this area has not been studied much and needs more research, but that's what we know now.
This seems to be a very common situation with programmers. When presented with evidence to the contrary, we all of us tend to respond in this sort of "it's wrong because it conflicts with my experience". You realize this is essentially the classic flat earther denial. I'm not picking on you: I've done it too. It seems to be an industry hazard.
I think there's a level at which you have to be willing to believe your own eyes over the consensus. And there are levels of how established the consensus is. Perhaps it's particularly common for programmers because the industry moves so quickly, so all of us can quote previous established wisdom that turned out to be utterly wrong.
I don't think the OP is particularly lucky. I never see errors like that, either. Just because the language doesn't enforce strong types doesn't mean variables don't conceptually have a type.
It seems like a symptom of other problems more than anything, and using a staticly typed language will just make those problems manifest in a different way.
> I'm not really ever seeing problems where I'm stuffing an integer into where I wanted a string in dynamic languages.
If you are writing all of your code from scratch, then it doesn't happen too often. You usually have enough of the current context in your head to avoid most type level mistakes.
The real strengths come from two situations.
The first is refactoring. There is just no contest here. Having every single place where your change affects the test of the program identified automatically is extremely useful.
The second is returning to old code and either trying to understand it or make small changes to it.
I see simple type errors like this ALL THE TIME in legacy code bases I get pulled in to salvage. Even in a dynamic language with a fair amount of static checking like Objective C I find people putting the wrong kinds of objects into arrays etc. So when people tell me now that they just don't make type errors in their code I don't believe them. People make mistakes, particularly when tired and under pressure.
If people are writing code, thinking, "I have no idea what else is stored in this container, so I'll just add an object of type Foo because I can!" then the project has bigger problems than static or dynamic typing.
The reality is that a lot of code is written by average programmers trying to get a lot done in a hurry. A static check is the difference between a subtle bug making it into production and a helpful, specific error at compile time. Most projects do have bigger problems than static or dynamic typing but static typing is even more helpful there than it is on projects staffed only by top-flight coders with plenty of time to do things right.
In the three years I've been doing consulting work I've had to clean up a lot of legacy code. Believe me, people do make these mistakes in real-world code all the time.
That may be useful if you have a lot of data types and transformations between types. I don't. I do a lot of work with (molecular) graphs, which have two data types: nodes and edges. If you code up a depth-first search which uses an edge instead of a node, or vice-versa, then even the most trivial of tests will find it in a dynamic language. A static type check adds little.
For an example of a project which I worked on for months, given N molecular graphs, find the maximum common subgraph which is in at least M of them. I did all that in Python. I don't think static types would have made a difference.
Matrix algorithms are an even more clear example. They work with a single numeric type, in a 1D vector or 2D array. It's hard to confuse the types. On the other hand, it's easy to get the algorithms wrong, and choosing the right algorithm can require a lot of design space exploration.
Just think of the number of clustering algorithms available which you might want to try out through prototyping before spending the time to scale things up. How does static typing really help there?
I'm not an expert on type systems, but I don't think it would add that much in your case. You might get more out of it if you wanted by using phantom types, which is just a way to differentiate among structurally identical types, like this:
reduceGraph :: Graph a -> Graph Reduced
Now you can't pass unreduced graphs to functions where you expect some sort of an already reduced graph, and you could encode as much invariants as you wanted in your types, but yea, in cases where there are homogenous types with little variance (machine learning, statistics, your use case) static typing won't probably bring that much (unless you go nuts with dependant typing I guess).
> For an example of a project which I worked on for months, given N molecular graphs, find the maximum common subgraph which is in at least M of them. I did all that in Python. I don't think static types would have made a difference.
Even with no knowledge of the problem space, I can think of a whole bunch of static types off the top of my head.
You've already given your input type: N molecular graphs, and a number M.
You've also given your output type: the maximum common subgraph which is in at least M of the given N molecular graphs.
Straight away I can spot a problem with this, since there are no programs of this type. In particular, there is no valid output when M > N. Hence I would refine the input type to ensure that M <= N.
Since it appears in your input type, it would be useful to have a type for "molecular graph". Likewise, based on your output type, it would probably be useful to have types for "subgraph of g", "common subgraph of gs", "maximum common subgraph of gs" and "subset of gs".
Naively, I'd give your program a type something like this:
(m : Nat, gs : Set MolecularGraph, m <= size gs) -> (hs : SubSet gs, size hs >= m, MaximumCommonSubgraph hs)
Programming with type constraints like these would be very different to using Python, where you'd have no guarantee that a value is even a graph, let alone a maximum common subgraph of the input set.
Whether it would be more or less appropriate for your situation is, of course, an entirely separate issue ;)
Matrices and vectors can be thought of as typed by dimension[0], and it's easy to get that wrong. (I want to say c++'s eigen can do some compile-time checks if it has the dimensions available).
[0] In some languages, like older versions of pascal, arrays of different lengths were different types, which was a horror for writing generic code.
Yes, C++ is a particularly easy language to get that wrong, where float * can be a pointer to scalar, vector or 2d array. Other more strongly typed languages, like Python, will make those errors easy to spot with any sort of test case.
Beyond strong typing, do you get the sense that static typing significantly improve prototyping ability for matrix analysis code?
> "Static languages, in particular Haskell, PureScript, etc, are unparalleled when it comes to fast prototyping or design space exploration. The compiler can show you whether your idea even makes sense before writing big chunks of business code. How? By building the structure/hard stuff first and letting the compiler verify it while leaving out the uninteresting parts."
Why shouldn't I be able to do this on a meta-level? There does not appear to be any restriction on, for example, Lisp code regarding the use of problem solvers and provers. The same should go for other metalanguages (perhaps even better, without forcing me into specific semantics).
(Likewise, generative solutions might reduce this problem simply by performing a sequence of correctness-preserving transformations from a higher-level specification. That would shift the burden to checking the specification, which would require a custom checker anyway. Haskell's capability to check or automatically derive stuff seemed limited the last time I looked at it.)
Agreed. My comment wasn't aimed that much at Haskell in particular (even if it's my language of choice), but rather the idea of using structure/types for fast experimentation, rather than just a REPL.
Also, using a type checker to wholly check your Lisp code basically makes it a static language, the difference then is pretty much philosophical I'd say.
Recently I've found myself having severe problems with seeing clear borders between many concepts. IF expressions, type-based dispatching, matching, generic functions, predicate dispatching...a lot of it basically boils down to the same thing, only expressed in different ways. The problem is that the same thing goes for type systems in my head. Types define sets of values, but if your language hardcodes what kinds of sets of values you can express, chances are that you'll have to express whatever residual distinction between values there is (that you can't encode into the value type) into one of the above-mentioned decision-making processes, and at that point, they go sort of out of sight of the typing/proving machinery.
I'd rather avoid someone forcing me into using a hardcoded type system, among other reasons because I'm contemplating a (perhaps novel?) search-based program/expression-manipulating system that wouldn't play well with any fixed typing system. For one thing, the types of expression results (being return values of operators applied to input expressions or programs) can easily be emergent (is the resulting expression linear? Is it an odd function? Is it analytically integrable? Etc.) and not easily arrived at in something ML-like. If I had to use something like Haskell, I'd still be rolling out my own type system/prover on top of the existing language implementation anyway. However, types in general (as something you can compute with in the meta-program) would be absolutely mandatory for a system like this (for example, some algorithms or operations can have more desirable refinements that work only on subsets of values - consider determinants of triangular matrices, with triangular matrices being an emergent type - and I absolutely want to find out if such a thing is available for the specific problem being transformed), just not types that any specific existing language offers, to my knowledge.
So it's not that I dismiss the notion of proving things about programs - that is a vital thing to have - but the ability to impose restrictions on a program from the outside seems much more flexible to me than going for some fixed type discipline that could force me to straddle inconvenient fixed walls in the structure of a complicated program (such as this transformation system that would search the tree of possible transformations for a result with some desirable properties, with the transformations concisely expressed in some very-high-level DSL source code).
While I agree, getting used to that mode of thought takes time. IME you have to write a lot of Haskell (or OCaml) before it becomes a very fast prototyping language. Of course, it's 100% worth the investment, and I prefer prototyping in OCaml or Mercury to basically anything else, but it certainly wasn't like that from the first time I picked up OCaml.
I used to do this with .. Java 5. Eclipse live typechecking allowed me to design in Interfaces until I had what I called the parametricity right. The thing is, I stumbled upon this out of paranoia (I hate being wrong down the road). Never was I invited to think or design this way. Maybe with embedded contracts in comments; but not in a real time, live repl prototyping way.
I think that you are comparing static vs dynamic typing but the grandparent comment is more about strong vs weak typing.
If instead of having separate Person and Address types you just use strings for everything (even "lists" can be strings - just keep the data serialized into HTML, or XML) then you can your program is likely to "do something" if you just compose a bunch of functions together. Sure, your code might have a bunch of logic bugs (like HTML injection / XSS) but at least it does something!
Static typing is great once you leave this "stringly typed" world and start separating your values into strongly-typed types. When you get a type error from passing a value to the function then its nice if you can be warned about this at compilation time.
Right, exactly. I want guidance on whether code (new code or a modification to existing code) can make sense before I've written all of it. With modern Haskell I can start to ask those questions before I've written all of the term-level code and before I've written out all of the relevant types. I can start working with whichever is more convenient at the time, and feel ahead to where things might break down before I get there.
I spent the last few years in scheme, caml, haskell, abstract algebra .. one day I remembered that I used to spend hours toying with raw PHP4 just saw I could see "live HTML", it was so exciting.[1] There was nothing of intellectucal value in it, it was just a superb toy. No amount of pedagogy can make haskell shine this way for a newcomer.
[1] I somehow forgot that I enjoyed it a lot and loved that thing that I'd avoid like plague a few years later (I don't care about it now, for or against)
"The best camera is the one you have with you" is a good adage. For prototyping a language needs to feel effortless so you can deal with the essential complexity of the problem and not hesitate to jump in and experiment.
For implementation, it's whatever tool best fits the job.
C is a good compromise, but it requires real discipline. The CPython and Linux code bases are great examples of how C can be wielded well in practice. But of course C also famously offers unlimited opportunities to make a complete hash of things. I certainly wouldn't recommend it to anyone as their first and only language.
An alternative approach is to be bilingual. For my current project the final code is in C++98 - sometimes there's no good alternative. But I do most of my experimentation, tooling and prototyping in Python. Python's excellent C/C++ interoperability is a huge benefit in this.
I definitely agree with the article that you shouldn't avoid hard things that improve you. But in choosing between ADA and C, the right answer is both and neither.
Ada has a lot of cool stuff going for it. I liked the type system well enough and a well written Ada program is very easy to read. My problem with it was I always had trouble with the stuff like
while ... loop
if ... then
case ... is
The number of times that a program failed to compile because I got the magic words wrong drove me nuts! The author is bang on in the comparison to C which despite its flaws is very internally consistent.
There's actually sense to the magic words. Each word which opens a block has a unique word which ends the block. So, begin..end, if..end if, loop..end loop, etc.
The advantage of this is that it's much harder to screw up block terminations --- one thing I've done many times in C is when closing a chain of blocks with:
}
}
}
do_something();
}
}
is to miscount the braces and put do_something() in the wrong place. In Ada, that'd be:
end if;
end;
end case;
do_something();
end loop;
end;
I agree. I've thought of writing a CoffeeScript-for-Ada thing with slightly nicer syntax. (I don't dislike Ada's syntax, but it's occasionally quirkier than necessary.)
That said, its easy range types and discriminated unions make writing C-level code SO much nicer.
IMHO Ada violated the most fundamental principle of programming, which is DRY (Don't repeat yourself). You have to invoke the name of every function twice while defining it, etc. The language seems to be designed for multi-page blocks, which are a bad idea to begin with.
It also lacks memory safety, despite being a safety-oriented language.
If someone added a better syntax to Ada (probably easy) and Rust-like memory safety (probably hard), that would make it a higher-value proposition.
I'm a big believer in RYINTBDIMOTB (Repeating Yourself Is Not The Big Deal It's Made Out To Be). In a safety-oriented language, checking for consistency in all of the repeated instances of a thing is one form of low-hanging-fruit safety check.
If you're restricting yourself to access types you have good memory safety. Access types cannot be aliased unless declared so, and are subject to accessibility checks to verify that they are "live". Unsafe stuff in Ada must be used explicitly; you cannot, in general, accidentally the whole stack or heap unless you are using a pathological coding style.
Ada requiring the redundant name after "end" isn't a violation of DRY at all. The DRY principle is about the codebase as a whole, where repetition can lead to elements getting out of sync.
For example, instead of having an ORM define a mapping between classes and tables in, say, XML, DRY dictates that the mapping ought to be where it belongs, namely in the class declaration itself.
Ada's repetition is really more about readability. If you see an "end", you always knows what it matches. (Personally, I think it's useless.)
> Ada requiring the redundant name after "end" isn't a violation of DRY at all.
Yours is an arbitrarily narrow interpretation of the principle. To quote Dave Thomas, DRY states that
"Every piece of knowledge in the development of something should have a single representation."
The name of your function or block is one such piece.
I agree that having the compiler check the consistency of the redundant statements ameliorates the situation somewhat, but it would be better if the problem wasn't there at all.
The page you linked to directly refutes your claim of narrowness:
Most people take DRY to mean you shouldn't duplicate code.
That's not its intention. The idea behind DRY is far
grander than that. [...] DRY says that every piece of
system knowledge should have one authoritative,
unambiguous representation
Note the terms knowledge and representation. He's referring to things like declarations, data models, schemas, facts. Duplicate source code symbols has nothing to do with it.
DRY is not "be terse". For example, turning a literal into a constant is DRY, because it reduces to a single "authorative, unambiguous representation". But "x = x + 1" (which certainly looks redundant) is not anti-DRY, nor is "end Foo;".
> IMHO Ada violated the most fundamental principle of programming, which is DRY (Don't repeat yourself).
DRY is far from being "most fundamental". Much more fundamental and important
are keeping modules composable, not mixing things of different abstraction
level in the same block of code, avoiding circular dependencies, or avoiding
unnecessary dependencies are more fundamental. DRY is just most easily
enforcable, that's why it's so popular.
To my mind, this is a corollary of DRY: if you are not repeating yourself, then you are reusing code, which, by definition will have made it composable.
> not mixing things of different abstraction level in the same block of code
Never heard this one, frankly. I happily mix addition (arithmetic, machine-level operation) and, say, Dice Coefficient in the same line of code.
> To my mind, this is a corollary of DRY: if you are not repeating yourself, then you are reusing code, which, by definition will have made it composable.
You don't get composability from DRY, as merely extracting common snippet of
code into a function doesn't make magically the function easy to put somewhere
else. Just "reusing code" doesn't make it composable. You can reuse body of
a car for something different, but it doesn't make it composable, so there's
no "by definition".
DRY is just a different thing than composability.
>> not mixing things of different abstraction level in the same block of code
> Never heard this one, frankly. I happily mix addition (arithmetic, machine-level operation) and, say, Dice Coefficient in the same line of code.
It's a practical thing. You don't put opening TCP connection to a database
service in the same chunk of code as building SQL query to be sent through the
connection.
>> avoiding circular dependencies
> Probably a good principle, but not fundamental
Oh yes it is. If code doesn't adhere to it, it ends up being a tangled,
monolitic mess of brittleness. As with every rule, there are cases when it
makes sense not to apply it, but this doesn't make it less fundamental than
DRY.
I think there's a huge gulf between repeating oneself with multiple snippets of code which would cause problems if not kept in sync (prone to people changing one without realizing there are others that must be updated too) and merely repeating the name of something at the termination of its definition in addition to naming it at the start. I regard the latter as a species of Poka-yoke [1], and am relatively happy to accept the extremely minor cost of repetition.
And Ada's repetition could easily be automated in the IDE or smart editor (does sublime have an Ada-syntax--and I probably don't even need to ask for emacs). The repetition is there for readability and maintainability, so the author loses nothing if it is automated.
ADA wasn't made for the average human, in large project a DRY means dozens of duplicated content, not just a small name redundancy. For the DOD I'm sure this wasn't even a question.
I worked with Ada in the early part of my career. For the most part, the strong typing really helps but it can often make the logic messier that you can't push an integer subtype one past the maximum value. Unlike C, the code base tends to remain quite readable and understandable even in big projects and it is more fun to use than Java (which I find to be soulless and dull).
I loved this article. I never knew about Userland Frontier Kernel and enjoyed looking it up. I also enjoyed other bits of history sprinkled in the article especially about Macintosh switching from Pascal to C and about what it was like to use the Ada and Pascal compilers.
The author's central argument is "don't give up on a language that's hard because it might have significant, practical advantages." In particular, he argues that Ada might be superior to C because C suffers from segfaults or other memory-related issues that Ada does not suffer from. Because C was initially easier to write, he went with it.
I think this principle — that it is worth sticking with a language despite difficulty in writing it — is flawed. While Ada was both difficult and resolved memory issues, languages like Python graceful avoid memory issues without being difficult to write.
Naturally, language safety is greater than ensuring memory integrity. Haskell ensures memory integrity and type safety and happens to be a hard language. I think that Haskell is a stepping stone. That in the future, languages will be safer, but also easier to write. That we haven't solved this paradox — that a language can be both safe and easy to use — is merely a matter of time. For now, I'll keep writing Python.
Ada is fairly easy to use once you know it. It has a few quirks and many nice features. It just takes a bit more time to learn, because its way more strongly typed than most other languages. IMHO, its main advantage is not safety, though, but readability and long-term maintainability. You can take a 30 year old Ada program and compile it with the latest version of GNAT.
Anyway, please don't compare languages like Ada and Python. That's like comparing Forth and Smalltalk or apples with oranges. They have different purposes. Ada compiles to executables that run around as fast as C and C++, and like those languages it's not very suitable for rapid lego-brick type programming. For gluing together existing libraries, languages like Python are awesome but you wouldn't want to write an Operating System in Python.
My point is that "fast as C" is meaningless. You can write quite abstract/unoptimized C. And you can write Python code with a few Cython-specific annotations that rival "regular" C code.
Python won't segfault but it is easy to get runtime failures that wouldn't occur in Ada. It may even suffer from C-like levels of security vulnerabilities - if the internal state of the program is thoroughly corrupt, as is the case for most Python runtime errors, then it would be odd if that state didn't create an exploitable vulnerability.
I used to be a big fan of Python, but having used Scala for a few years I wouldn't go back.
And now whenever I encounter a difficult moment learning new languages like Haskell or Pony, I try to remember my Ada/C decision and stick will the language whose compiler is trying to tell me I'm doing it wrong.
What this tells me is that the author gained some insight, but still did not learn his lesson: one of the worst mistakes one can make in programming is learning new languages, just because they are there. Pick at most three languages covering 95-99% of required functionality, then master the living daylights out of them; you'll become seemingly superhuman at programming, writing small, portable code in fraction of the time it will take dingle-dongs out there to do the same thing. Just because you know your tools so well. To be clear: it takes about a decade working in a language every day, eight hours per day, to begin to master it. So that's per language, and I wrote pick three languages. Don't cater to fashion, because there is always some dingle-dong out there thinking he can invent a new language where things can be done easier and better, but that's not true at all.
Sean misses the irony that he would have given up on writing a lot of software if he tried to do it in Ada rather than C.
I think C is an excellent and useful language which doesn't attempt to change the way you program. While one can argue that one shouldn't be able to compile mistakes; the cost of avoiding mistakes often exceeds the cost of amending them. A sort of personal “It's easier to ask for forgiveness than for permission” situation.
I often ponder whether the time I spend debugging C programs is worth it. Each time I conclude that I wouldn't have bothered to write it without C.
I wrote an incremental generational garbage collector in C, and rather regretted the time I spent debugging it. If I wanted it to be at all concurrent, I think I would've given up doing it in C.
Right tool for the right job and all that. But lately I've strongly been preferring Rust, Ada, and ATS for anything I'd use C for.
Fair enough, I do like ATS as well; but there are a lot of programs which are just too frustrating to write at all without being paid a lot of money for a decade.
Turbo Pascal, Turbo BASIC, and Modula-2 don't have any of the ugly formality of Ada though. I'm not making the point that C and C alone is the only language less frustrating than Ada. I'm saying that for many systems, the comfort and simplicity of C allow you to solve problems which would be too frustrating to solve in Ada.
I find Modula-2 pretty dreamy; it's a shame that it's mostly dead.
I used to use Modula 2 for embedded work on small CPUs. It's hard to get programs to compile, but then they usually work. This is important when your target doesn't have much debug capability.
I don't think a tool has to be hard to use to be better. You can learn most of Lisp and Smalltalk very fast, but they're regarded as some of the most useful and effective tools out there.
How does smalltalk compare to objective-c? It's on my list of languages to learn because it started with message passing, but if it's user experience is as bad as objective-c I might pass
It depends on how you think objective-c's UX is bad. Smalltalk's syntax is much simpler than O-C's. It has three message passing formats:
object message.
object + arg. "There are very few valid names for these sorts of messages"
object argNameOne: argOne argNameTwo: argTwo.
Double quote is a comment, period is the statement delineator, and semicolon is the cascade operator, allowing you to send multiple messages to one object. It has a block (or lambda) syntax similar to Ruby's, although multiple blocks can be passed to a function.
This is pretty much all of SmallTalk's synax. Yes, really. There's also syntax for local variables, and returning values from method calls, and maybe one or two other things. That's it.
It should also be noted that Smalltalk is inseparable from its IDE and image. GNU Smalltalk tries, but it's not the same. To speak of one is to speak of the other, so I shall speak of both.
Smalltalk code exists entirely within its image, along with all the tools that make up the IDE. It's sort of an OS in and of itself, although you can interact with the native system. The practical upshot is that when you edit smalltalk code, you have all the source code for the entire implementation along for the ride: You can not only query Smalltalk about your own code, you can query it about itself using the same mechanism. You can also edit and debug your app in real time, as it runs. Smalltalk's debugger allows to to inspect every single stack frame, and make changes to your source as necessary. You aren't editing text that will become objects: You are editing objects. And because the entire system state is saved to the image, none of the changes you make to the running code will be lost, unlike most Lisps, where changes you make at the REPL go away as soon as your app shuts down. It's truly unlike anything else you will see.
I'm not really well versed in the language, but from what I've seen it takes the whole 'everything is an object' concept seriously, which I think is kinda fun.
For example, booleans are an object that can have two values: True and False. There's also two methods on the boolean object, ifTrue: and ifFalse:, and they both take a code block/anonymous function as the only argument. Both True and False override these functions. True's version of ifTrue: calls the code it's passed, and False's version does nothing (and vice-versa for ifFalse:).
For example:
a < b
ifTrue: [^'a is less than b']
ifFalse: [^'a is greater than or equal to b']
They're implemented more or less as a standard library package rather than a language construct. You could potentially extend the boolean class with different implementations of ifTrue and ifFalse, maybe reversing the logic or logging the branch taken or whatever. The functionality can be changed dynamically.
I think it's neat when a language eats itself like that.
Yeah. Smalltalk is pretty much that. Even moreso than Lisp. And now for the mandatory quote from "A Brief, Incomplete, and Mostly Wrong History of Programming Languages":
1980 - Alan Kay creates Smalltalk and invents the term "object oriented."
When asked what that means he replies, "Smalltalk programs are just objects."
When asked what objects are made of he replies, "objects." When asked again he says
"look, it's all objects all the way down. Until you reach turtles."
I was taught Ada at university, it is a complete bitch of a language but it forces you to be disciplined and do things "Properly" - something that is crucial in real-time systems.
I don't regret being taught it, I do however regret that it was used as the main teaching language meaning that real world usage was an after thought!
I think this dynamic plays out in a lot of technology evaluation decisions. It's probably why techs like Javascript, MongoDB, Ruby on Rails, Java, PHP, MySQL, Wordpress, etc. have gotten widespread adoption despite numerous technical flaws. They have a very low barrier to getting something useful up on screen, and feed peoples' need for instant gratification. When you feel good about yourself while using the language, you feel good about the language.
The interesting thing about the software industry is that network effects are often so strong that it's rational for a disciplined, expert developer to use one of these technologies rather than something more niche that plays to their expertise. You may hate Java, but if you need to use Hadoop/Storm/SparkML/OpenNLP and the myriad of well-tested libraries, it may be a lot better choice than building your own distributed big-data stack.
I guess this is the idea behind "Worse is Better" [1]. It makes me wonder if "better" will ever be "better" (perhaps after the pace of adoption in the software industry slows down, and we start spending time getting known product architectures right rather than finding the next big unknown product architecture), or whether by that time the entrenched base written on top of ad-hoc technologies will be too big to change.
[1] https://www.dreamsongs.com/WorseIsBetter.html