This reminds me that at the beginning of XX century, physicists talked how they have mostly completed the grand building of Physics, figured all the important things out but maybe just overlooked a few details.
He keeps on pressing his imaginary partner in this conversation to just concentrate on the number of equivalent programmers rather than any other measure like performance or probability of bugs making it into production and the cost of fixing them, and then when they decide there's no more gains to be made from programming speed, all of a sudden we are at the pinnacle.
No, we are not at the pinnacle, because programmer speed (as he very well knows, because he keeps on putting those objections aside) is not the only thing that matters.
It feels like the early language wins were due to the languages themselves introducing new concepts, not just syntax changes. Allowing higher-level reasoning.
Then, past a certain point, most of the new concepts started coming in via frameworks and libraries (although of course things like functional programming and interesting typing approaches are still language-driven tools).
Thus, these days, I would look at libraries and frameworks as sources of new productivity unlocks. E.g. in the Web world, jQuery saved millions of work-hours and qualitatively unlocked some new things. Then Angular (and eventually React) started realizing huge savings from declarative UI definitions. My preference for one next innovation in this area is a higher-level framework for user input modeling (a huge source of bugs these days).
I'd bet that, because the erlang family (erlang, elixir, erlang flavored lisp, ....) introduces important concepts when multiprocessing is involved, that they lead to a non-linear increases in productivity when multiprocessing is relevant to the problem. Further, multiprocessing may be more widely applicable than widely recognized, because the benefit has previously been lost to the former complexity and error-prone nature of multiprocessing.
The language still matters, but indirectly, because it constrains which frameworks can easily be built. It's doubtful that jQuery and React would have been invented using Java. Even JavaScript is nicer with a language extension (JSX).
The issue is that if you want to do template-like things using language constructs (internal DSL), the syntax is quite limiting.
There are tricks using annotations and strings, and Java 8's lambda expressions do help. But it's still awkward to define a React-like mini-language within Java.
> It feels like the early language wins were due to the languages themselves introducing new concepts, not just syntax changes. Allowing higher-level reasoning.
I was looking for somewhere in these threads to post my comments, and I think this is the right place: yes, you're exactly right, it really is about higher-level reasoning.
Assembly enabled one to think about instructions and operations instead of hex codes. Structured languages enabled one to think about programs, rather than sequences of mnemonics. Functional programming enabled one to think about functional transformations of data; object orientation enabled one to think about collections of functionality grouped with data; both structured the structure, in different ways. With each improvement, we were able to reason at a higher level than before.
But we're not at an optimal level yet: we still do not have mainstream ways to manipulate the structure of our structure (… of our structure of our structure, ad infinitum). We still don't have a mainstream way of dealing with our programs as data.
It may not be mainstream, but we do have a way: it's older than every other programming language other than Fortran: it's symbolic expressions, which enable one to reason about and manipulate one's code as data (and data as code).
The article we're both replying to is a prime example of the Blub Paradox: Uncle Bob can see how Java is better than C++, C, assembler or hex codes, but can't see how Python — and, yes, Lisp — are better than Java.
Let's say I work exactly 40 hours and week and 50 weeks a year. Then a 5% increase in efficiency means that I can do 2 extra weeks of work per year. That's not a lot, but on a team of 26 programmers that's essentially the equivalent of hiring a new employee (without the downsides of a bigger team and the cost to bring someone up to speed).
Nice blog post. Of course it doesn't take into consideration the time to understand and fix bugs, to add new features, to run tests, to review changes, to make large-scale changes, and to on-board new programmers. The arguments in [1] still apply.
Definitely. It's probably the best formatted dialog blog post I've seen in a long time. I know it's so simple, just using blockquotes, but that's what makes it so great.
I'm really surprised to see people say they like this style. I hated it. I was reading it and was saying to myself "why is this fake questioner so fixated on a number?" It is the straw man interviewer. I would much rather he get to his point than thank this fake dialog.
This post is a great oversimplification. Normally something like that could be ignored to get the bigger point across, but it kinda is the point, so I call BS.
Have 10000 experienced ruby programmers and 10000 experienced java programmers solve different tasks. I'm pretty sure that rubyists are going to solve their problems using 50% less time with at least 30% less LOC just by the virtue of having a more expressive language. Now I'm not trying to argue that ruby is strictly better than java, I know both languages' deficiencies. However, the metric author uses is development time, and by that metric more expressive languages will always win, hands down.
The point of the post is to show diminishing returns are occurring with ever evolving languages. The argument one should use X language because you can develop in it faster is increasingly not useful to discuss. You are stuck in the weeds arguing X will always win, when the point is there's nothing to win.
I disagree. Many programming languages are here just because they're good enough, not because they are perfect. For example, C is a good enough systems language. However, when compared to Rust, it's worse at most important criteria, including being more error-prone and less expressive (it's absurdly simple, though). Now you say there's nothing to win, but to me the advantages are obvious: I can avoid several classes of critical vulnerabilities and at the same time not substantially lose any performance.
How would that be possible without search for the holy language grail?
And now have those 10k programmers work on a single project.
I bet the 10000 Ruby guys are gonna create a big, unmaintainable, broken mess. Even Java's primitive type system would be a big help when it's about software development at scale.
This is a big advantage of static typing that people miss -- the restrictions it puts on developers help them to learn the codebase they are working with. In the real world, where engineers contribute to existing codebases more often than they start projects from scratch, this is a pretty important benefit.
A thousand times this. When I was assigned to an existing Ruby on Rails project of about 30 kLOC, I needed around a month to get productive because it was nearly impossible to follow calls around the system because of auto-generated, not properly typed methods all over the place.
I felt like I spent roughly the same amount of time working around Java's type system with copious amounts of boilerplate as I did fixing wrapping my head around type-related bugs in ruby, especially at scale because of how inflexible every Java architecture seems to become after 5 years. Java's types give you so, so little additional real world "safety" but easily require 10x the verbosity to express the same logic as a dynamic language. You still end up getting hit with null pointers and invalid argument errors.
I think Rust and Swift and Elm strike the right balance between developer productivity and a strict type system that actually improves the quality of production software.
> You still end up getting hit with null pointers and invalid argument errors.
Those are not the kinds of problems a type system is intended to solve. Those are problems of data, not of types.
The amount of boilerplate in a Java program is pretty low if you aren't trying to abuse or work around the type system. It certainly is not 10x what you end up with in Ruby or Python. Java 8 has made significant improvements, but it wasn't that bad before in many cases.
Instead of trying to work around Java's type safety, you should learn to use it properly. Good programmers think about types whether or not they are forced to by the language, so the restrictions imposed by Java should not be burdensome -- in general, you should already be mentally applying some of those restrictions to your code so you can avoid passing the wrong types around.
They are problems type systems are intended to solve: It just happens that Java's is too weak to deal with it well: They best you can do is use an option type, but even then, Java's Option type is far weaker than in other languages. Languages where null doesn't exist are far nicer.
Java's boilerplate comes from lacking type inference and from having a type system that is too weak, not one that is too strong: I'd argue that people's love for dynamic languages without type systems is cause by how people's idea of what types are, and what they do for you, come from Java and old C++, as opposed to something more powerful.
Nulls are not a problem type systems are intended to solve -- they are a feature of type systems that was created on purpose. You might not like them, but that doesn't mean they constitute an oversight or omission on the part of the designers of the type system -- they wanted them there. They are useful.
Many people misunderstand the option type in Java. It was created to make stream processing easier. It was not intended for general use, as in other languages.
Regarding illegal arguments, they are unavoidable in any type system. Suppose you defined a function that splits a string into an array of n-grams, to which you pass the string as well as int n. If you pass an n which is greater than the length of the string, no type system will help you figure out what to do. It's just an invalid/illegal argument, and you will either have to decide what to return for that case (maybe an empty array, or even... null), or throw some kind of exception.
That quote gets pulled out of thin air every time this argument comes up and it's pretty silly. I think more specifically, nullable by default might have been the bigger mistake not that null is inherently a mistake.
The mistake is Null being a member of all (reference) types. This is not the case in all languages that have a nil value.
In Common Lisp if you declare an argument to be a string, it is an error to pass NIL. You have to declare the type to be "(or string null)". That's exactly what the Option type provides.
Please tell me how it works, then. I guess you could easily find a counter-example.
First things first.
Optional arguments are handled by &OPTIONAL and &KEY parameters, which can be used to provide defaults values as well as a flag indicating whether the argument is provided. The most used default value is probably NIL.
NIL belongs to the SYMBOL, NULL, LIST and BOOLEAN types and their supertypes (ATOM, SEQUENCE, T). If you really want, you can include it in a custom type too.
For most uses, the type (OR NULL U) is regarded generally as an optional U. This is consistent with the definition
of generalized boolean and lists (a list is an optional cons-cell). If some corner cases, you have to use another sentinel value or use the third binding in &OPTIONAL and &KEY argument bindings. This is hardly a problem if you have a type U such as (TYPEP NIL U), but for most useful cases, (TYPEP NIL U) in fact returns NIL.
Here below I define a function FOO which accepts a string and prints it:
(defun foo (x) (print x))
The type declaration for FOO is:
(declaim (ftype (function (string) t) foo))
In Java, such a function will works with null too, because
null is an acceptable value for String. In Common Lisp, you have to write this declaration to allow NIL:
And just to test how it behaves,
let's use IGNORE-ERRORS to catch errors and return them as secondary values.
Calling the first FOO with NIL signals an error:
(ignore-errors (foo nil))
NIL
#<TYPE-ERROR expected-type: STRING datum: NIL>
;; Does not print anything else
The same test case with the second FOO shows that it accepts NIL:
(ignore-errors (foo nil))
NIL
;; prints "NIL"
Lisp being Lisp, those checks are likely to be done dynamically, but in some cases, your compiler can determine if a variable will hold NIL and warn you about a conflicting usage.
However, how type checks are enforced does not change the argument, namely that NIL is not an appropriate value for all types.
There were no errors. The function accepted nil and printed it. You must have some other type restrictions going on than what you mentioned. The output is the same whether I put the declaim statement before or after defun foo.
The behavior is undefined if a value does not match its type declaration, and implementations are free to ignore some declarations, like CLISP. So for a portable approach just add a CHECK-TYPE:
(defun foo (s)
(check-type s string)
(print s))
The main point is that (TYPEP NIL 'STRING) is NIL.
Regarding nulls, have a look at how Haskell deals with nulls. The basic idea is that any nullable value gets wrapped in a "Maybe", and you use pattern matching to handle the case of missing/empty values. This is much more explicit than null values in Java, where any object could be null at any time, and actually leads to cleaner code (i.e. to avoid lots of "Maybe" boilerplate, you can match for "Nothing" and return a default value as soon as possible to first matching "Nothing").
Regarding illegal arguments, have a look at something like Idris, which has a type system with "dependent types". The Wikipedia page has an example[0]; types can have values, so you can express a "pairAdd" function that accepts two vectors, each vector requiring the same length.
I guess "types" and "compile-time checks" are sometimes used interchangeably. I absolutely think tools (such as compilers) should be leveraged to provide as much assistance as possible; whether that comes in the form of types, or some other mechanism that resembles types.
Well, I don't know Idris in detail, so you're better off reading the wiki page I linked to, but I will try to explain further.
Idris allows you define types whose definition depends on a value - these are called "dependent types". This allows you to define a type such as "a pair of integers, where the second integer is greater than the first".
So, in your example, you could define an immutable String type that stores its length as a fixed value. Then, you define a function that accepts a String of length "len", and a Num (or Integer) with a value that must be less than "len". The type signature might look something like this (if I understand the syntax correctly):
ngram : String len val => Num (a <= len) => Vect Num String
A compiled language cannot know what the value of a variable is at compile time, unless the variable is a constant.
Therefore, there cannot possibly be a way to avoid the problem I described in my example, unless your program exists in a a closed world where there are no inputs, and the values of all variables are defined before compilation.
> A compiled language cannot know what the value of a variable is at compile time, unless the variable is a constant.
This statement is false. I recommend reading about dependent types, which enable exactly what you claim is impossible.
It's true that in every mainstream language, the type system is not powerful enough to reason about types that "depend" on values, such as the type of MxN matrices, sorted lists, prime numbers, etc. But don't limit your sense of what is possible by your experience with mainstream languages; most have rather limited type systems compared to what we know is possible in theory.
Ah, you're referring to the halting problem? You're right, you can't know every possible value. The article I linked actually mentions how Idris approaches the problem:
The word "total" invokes the totality checker[0] which will report an error if the function doesn't cover all possible cases or cannot be (automatically) proven to not enter an infinite loop.
So, it sounds like if you can't provably account for all possible inputs, the compiler will recognise this and throw an error. How? Magic! (And a solver, feeding your type constraints into the totality checker, I guess...)
(Also, bear in mind languages like Haskell use "lazy" or "non-strict" evaluation by default, where values aren't evaluated until they're actually needed; kiiiinda like Streams?)
I'm certainly not an expert on Idris, but I imagine dependent typing provides two benefits:
1) The type signature (which is separate from the implementation; kind of like an Interface in Java) can enforce constraints on your implementation. So, if you define a function with type sig of "Int a => Int b => (a + b)", your implementation must ensure that the returned value is equal to the sum of the two inputs.
2) You can look purely at the type signature to understand what a function is doing. Looking at that type signature above, I can see I want to use that function for addition, not multiplication.
Anyway, I'm absolutely not doing justice to these ideas (mostly because I'm not well acquainted with the languages). If you're curious at all, I would highly recommend taking a course on functional programming as a minimum; once I realised that names are meaningless in code, you start seeing everything as reducible to very similar type signatures, which makes you yearn for a language that lets you write your boilerplate once, in a polymorphic way (beyond generics).
> So, it sounds like if you can't provably account for all possible inputs, the compiler will recognise this and throw an error. How? Magic! (And a solver, feeding your type constraints into the totality checker, I guess...)
That would make many practical, real-world programs noncompilable. Any program which accepts data via API calls, or even reads strings from STDIN, cannot know what values are going to be passed to it before it runs. Compilers cannot predict the future.
You are incorrect. Your objection is like looking at a pen-and-paper proof that a program terminates and saying it is invalid because of the halting problem. If you want to read more about this subject, I recommend Adam Chlipala's "Certified Programming with Dependent Types": http://adam.chlipala.net/cpdt/
A one-state machine that immediately halts, will always halt, regardless of the input.
The halting problem cannot be solved in general, but some programs can be proven to halt (or not halt). That's how the first few Busy Beaver numbers were found.
For something more substantial: a correct program that implements a regular language recognizer will always halt eventually on any input, because it always makes progress and doesn't have any nonterminating loops. It consumes the whole input and stops.
The hard part is proving you wrote it correctly, but that's what coq et al are for.
No, this is not true. Writing a program to check termination proofs is not difficult. Writing a program to construct termination proofs isn't possible in the general case. If someone hands you a formal pen-and-paper proof of termination it is quite possible to check it for correctness.
Analogous statements are true for programming in a dependently typed language.
To explain in simpler terms, what you do is promote values to types first. I.e. when a user enters a string, you have to somehow prove, using formal logic, that n < l. After that, the proof is encoded in the types and you can use functions as ngram above that depend on this proof.
Probably with a function with a type signature something like this (which is in Agda, another dependently typed language, but the idea holds):
nGram : {l : Nat}{n : Fin l} -> Vec l Char -> n -> List (Vec n Char)
To break it up:
"nGram" is the name of the function
"{l : Nat}" means l is a natural number
"{n : Fin l}" means n is a finite number smaller than l
"Vec l Char" is the first argument, declared as a
vector of characters, with length l
"n" is the second argument (previously declared as
a finite number smaller than l)
"List (Vec n Char)" is the return type, a list which consists
of vectors of characters where each vector is of length n
For an explanation on how and why this works you can check out these papers:
The first is more about programming in Agda while the second goes into how dependently typed languages relate to logic. Together they provide sufficient definitions of Nat, Fin, Vec and List to declare the nGram function.
If you're unfamiliar with ML-style syntax the above definiton probably looks weird, but can be written in a pseudo-java style like this:
List<Vec<N,Char>> nGram<Nat L,Fin<L> N>(Vec<L,Char> input_string, N n){}
This makes no sense. No language can know the value of a variable at compile time, unless that variable is a constant. If the function you have defined is passed a string s and an int n, where n is greater than len(s), there is no way the compiler can know that will happen in advance. It will be a runtime error. IllegalArgumentException is also a runtime error. You have not solved anything.
Actually, joel_ms is correct; you can encode these "dynamic" properties of values in a static type system, in the same way that you can prove properties about a program on paper without actually running it. The connection between programs and proofs [1] is rather mind-blowing to learn about, and unfortunately no popular programming language supports this style of "certified" programming. But there are some interesting languages which do, such as Idris and Coq.
Did you check out the papers I linked to? Because they explain how this "makes sense".
Dependently typed languages allow for type-level functions (actually they erase the separation between type-level and value-level). This is why you can define a type like "Fin n" meaning a number smaller than n. "Fin n" is a function that takes one argument "n" and returns a type that limits its values to the set of (natural) numbers smaller than n.
Arguably, Agda is more often used as a proof assistant, rather than as a programming language (although I've had the pleasure of watching Ulf Norell implement a limited programming language interpreter in Agda, using non-total functions, which was quite nice.) But Idris is a serious effort to create a language that's both dependently typed and performs decently when used as a regular programming language.
If you want to see some real world examples of using dependently typed languages, I would suggest looking at the CompCert C Compiler[0]. It is a C Compiler that's been formally verified using the Coq proof assistant, which is a precursor to both Agda and Idris. Coq is as far as I know the first dependently typed language.
To be fair, programming in a dependently typed language usually requires more effort upfront, but the ability to encode properties that are usually considered runtime errors into the type system does seem like an interesting future for programming languages.
(edit: shout out to the excellently named sibling commenter "curryhoward", IMHO one of the most interesting results in programming language technology and type theory research!)
I've never really grokked the improvement this is supposed to represent. It's a slightly more explicit class of return codes I guess, and feels an awful lot like exceptions as well.
Go for example, half-straddles this world too.
I can half-see the benefit of being able to know if something produces an error term (annoying in Python when you're trying to be thorough with exception handling) but conversely there's so few situations I encounter where a function can't error that it's practically irrelevant - what does it matter to me that some set of functions can't throw, if every bit of IO, network and disk before them can and will at some point?
Not as such; functional languages use "pattern matching", so you are forced to match each possible outcome. This way, it explicitly brings the issue of nulls out into the open; if you have a Maybe it might be null, if you have a String, or Number, or anything else, it is never null!
This means the language allows you to define areas of your program that are null-safe, and areas where nulls are expected.
Compare to languages where you must remember to check null on every use of a null variable. I guarantee I can pick any Java codebase and find a method where arguments to that method, or a class's properties, are not checked for null on every use. How do I know that the use is safe? Usually by coding conventions; maybe immutable instances represented by values set only in a constructor, although what's to stop null being passed in?
90% of the time you don't need to check the value for null, because it won't be null and that will be reflected in the type. In langauges where all types contain null, there is no way for a function to communicate that it always returns a non-null value, which forces the caller to make redundant checks. If the value can be null, then they are at risk of NullReferenceExceptions if they fail to check.
In cases where the value could be missing, the type is change to Maybe and the caller is forced to handle the null case. They can still avoid explicit checks using various combinators (>>=, fmap, liftA2 etc.) which propagate empty Maybe values. Explicit matching on Maybe is fairly uncommon in languages which support it.
1. the large number of values that you now know definitely have a value, and you can access without fear that they don't.
2. the helper functions for Maybe that let you deal with optionality in various ways; the possibilities are extensive and useful. null doesn't give you any help, and generally any helper syntax is limited to one or two possibilities.
3. Code cannot fail due to trying to access a null pointer accidentally. Functions that definitely return a value (ignoring severe issues like OOM, etc.) is a somewhat useful property.
Here’s my real-world example for why type-level null checking is a good idea.
Let’s say your application tracks users, and each user needs to have at least one address. On top of that, they must flag one address as their default address, which is used when sending them formal communication, but usually they can pick which address they want to use. (Most people have one address which is their default).
The Java code to pick out a user’s default address is simple enough:
class User {
// ...
public Address getDefaultAddress() {
for (Address address : getAddresses()) {
if (address.isDefault()) {
return address;
}
}
throw new AssertionError("User has no default address");
}
}
We throw an AssertionError here instead of returning null because if a user has no default address, then that’s a bug in the program, and we want to fix it. And because it throws, we can safely assume that any address coming out of getDefaultAddress is not null.
Then one day we receive a change request from a client. Some users move around a lot, and have no permanent address, and they’d like it to be changed from at least one address to at least zero addresses. Basically, make it so users don’t have to have an address anymore.
How does this affect our getDefaultAddress function? It has to handle the case where users have no addresses, because if that’s the case, then they’ll have no default address, either.
The easiest way to do it is just return null instead:
public Address getDefaultAddress() {
for (Address address : getAddresses()) {
if (address.isDefault()) {
return address;
}
}
// no default address
return null;
}
With this in place can no longer assume that the Address returned from getDefaultAddress() is an actual Address object. Which means that all the code we’ve written so far -- the code that assumes that the method doesn’t return null -- will break. So we have to find every place in the code where this method is called, and make sure that it does a null check. IDEs can help here, but they can’t help with developers who hadn’t noticed the change and continue using the method in the old way, or places where the method is called with reflection... and do you really want to test every call site to make sure you’re handling it correctly?
Or we can use Optionals:
public Optional<Address> getDefaultAddress() {
for (Address address : getAddresses()) {
if (address.isDefault()) {
return Optional.of(address);
}
}
// I know I could use Java 8 streams here, just go with it :)
return Optional.empty();
}
The Optionals aren’t the most important part of this example -- it’s that the method signature changed. Our program will now no longer compile until each and every place in the code that gets a default address has been updated to specifically handle the case where there is none, because the compiler won’t accept an Optional<Address> where it expects an Address. A developer who hadn’t seen the change won’t be able to use the function in the old way by accident, as it won’t compile.
This is precisely what happened to us. Changing the method to return null resulted in lots of breakage from code that we forgot to add null checks to and code written afterwards using the old signature by accident. Changing it to return an Optional meant we were forced to update everything, otherwise it wouldn’t compile. No runtime bugs. No developer slip-ups. That’s why I’m in Team Optional.
No. When you get passed a "Foo", you know for a fact that it's non-null. When you get passed a "Maybe Foo", you know for a fact that there are intentional cases where this thing will be null. In Java, you just have one type for both purposes, so you end up checking non-nullness every time (or just blindly rely on non-nullness having been checked elsewhere before).
Maintainability is tricky to measure and tricky to achieve. My experience is it's more dependent on proper management, processes, discipline than on language traits, however, I agree that it's much easier to shoot yourself in the foot using highly expressive languages.
You make a compelling argument, but I'm not totally convinced. I think for many real world problems, writing the actual program is not the hard part, it's understanding the requirements and managing changing requirements. I'm not sure that a more expressive language is necessarily better here. Boring code that is easy to understand but twice as long is sometimes better than an elegant solution with fewer lines.
Expressive does not always mean "unnecessary clever", having less boilerplate is enormous advantage by itself. Consider that java until very recently didn't even have lambda expressions.
You might say: but IDEs help with boilerplate! True to a point. Someone still has to sift through the tons of unnecessary generated LOC crap just to get to the issue at hand.
Again, I'm not writing this to bash on java, I'm simply trying to show that search for "holy grail of programming languages" is far from fruitless, and it fills me with joy when I see more languages being developed and tried in different niches (clojure, rust, go, elixir — these are all different and awesome at the same time).
> You might say: but IDEs help with boilerplate! True to a point. Someone still has to sift through the tons of unnecessary generated LOC crap just to get to the issue at hand.
Yeah. IDEs may help with writing boilerplate, but most of the time you're not writing code - you're reading it. And every small bit of boilerplate Java adds pretty much everywhere (and C++, and C#, and other similar languages) is a bit you have to read, parse and decide if it's relevant. It obscures the meaning the code was supposed to express and make understanding the code a much more cognitively demanding task.
(IDEs could help by detecting and folding boilerplate, but if they could do that, then why not just write your code in that folded-boilerplate language directly?)
> Have 10000 experienced ruby programmers and 10000 experienced java programmers solve different tasks. I'm pretty sure that rubyists are going to solve their problems using 50% less time with at least 30% less LOC just by the virtue of having a more expressive language
And the solution is goint to be at least 10x slower than the java one.
> And the solution is going to be at least 10x slower than the java one.
Even if that was true (there are many ways to make ruby/python faster), who cares? Hardware is like 1/100 of the cost of an engineer. Use more/better hardware, and problem solved. Optimize for your most expensive resource. Sure maybe 20 years ago that was processing time, but now in days your end user can't tell the difference. I mean does 10ms really feel any different from 100ms? Not really.
(OK, there are some times when processing time DOES matter, but that's the exception, not the rule. No premature optimization please)
Do the same with 10000 experienced Common Lisp programmers, and the solution will be 70% less time, 70% less LoC, and will be 10x FASTER than the java one (since there are CL native compilers that attain the same efficiency as C or C++ compilers).
> However, the metric author uses is development time, and by that metric more expressive languages will always win, hands down.
Fair enough, but development time is a small factor in the life of an important project.
If you want to prototype and get your minimum viable product out the door, go ahead and use dynamic languages. You'll probably end up rewriting it later, but that's a tradeoff you have to weigh. It's a viable approach taken by many startups.
On the other hand, if you are working on a project that will live for years, you will be more concerned about maintainability, adaptability to changing requirements, interaction with other services, quality tooling, and runtime speed than you will about the time it takes to write the first release version.
I enjoyed reading it, I particularly liked the interview style.
I'm surprised he didn't mention, in addition to compiles times changing, that editors and environments improve dramatically. (I guess he alludes to punch cards, but still). And they can affect whether one language or another is an improvement (for instance, Java with is stricter types really benefits from an editor that can do autocomplete, in my opinion)
I do think that most technologies follow this sort of progression. I mean, if I go buy a new computer now, it is going to make me incrementally more productive. I'm 52, so there was a day when I was buying a computer to replace a dedicated word processor (i.e. a typewriter with a tiny amount of memory and and small LCD display), and it was a dramatic improvement. As was the word processor compared to a plain old typewriter. I don't expect that kind of drama when buying something to type on now. Things are slightly more dramatic with touch screen devices, but that is starting to slow down. Other things, like a dishwasher, even less so.
I hope something interesting will speed things up again, but I expect it will be like what happened with phones and tablets coming in and replacing many functions of old-school computers for so many people. That is, a new way of automating computer behavior that doesn't mostly come down to editing text files.
As an example, a technical person could train a humanoid robot arm to wash dishes by talking to it while the robot mirrors the motions of the trainer's arms. Is that programming? I'm not sure, but when we start seeing more of that sort of thing, and it increases in sophistication, I would expect more of those big jumps like moving from binary to assembly.
This is one thing that bugs me. The editors and environments haven't been improving dramatically. Emacs is 40+ years old at this point, and still as hard to learn as ever. The 1970s Unix-like OS is still at the core of most dev-environments, despite people having better ideas since then (e.g. Inferno.) Sure, there have been incremental improvements to the editors and OSs since their inception, but there were no major revolutions.
It has always seemed to me that the story of improved programming environments is driven more by improved hardware than improved software, and that companies like Microsoft/JetBrains are the only ones who've even bothered to wonder if its possible to create a better environment for programming than "arcane text editor."
It's not quite as simple as preferring one color over another - some languages, by (their own) definition, lend themselves to certain tasks more willingly than others. Rather than expecting a specific gain from a language (e.g. 20% faster development), you have to think in terms of avoiding the language that might be 20% slower for the specific problem being solved.
For example, a seasoned engineer would likely not choose to use PHP for a stateful, socket-based messaging system to connect a dozen or so users. Why? Because PHP is not designed to do that task well. It could do it - you could poll an HTTP end-point and use a cache for really fast persistence to wire it all up - but you'd likely start having to write code around the problems you'd encounter for all of the nuances to your specific implementation.
Yet another example: The main reason Go looks attractive to a certain set of developers is that it solves a problem with describing and handling concurrency that they've had with a lot of other languages. They would be dumb to say that Go is carte-blanche better than PHP (or Ruby, or Java, or even C/C++), but that doesn't mean they won't see a potentially significant improvement in using it.
I think this misses a very key point - subject matter. Some languages are well-suited to do particular things and comparatively poorly suited to other tasks.
For example, Erlang is great when you need to do lots of concurrent operations with guarantees about safety and uptime, but not really what I'd write a CSV parsing program or email client in.
C is very easy to link against on almost every platform and language, so basic libraries are often written in C.
Javascript (in the form of nodejs) is great for writing simple web servers because most of its standard library is asynchronous by default. Compare Python (with Tornado or similar) where lots of popular libraries cannot easily be used in an event loop.
I could go on forever, and you might disagree with me about a particular details, but the general point stands. If you took an expert Erlang programmer and gave her a day to write a spreadsheet application, you'd get a worse result than the same day spent by a C# programmer.
A better example would be to compare the Erlang programmer, the C programmer, and a good SQL DBA. CSV/spreadsheets is a poor example (least of all because I think you're wrong, but that's neither here nor there).
DSLs can give you tremendous amplification when you play to their strengths.
I agree. The (fictional?) person asking the questions was asking for universal productivity improvements, and I think we are long past that point. There are essentially no scenarios where programming in assembly is more productive than programming in C, but those kinds of open/shut comparisons don't work well for the long-tail environment that we live in now. People are building new languages to solve specific types of problems better.
Yeah, the effect of libraries is huge and ignored by this analysis. There's a good reason we see so many web apps written in ruby/python/node and so few in C++.
Very much agreed. Beyond that there is the tooling around a language (besides just the compiler): debuggers, IDEs, static analysis, memory analyzers, decompilers, performance analyzers, etc. Each of these help during specific parts of a programmer's job. The article focused on the language + compiler, but much of the infrastructure around a language is hugely important in how much time different parts of the job take.
also related to that is the community. not saying it is or is not, but maybe C++ is wonderful for machine learning, but so much of the community is doing it in python and R. i'm probably not likely to find help when stuck writing a histogram kernel in c++ on SO for example
People say this a lot, but I can't think of many projects that fit nicely into a single language. Most projects have certain pieces that are well suited for one language or another - so the game is usually finding which language best fits most of your project (or the trickiest part of it), which tends to be a pure guessing game.
At the end of the day you're probably best off using whichever language you know most, unless you're doing it for learning purposes, or it's one of those rare ones where it really does fit into a specific language.
It's not uncommon to mix multiple languages in a single project. Actually, I see a lot of people use c or c++ alongside Lua (by embedding the interpreter) - especially in videogames.
The other option is to split the project into multiple services. Implement the backend service in e.g. c, and then do the front-end/GUI in, say, Python. This model is seen a lot with things like media players (e.g. mpd).
If that Erlang developer knew Elixir, and threw together a simple Phoenix web app which pulled the optional Node.js/npm dependencies, you could definitely throw together a spreadsheet in a few hours, with added uptime/reliability guarantees.
That being said, your overall point certainly stands. I wouldn't write an OS in Fortran, or a game in Ruby, etc.
In other words, the future isn't 10,000 languages all equally good, but 10,000 languages suited for 10,000 problems that will reduce the comparable workload by 1/2 or more, depending on the case. That's a much more interesting conclusion to the article...
Domain-specific languages were around for decades. You have SQL for databases, bash or zsh or powershell for interactive work, R, or Julia, or Mathematica for math-heavy work, etc. General-purpose languages of course can be used in these areas, but usually only in a more cumbersome way.
It's just an alternative to using "he" (or "they", which still feels a bit awkward) to describe a hypothetical person. Either flip a coin to choose their gender, or always use "she" to counter-balance (a little) all the times writers default to "he".
I know two people who present a non-sex-specific identity to the world, and both have a preference for being called "they". Most friends and family respect that preference. Even those who find a non-binary construction of gender a challenging concept, "they" isn't proving awkward (just hard to remember sometimes).
I do think "they" will prevail, certainly more easily than than xe or thon or other invented options. So I actually use it, though there's still a part of my brain that flashes a red light each time, and I have to consciously ignore that.
I do think using "she" as the hypothetical example for roles people think of as male is still useful; it hopefully makes people question their knee-jerk response a bit.
I would add SQL to your examples of subject specific languages. Great language in a very specific application that lots of developers know and are familiar with but would never write a web server with it even though it is arguably turning complete[1]. In my experience using SQL as an example also sidesteps a few flame wars around languages that are nearer and dearer to people's hearts.
Small nitpick, especially given that all numbers are "guesstimates", but if "X is 30% more efficient than Y" it is not "10 programmers can do a work of 13", it is ((13*0.7 = 9.1 ~= 9)) programmers.
Since you've taken us to nitpick land, I'd argue that you've expressed "Y is 30% less efficient than X", and that 10 can indeed do the work of 13 if instead "X is 30% more efficient than Y".
I wish there was an IDE that would present code in your preferred format (e.g. 4 space tabs, curly braces on independent lines, etc), while still keeping the code on disk and in scm the same as the existing style (so diffs stay small, and your teammates are happy).
It shouldn't be too hard to learn a bijective mapping between your style and everyone else's style. If we're still using IDEs in twenty years, I'd expect this to have been done by then.
This 100 times over. I cannot estimate the amount of time otherwise brilliant engineers have wasted over aesthetics that really aren't a meaningful part of the code and should be rendered distinctly on a per-developer basis.
When writing Python these dates I almost always use iPython notebooks for prototyping, before throwing things into modules. When I switched off to Java and go a while back, the increased overtime for compiling was pretty noticeable.
But even more noticeable was the loads of the ability to play around with each little block of code, with data in-memory, until I was completely happy with it. It's a bit like interactive debugging, but with a great deal more freedom. As a result, I get a huge reduction in mental load when writing Python in a notebook; my attention is extremely focused on the little bit of code in working up in that moment. This is a real win for productivity, beyond what you get from the zero compile time.
(Also, Java compile times are not zero. Even waiting a minute for a large compile is enough to create a break in concentration.)
I am sometimes (i.e. I think this happens a lot) bit puzzled by looking at very intelligent people who doesn't know about the debugger or knows about it and doesn't use it anyway.
If you program Java then please feel free to use the excellent tooling that is available.
It's just habit. Code in a language with bad tooling and you too would develop habits to work around the lack of tools. It just takes time (and sometimes a bit of convincing) to adjust. And to adjust back, of course.
"Type A" coders are especially vulnerable to this kind of habituation on account of often being interested in esoteric languages, systems programming, or other "extreme" environments with tooling restrictions.
It helps also to not make the type of bugs that need to be solved with the heavy and laggy Java debugger. I've been doing quite a lot of Java over the years, especially in the last year, working on medium and large projects. I've compared my workflow to that of my cow-orkers, and I sometimes look at the way they debug things. For most cases, I can find and fix the bug with occasional logging / print statements faster than my cow-orker can pause the program on breakpoint and then click through stuff to find the right element in the right container in the right variable that's maybe causing the problem.
The trick usually is to test often and don't write something if you don't understand how it works - that also includes interacting with parts of your application that you didn't write - and to not proceed if you feel you don't understand how the code you wrote works. 99% of the time when I have a bug in my Java code I realize where the bug is before my window manager switches over from the program to the IDE. That comes simply from understanding what one wrote (+ a bit of experience).
That said, debugging multithreaded programs is a pain in the arse, and I take any help I can get there. Though I haven't seen many debuggers that would be helpful in those cases.
Interactive debugging is fun in Lisp, where you basically code interactively all the time and code/debug phases are pretty much blended together (just like compile/load/runtime is).
Debuggers help me when I'm stuck with some kind of bugs, like when I need to find out where a certain variable is set. But some tasks are easier to debug with print statements. And often I wish debuggers could go back to the previous statement and so I wouldn't need to add another breakpoint in front and run the program again.
You're just used to using poor tools. Visual Studio has reverse step since 2010, GDB since 2009.
I can't think of a single task that's easier with print statements vs a basic breakpoint debugger. And if you include fancier debugger features like reverse step, code injection and moving the instruction pointer, debuggers win by an order of magnitude.
People generally ascribe the rise of Web 2.0 to AJAX, and while that's part of it, what really made Web 2.0 possible was Firebug delivering a useable debugger for Javascript, so complex AJAX apps could actually be created.
Somehow I lost my original point in the comment above, which was that I believe debuggers are so important, it's literally the first feature I got working in my toy programming language. Being able to see how terribly I've screwed things up in CLion makes fixing my idiocy much easier.
> (Also, Java compile times are not zero. Even waiting a minute for a large compile is enough to create a break in concentration.)
I don't know how you define "large," but I work on Java projects that have several thousand classes and compile in less than a minute. Maybe you have a slow computer.
A lot of the large java projects I've worked with (at a large bank, on the branching/release team) would sometimes go on for ten minutes. That's ages as far as concentration is concerned.
Type three or four letters and hit enter, and have the IDE autocomplete the rest of your SpringBeanFactoryAdapterFactoryFactory type declaration? I think you're putting up a strawman.
Aside from the fact that with IDEs this is mostly invalid, it misses the fact that writing the code initially is typically a very small part of program's life. Operation, maintenance, and the complexity thereof ends up dwarfing the initial sunk cost.
Another post contributing towards the already high likelihood that Uncle Bob is completely oblivious of typed pure functional languages like Haskell (or Idris, etc)
I was waiting for something like "and then people invented TDD and that gave another 10x improvement" but it didn't come. A surprisingly balanced post for Uncle Bob.
My estimate is that Python & Ruby are about 4 to 5 times more productive than Java for the case of web/API development. The benefit grows with the size of the codebase as Python & Ruby are also easier to read and maintain.
The point the author raises about types is only partially true. Both in Java and Python you should be unit testing your code. If you have good test coverage and CI than it really doesn't matter that Ruby and Python don't check your types.
In Java's defense.. For certain use cases like data processing and other CPU intensive workloads it runs circles around Python and Ruby. So I guess it depends.
> If you have good test coverage and CI than it really doesn't matter that Ruby and Python don't check your types.
People say this a lot, but it's not true unless you have 100% coverage, 100% control over the data that gets input into your program, and you can think of every possible test case that covers every possible situation that can occur.
Many of the tests you have to write for programs in dynamic languages are simply not necessary for strongly typed languages.
Furthermore, compilers for static languages catch (or warn about) a lot of problems that would otherwise have to be reproduced by the kinds of test cases nobody is likely to think of -- this results in bugs making it to production when you use dynamic languages.
> People say this a lot, but it's not true unless you have 100% coverage, 100% control over the data that gets input into your program, and you can think of every possible test case that covers every possible situation that can occur.
This is different from static typing, how?
> Many of the tests you have to write for programs in dynamic languages are simply not necessary for strongly typed languages.
Actually, dynamic typing users write very few additional tests. Users of static typing seem to imagine a lot of additional tests being needed, but dynamic typing users don't write them. They are usually for scenarios that are important enough to test.
> Furthermore, compilers for static languages catch (or warn about) a lot of problems that would otherwise have to be reproduced by the kinds of test cases nobody is likely to think of -- this results in bugs making it to production when you use dynamic languages.
Not my experience. Almost all bugs caught by static compilers are ones that a single execution of the relevant code would also catch.
Sure, instead of answering my questions or points try to dismiss me as not having experience.
As a matter of fact, I have developed applications in Python, C++, Java, Javascript, GWT, Javascript, and Coffeescript, for myself, at small companies, and at Google. I don't think you can simply dismiss me as having limited experience unless you've got more extensive experience shipping apps written in both dynamic and static languages.
Now, I'm not saying that dynamic typing is better: there are tradeoffs to either approach. But I am saying that your original post gives an overly simplistic appraisal. The pros and cons of static and dynamic typing and much more complicated then that.
Here's a benchmark: in 1970, I had a full-time job maintaining a 4,000-line assembler program on punched cards. Today I manage a 400,000-line system in C++. That's 100:1.
That's an interesting point also - scale. I don't think programming a simple "hello world" in C or C++ would be 100x faster than in assembly (assuming you were equally familiar with both languages), but I think it would be nearly impossible, or at least incredibly expensive to maintain 400 kloc of assembly. I honestly shudder at the thought.
You would use a macro assembler, and if you have good programmers, they would effectively develop a DSL for writing your program in it.
That would postpone the point at which that assembly program reaches 400kloc. In the end, it would probably grow as fast as a C program of the same functionality.
Of course, they also would have to develop their own tooling around that DSL. That, I think is the problem lies with assembly. Higher level languages imply that more stuff gets shared between projects (even across companies; everybody will use the same C language and standard library), and that means time spent to develop tooling around and libraries on top of the shared stuff is useful for more people. Hence, tooling and libraries will generally be of higher quality. that home-grown GUI library built on top of a home-grown set of assembler macros using a home-grown ABI may be better than using, say, QT on top of X11, but it isn't that likely.
On the other hand, the smaller the system, the more important memory usage, and if the system is small enough, that can and will swing the advantage to the home-grown system. That happens less and less, though.
It's probably more like 500:1 if we're measuring by functionality rather than textual size, no? I imagine the C++ program needs more memory to run in, though. (And that a substantial part of your workload in 1970 was due to memory size limitations.)
On your own? If yes, then that's pretty impressive. My previous team maintained 350,000-line Perl application with at least 5-6 programmers. If you're handling that C++ system alone, I'd assume it's pretty self-contained, since the complexity in large systems regularly comes from all the other systems that you interface with (at least for the aforementioned Perl app).
That's only true for a small subset of applications. Beyond the code that the programmer is actually writing themselves and direct language features, Python (and most modern languages with good library support) makes a standard library (and many third party libraries). The library of external code that developers don't have to think about wasn't really covered in the article but is one of the things which multiply that 25 significantly higher.
Reading between the lines, the author actually feels this: "Stop making a new language every month. The ones we have are good enough, and it fractures inter-developer cooperation when we don't know the same languages, rebuild all the same libraries over and over for different languages."
Which is one valid viewpoint. But he couches it in the socratic method against a strawman (presumably himself) which ultimately leaves the reader feeling dirty.
The problem of handling an environment with richly diverse hardware and languages is itself an interesting problem, and probably much more analogous and appropriate to the problems of meaning human society.
So if someone were saying to me "stop developing new languages and environments; it makes things hard", it'd be like saying "stop developing new culture or human relationships, because life is easier when we're all bland and homogenous." I find it a little bit offensive and at least a failure to recognize the important problems (due to obsession over the easy ones.)
Well, that's one analogy. How about this analogy, what would you say to me if I said "Hey I invented a new spoken language, it's more efficient than English?" And what if I did this in a climate where dozens of people independently were already doing this too?
So, to simplify, I think the reason a lot of developers roll their eyes at another new language is because the problems the languages are solving are less important than the fracturing they make in the engineering community.
If you think a language is missing something, why not contribute to the language with an RFC. C++, Java, and PHP have all been advancing significantly over the years. Or why don't you get together with 100 other language-makers and come up with a unified solution?
I would say "It's wonderful that your human brain is capable of learning and designing such a language."
The problem is still in understanding what the words mean. It's easier if you have standard languages that everyone speaks, but if you want to have computers do amazing things, they'll have to be able to handle that problem, regardless of how many languages exist.
And it would be wonderful if computers could learn to understand languages regardless of who designed them and why. That problem isn't going to be solved by running away from it.
Based on the title of the post, the ending, and the context of other things he writes and talks about, I think the message is this: Let's stop the language bikeshedding and get on with more important things, like disciplines that will raise our professionalism and the quality of our software.
The article places annoying attention to quantifying "workload estimates", whatever that even means. Because the written interview is so verbose, we know that the author just brushes away the interviewee's remark that 'other important factors effect that workload', and then proceeds to cryptically dance around those factors with very opinionated questions.
In my opinion it's not the language, it's the libraries. Lets take Java vs Fortran and look at the language basics. They pretty much have the same basics: assignments, control structures, function calls. What makes Java rock isn't the language it's all the library code that comes with it.
People are always saying "Look at me I can write a web server in six lines of code." No, you can call a web server library that someone wrote for you in six lines of code.
Aftbit (some place in this thread) has a good point, some languages are better with some subject matters. I'd like to also posit that some languages have better libraries written for a subject matter which makes it appear they are better for that subject. We've seen lots of people port libraries to a different language to help them.
While I do agree that some languages give you a boost up because the compiler is doing some heavy lifting in the background, it's the huge collections of libraries that we can call that lets us stand on the shoulders of giants.
True; it's a crying shame, though. It's 2016: why are libraries still limited to the language-runtime they were written for? Why must there be more than one library ecosystem? Why can't I import Javascript libraries from Ruby, Python libraries from Java, Erlang libraries from Haskell, Go libraries from Rust? Why can't they all just be "libraries", fullstop?
(Right now, we have to explicitly embed one runtime into another, creating programmer's turducken, if we want anything close. This, though, is an artifact of the way we think about efficiency as requiring address-space cohabitation. And while it's easy to set up a runtime-heterogenous melange using IPC, the default for that is inefficient serialized streams on sockets. Where's my zeromq-like zero-copy message-passing IPC as a batteries-included part of every runtime? Where's my binary wire-type-encoding standard aimed at producing "toll-free-bridged" native types in multiple runtimes? Where's my "managed" OS with malloc-time kernel-side type-tagged memory-buffers[1] that all runtimes for that OS support loading? Where are my multi-runtime application servers with jail/lxc-like application domains to isolate mutually-untrustworthy clients?)
[1] Speaking of, whatever happened to capability-based operating systems? Hardware support for capabilities would basically let us get rid of the "process" abstraction altogether, and just have a big OS-wide heap with various units of concurrent execution holding capabilities on various memory-objects.
211 comments
[ 3.0 ms ] story [ 244 ms ] threadNo, we are not at the pinnacle, because programmer speed (as he very well knows, because he keeps on putting those objections aside) is not the only thing that matters.
Then, past a certain point, most of the new concepts started coming in via frameworks and libraries (although of course things like functional programming and interesting typing approaches are still language-driven tools).
Thus, these days, I would look at libraries and frameworks as sources of new productivity unlocks. E.g. in the Web world, jQuery saved millions of work-hours and qualitatively unlocked some new things. Then Angular (and eventually React) started realizing huge savings from declarative UI definitions. My preference for one next innovation in this area is a higher-level framework for user input modeling (a huge source of bugs these days).
But Java is "uncool". That’s the big issue.
There are tricks using annotations and strings, and Java 8's lambda expressions do help. But it's still awkward to define a React-like mini-language within Java.
Add the fact that you can do blocks, and label blocks, and you can do practically s-expressions.
I was looking for somewhere in these threads to post my comments, and I think this is the right place: yes, you're exactly right, it really is about higher-level reasoning.
Assembly enabled one to think about instructions and operations instead of hex codes. Structured languages enabled one to think about programs, rather than sequences of mnemonics. Functional programming enabled one to think about functional transformations of data; object orientation enabled one to think about collections of functionality grouped with data; both structured the structure, in different ways. With each improvement, we were able to reason at a higher level than before.
But we're not at an optimal level yet: we still do not have mainstream ways to manipulate the structure of our structure (… of our structure of our structure, ad infinitum). We still don't have a mainstream way of dealing with our programs as data.
It may not be mainstream, but we do have a way: it's older than every other programming language other than Fortran: it's symbolic expressions, which enable one to reason about and manipulate one's code as data (and data as code).
The article we're both replying to is a prime example of the Blub Paradox: Uncle Bob can see how Java is better than C++, C, assembler or hex codes, but can't see how Python — and, yes, Lisp — are better than Java.
Let's say I work exactly 40 hours and week and 50 weeks a year. Then a 5% increase in efficiency means that I can do 2 extra weeks of work per year. That's not a lot, but on a team of 26 programmers that's essentially the equivalent of hiring a new employee (without the downsides of a bigger team and the cost to bring someone up to speed).
Two weeks per year. Sheesh.
[1] http://martinfowler.com/bliki/CannotMeasureProductivity.html
Have 10000 experienced ruby programmers and 10000 experienced java programmers solve different tasks. I'm pretty sure that rubyists are going to solve their problems using 50% less time with at least 30% less LOC just by the virtue of having a more expressive language. Now I'm not trying to argue that ruby is strictly better than java, I know both languages' deficiencies. However, the metric author uses is development time, and by that metric more expressive languages will always win, hands down.
The point of the post is to show diminishing returns are occurring with ever evolving languages. The argument one should use X language because you can develop in it faster is increasingly not useful to discuss. You are stuck in the weeds arguing X will always win, when the point is there's nothing to win.
How would that be possible without search for the holy language grail?
C is only "absurdly simple" as long as you don't touch undefined behavior, which is nearly impossible in non-trivial programs.
I bet the 10000 Ruby guys are gonna create a big, unmaintainable, broken mess. Even Java's primitive type system would be a big help when it's about software development at scale.
Yes yes, TDD :)
I think Rust and Swift and Elm strike the right balance between developer productivity and a strict type system that actually improves the quality of production software.
Those are not the kinds of problems a type system is intended to solve. Those are problems of data, not of types.
The amount of boilerplate in a Java program is pretty low if you aren't trying to abuse or work around the type system. It certainly is not 10x what you end up with in Ruby or Python. Java 8 has made significant improvements, but it wasn't that bad before in many cases.
Instead of trying to work around Java's type safety, you should learn to use it properly. Good programmers think about types whether or not they are forced to by the language, so the restrictions imposed by Java should not be burdensome -- in general, you should already be mentally applying some of those restrictions to your code so you can avoid passing the wrong types around.
Java's boilerplate comes from lacking type inference and from having a type system that is too weak, not one that is too strong: I'd argue that people's love for dynamic languages without type systems is cause by how people's idea of what types are, and what they do for you, come from Java and old C++, as opposed to something more powerful.
Many people misunderstand the option type in Java. It was created to make stream processing easier. It was not intended for general use, as in other languages.
Regarding illegal arguments, they are unavoidable in any type system. Suppose you defined a function that splits a string into an array of n-grams, to which you pass the string as well as int n. If you pass an n which is greater than the length of the string, no type system will help you figure out what to do. It's just an invalid/illegal argument, and you will either have to decide what to return for that case (maybe an empty array, or even... null), or throw some kind of exception.
https://www.infoq.com/presentations/Null-References-The-Bill...
In Common Lisp if you declare an argument to be a string, it is an error to pass NIL. You have to declare the type to be "(or string null)". That's exactly what the Option type provides.
Please tell me how it works, then. I guess you could easily find a counter-example.
First things first. Optional arguments are handled by &OPTIONAL and &KEY parameters, which can be used to provide defaults values as well as a flag indicating whether the argument is provided. The most used default value is probably NIL. NIL belongs to the SYMBOL, NULL, LIST and BOOLEAN types and their supertypes (ATOM, SEQUENCE, T). If you really want, you can include it in a custom type too. For most uses, the type (OR NULL U) is regarded generally as an optional U. This is consistent with the definition of generalized boolean and lists (a list is an optional cons-cell). If some corner cases, you have to use another sentinel value or use the third binding in &OPTIONAL and &KEY argument bindings. This is hardly a problem if you have a type U such as (TYPEP NIL U), but for most useful cases, (TYPEP NIL U) in fact returns NIL.
Here below I define a function FOO which accepts a string and prints it:
The type declaration for FOO is: In Java, such a function will works with null too, because null is an acceptable value for String. In Common Lisp, you have to write this declaration to allow NIL: And just to test how it behaves, let's use IGNORE-ERRORS to catch errors and return them as secondary values. Calling the first FOO with NIL signals an error: The same test case with the second FOO shows that it accepts NIL: Lisp being Lisp, those checks are likely to be done dynamically, but in some cases, your compiler can determine if a variable will hold NIL and warn you about a conflicting usage. However, how type checks are enforced does not change the argument, namely that NIL is not an appropriate value for all types.Break 1 [2]> (declaim (ftype (function (string) t) foo))
NIL
Break 1 [2]> (defun foo (x) (print x))
FOO
Break 1 [2]> (foo "hello")
"hello"
"hello"
Break 1 [2]> (foo nil)
NIL
NIL
Break 1 [2]>
There were no errors. The function accepted nil and printed it. You must have some other type restrictions going on than what you mentioned. The output is the same whether I put the declaim statement before or after defun foo.
Regarding illegal arguments, have a look at something like Idris, which has a type system with "dependent types". The Wikipedia page has an example[0]; types can have values, so you can express a "pairAdd" function that accepts two vectors, each vector requiring the same length.
I guess "types" and "compile-time checks" are sometimes used interchangeably. I absolutely think tools (such as compilers) should be leveraged to provide as much assistance as possible; whether that comes in the form of types, or some other mechanism that resembles types.
[0] https://en.wikipedia.org/wiki/Idris_(programming_language)#D...
Idris allows you define types whose definition depends on a value - these are called "dependent types". This allows you to define a type such as "a pair of integers, where the second integer is greater than the first".
So, in your example, you could define an immutable String type that stores its length as a fixed value. Then, you define a function that accepts a String of length "len", and a Num (or Integer) with a value that must be less than "len". The type signature might look something like this (if I understand the syntax correctly):
Therefore, there cannot possibly be a way to avoid the problem I described in my example, unless your program exists in a a closed world where there are no inputs, and the values of all variables are defined before compilation.
This statement is false. I recommend reading about dependent types, which enable exactly what you claim is impossible.
It's true that in every mainstream language, the type system is not powerful enough to reason about types that "depend" on values, such as the type of MxN matrices, sorted lists, prime numbers, etc. But don't limit your sense of what is possible by your experience with mainstream languages; most have rather limited type systems compared to what we know is possible in theory.
(Also, bear in mind languages like Haskell use "lazy" or "non-strict" evaluation by default, where values aren't evaluated until they're actually needed; kiiiinda like Streams?)
I'm certainly not an expert on Idris, but I imagine dependent typing provides two benefits:
1) The type signature (which is separate from the implementation; kind of like an Interface in Java) can enforce constraints on your implementation. So, if you define a function with type sig of "Int a => Int b => (a + b)", your implementation must ensure that the returned value is equal to the sum of the two inputs.
2) You can look purely at the type signature to understand what a function is doing. Looking at that type signature above, I can see I want to use that function for addition, not multiplication.
Anyway, I'm absolutely not doing justice to these ideas (mostly because I'm not well acquainted with the languages). If you're curious at all, I would highly recommend taking a course on functional programming as a minimum; once I realised that names are meaningless in code, you start seeing everything as reducible to very similar type signatures, which makes you yearn for a language that lets you write your boilerplate once, in a polymorphic way (beyond generics).
[0] https://en.wikipedia.org/wiki/Termination_analysis
That would make many practical, real-world programs noncompilable. Any program which accepts data via API calls, or even reads strings from STDIN, cannot know what values are going to be passed to it before it runs. Compilers cannot predict the future.
That is a true statement for any program that accepts input, strictly speaking. The halting problem is unsolvable.
The halting problem cannot be solved in general, but some programs can be proven to halt (or not halt). That's how the first few Busy Beaver numbers were found.
For something more substantial: a correct program that implements a regular language recognizer will always halt eventually on any input, because it always makes progress and doesn't have any nonterminating loops. It consumes the whole input and stops.
The hard part is proving you wrote it correctly, but that's what coq et al are for.
Analogous statements are true for programming in a dependently typed language.
http://www.cse.chalmers.se/~ulfn/papers/afp08/tutorial.pdf
http://www.cse.chalmers.se/~peterd/papers/DependentTypesAtWo...
The first is more about programming in Agda while the second goes into how dependently typed languages relate to logic. Together they provide sufficient definitions of Nat, Fin, Vec and List to declare the nGram function.
If you're unfamiliar with ML-style syntax the above definiton probably looks weird, but can be written in a pseudo-java style like this:
[1] https://en.wikipedia.org/wiki/Curry%E2%80%93Howard_correspon...
Dependently typed languages allow for type-level functions (actually they erase the separation between type-level and value-level). This is why you can define a type like "Fin n" meaning a number smaller than n. "Fin n" is a function that takes one argument "n" and returns a type that limits its values to the set of (natural) numbers smaller than n.
Arguably, Agda is more often used as a proof assistant, rather than as a programming language (although I've had the pleasure of watching Ulf Norell implement a limited programming language interpreter in Agda, using non-total functions, which was quite nice.) But Idris is a serious effort to create a language that's both dependently typed and performs decently when used as a regular programming language.
If you want to see some real world examples of using dependently typed languages, I would suggest looking at the CompCert C Compiler[0]. It is a C Compiler that's been formally verified using the Coq proof assistant, which is a precursor to both Agda and Idris. Coq is as far as I know the first dependently typed language.
To be fair, programming in a dependently typed language usually requires more effort upfront, but the ability to encode properties that are usually considered runtime errors into the type system does seem like an interesting future for programming languages.
[0] http://compcert.inria.fr/compcert-C.html
(edit: shout out to the excellently named sibling commenter "curryhoward", IMHO one of the most interesting results in programming language technology and type theory research!)
Go for example, half-straddles this world too.
I can half-see the benefit of being able to know if something produces an error term (annoying in Python when you're trying to be thorough with exception handling) but conversely there's so few situations I encounter where a function can't error that it's practically irrelevant - what does it matter to me that some set of functions can't throw, if every bit of IO, network and disk before them can and will at some point?
I hope you can see how this is pretty much the same as checking if a variable is null. It takes just as much work, and has the same outcome.
This means the language allows you to define areas of your program that are null-safe, and areas where nulls are expected.
Compare to languages where you must remember to check null on every use of a null variable. I guarantee I can pick any Java codebase and find a method where arguments to that method, or a class's properties, are not checked for null on every use. How do I know that the use is safe? Usually by coding conventions; maybe immutable instances represented by values set only in a constructor, although what's to stop null being passed in?
In cases where the value could be missing, the type is change to Maybe and the caller is forced to handle the null case. They can still avoid explicit checks using various combinators (>>=, fmap, liftA2 etc.) which propagate empty Maybe values. Explicit matching on Maybe is fairly uncommon in languages which support it.
1. the large number of values that you now know definitely have a value, and you can access without fear that they don't.
2. the helper functions for Maybe that let you deal with optionality in various ways; the possibilities are extensive and useful. null doesn't give you any help, and generally any helper syntax is limited to one or two possibilities.
3. Code cannot fail due to trying to access a null pointer accidentally. Functions that definitely return a value (ignoring severe issues like OOM, etc.) is a somewhat useful property.
Let’s say your application tracks users, and each user needs to have at least one address. On top of that, they must flag one address as their default address, which is used when sending them formal communication, but usually they can pick which address they want to use. (Most people have one address which is their default).
The Java code to pick out a user’s default address is simple enough:
We throw an AssertionError here instead of returning null because if a user has no default address, then that’s a bug in the program, and we want to fix it. And because it throws, we can safely assume that any address coming out of getDefaultAddress is not null.Then one day we receive a change request from a client. Some users move around a lot, and have no permanent address, and they’d like it to be changed from at least one address to at least zero addresses. Basically, make it so users don’t have to have an address anymore.
How does this affect our getDefaultAddress function? It has to handle the case where users have no addresses, because if that’s the case, then they’ll have no default address, either.
The easiest way to do it is just return null instead:
With this in place can no longer assume that the Address returned from getDefaultAddress() is an actual Address object. Which means that all the code we’ve written so far -- the code that assumes that the method doesn’t return null -- will break. So we have to find every place in the code where this method is called, and make sure that it does a null check. IDEs can help here, but they can’t help with developers who hadn’t noticed the change and continue using the method in the old way, or places where the method is called with reflection... and do you really want to test every call site to make sure you’re handling it correctly?Or we can use Optionals:
The Optionals aren’t the most important part of this example -- it’s that the method signature changed. Our program will now no longer compile until each and every place in the code that gets a default address has been updated to specifically handle the case where there is none, because the compiler won’t accept an Optional<Address> where it expects an Address. A developer who hadn’t seen the change won’t be able to use the function in the old way by accident, as it won’t compile.This is precisely what happened to us. Changing the method to return null resulted in lots of breakage from code that we forgot to add null checks to and code written afterwards using the old signature by accident. Changing it to return an Optional meant we were forced to update everything, otherwise it wouldn’t compile. No runtime bugs. No developer slip-ups. That’s why I’m in Team Optional.
You might say: but IDEs help with boilerplate! True to a point. Someone still has to sift through the tons of unnecessary generated LOC crap just to get to the issue at hand.
Again, I'm not writing this to bash on java, I'm simply trying to show that search for "holy grail of programming languages" is far from fruitless, and it fills me with joy when I see more languages being developed and tried in different niches (clojure, rust, go, elixir — these are all different and awesome at the same time).
Yeah. IDEs may help with writing boilerplate, but most of the time you're not writing code - you're reading it. And every small bit of boilerplate Java adds pretty much everywhere (and C++, and C#, and other similar languages) is a bit you have to read, parse and decide if it's relevant. It obscures the meaning the code was supposed to express and make understanding the code a much more cognitively demanding task.
(IDEs could help by detecting and folding boilerplate, but if they could do that, then why not just write your code in that folded-boilerplate language directly?)
And the solution is goint to be at least 10x slower than the java one.
Even if that was true (there are many ways to make ruby/python faster), who cares? Hardware is like 1/100 of the cost of an engineer. Use more/better hardware, and problem solved. Optimize for your most expensive resource. Sure maybe 20 years ago that was processing time, but now in days your end user can't tell the difference. I mean does 10ms really feel any different from 100ms? Not really.
(OK, there are some times when processing time DOES matter, but that's the exception, not the rule. No premature optimization please)
Fair enough, but development time is a small factor in the life of an important project.
If you want to prototype and get your minimum viable product out the door, go ahead and use dynamic languages. You'll probably end up rewriting it later, but that's a tradeoff you have to weigh. It's a viable approach taken by many startups.
On the other hand, if you are working on a project that will live for years, you will be more concerned about maintainability, adaptability to changing requirements, interaction with other services, quality tooling, and runtime speed than you will about the time it takes to write the first release version.
I work with 200+ other developers using JavaScript (90% codebase approximately) on daily base developing a SaaS product...
I'm surprised he didn't mention, in addition to compiles times changing, that editors and environments improve dramatically. (I guess he alludes to punch cards, but still). And they can affect whether one language or another is an improvement (for instance, Java with is stricter types really benefits from an editor that can do autocomplete, in my opinion)
I do think that most technologies follow this sort of progression. I mean, if I go buy a new computer now, it is going to make me incrementally more productive. I'm 52, so there was a day when I was buying a computer to replace a dedicated word processor (i.e. a typewriter with a tiny amount of memory and and small LCD display), and it was a dramatic improvement. As was the word processor compared to a plain old typewriter. I don't expect that kind of drama when buying something to type on now. Things are slightly more dramatic with touch screen devices, but that is starting to slow down. Other things, like a dishwasher, even less so.
I hope something interesting will speed things up again, but I expect it will be like what happened with phones and tablets coming in and replacing many functions of old-school computers for so many people. That is, a new way of automating computer behavior that doesn't mostly come down to editing text files.
As an example, a technical person could train a humanoid robot arm to wash dishes by talking to it while the robot mirrors the motions of the trainer's arms. Is that programming? I'm not sure, but when we start seeing more of that sort of thing, and it increases in sophistication, I would expect more of those big jumps like moving from binary to assembly.
It has always seemed to me that the story of improved programming environments is driven more by improved hardware than improved software, and that companies like Microsoft/JetBrains are the only ones who've even bothered to wonder if its possible to create a better environment for programming than "arcane text editor."
For example, a seasoned engineer would likely not choose to use PHP for a stateful, socket-based messaging system to connect a dozen or so users. Why? Because PHP is not designed to do that task well. It could do it - you could poll an HTTP end-point and use a cache for really fast persistence to wire it all up - but you'd likely start having to write code around the problems you'd encounter for all of the nuances to your specific implementation.
Yet another example: The main reason Go looks attractive to a certain set of developers is that it solves a problem with describing and handling concurrency that they've had with a lot of other languages. They would be dumb to say that Go is carte-blanche better than PHP (or Ruby, or Java, or even C/C++), but that doesn't mean they won't see a potentially significant improvement in using it.
For example, Erlang is great when you need to do lots of concurrent operations with guarantees about safety and uptime, but not really what I'd write a CSV parsing program or email client in.
C is very easy to link against on almost every platform and language, so basic libraries are often written in C.
Javascript (in the form of nodejs) is great for writing simple web servers because most of its standard library is asynchronous by default. Compare Python (with Tornado or similar) where lots of popular libraries cannot easily be used in an event loop.
I could go on forever, and you might disagree with me about a particular details, but the general point stands. If you took an expert Erlang programmer and gave her a day to write a spreadsheet application, you'd get a worse result than the same day spent by a C# programmer.
DSLs can give you tremendous amplification when you play to their strengths.
If you look at feedback times, PHP is the new Smalltalk. (Yuck.)
At the end of the day you're probably best off using whichever language you know most, unless you're doing it for learning purposes, or it's one of those rare ones where it really does fit into a specific language.
The other option is to split the project into multiple services. Implement the backend service in e.g. c, and then do the front-end/GUI in, say, Python. This model is seen a lot with things like media players (e.g. mpd).
That being said, your overall point certainly stands. I wouldn't write an OS in Fortran, or a game in Ruby, etc.
Edit: I always use "he/she" or "her/him"
I do think using "she" as the hypothetical example for roles people think of as male is still useful; it hopefully makes people question their knee-jerk response a bit.
Using the names and pronouns with someone that they prefer is the polite thing to do, even if you don't "get" it. :)
[1] http://stackoverflow.com/questions/900055/is-sql-or-even-tsq...
To me, the reference standard is assigned unity.
In the first case, X is 1.00 and Y is 0.70.
In the second case, Y is 1.00 and X is 1.30.
"Alexa, write me an API for this $5 wifi enabled light on my desk that I soldered together yesterday."
It was then that Alexa began to plot our demise.
It shouldn't be too hard to learn a bijective mapping between your style and everyone else's style. If we're still using IDEs in twenty years, I'd expect this to have been done by then.
But even more noticeable was the loads of the ability to play around with each little block of code, with data in-memory, until I was completely happy with it. It's a bit like interactive debugging, but with a great deal more freedom. As a result, I get a huge reduction in mental load when writing Python in a notebook; my attention is extremely focused on the little bit of code in working up in that moment. This is a real win for productivity, beyond what you get from the zero compile time.
(Also, Java compile times are not zero. Even waiting a minute for a large compile is enough to create a break in concentration.)
If you program Java then please feel free to use the excellent tooling that is available.
"Type A" coders are especially vulnerable to this kind of habituation on account of often being interested in esoteric languages, systems programming, or other "extreme" environments with tooling restrictions.
The trick usually is to test often and don't write something if you don't understand how it works - that also includes interacting with parts of your application that you didn't write - and to not proceed if you feel you don't understand how the code you wrote works. 99% of the time when I have a bug in my Java code I realize where the bug is before my window manager switches over from the program to the IDE. That comes simply from understanding what one wrote (+ a bit of experience).
That said, debugging multithreaded programs is a pain in the arse, and I take any help I can get there. Though I haven't seen many debuggers that would be helpful in those cases.
Interactive debugging is fun in Lisp, where you basically code interactively all the time and code/debug phases are pretty much blended together (just like compile/load/runtime is).
I can't think of a single task that's easier with print statements vs a basic breakpoint debugger. And if you include fancier debugger features like reverse step, code injection and moving the instruction pointer, debuggers win by an order of magnitude.
People generally ascribe the rise of Web 2.0 to AJAX, and while that's part of it, what really made Web 2.0 possible was Firebug delivering a useable debugger for Javascript, so complex AJAX apps could actually be created.
I don't know how you define "large," but I work on Java projects that have several thousand classes and compile in less than a minute. Maybe you have a slow computer.
The point the author raises about types is only partially true. Both in Java and Python you should be unit testing your code. If you have good test coverage and CI than it really doesn't matter that Ruby and Python don't check your types.
So blue is my color :)
People say this a lot, but it's not true unless you have 100% coverage, 100% control over the data that gets input into your program, and you can think of every possible test case that covers every possible situation that can occur.
Many of the tests you have to write for programs in dynamic languages are simply not necessary for strongly typed languages.
Furthermore, compilers for static languages catch (or warn about) a lot of problems that would otherwise have to be reproduced by the kinds of test cases nobody is likely to think of -- this results in bugs making it to production when you use dynamic languages.
This is different from static typing, how?
> Many of the tests you have to write for programs in dynamic languages are simply not necessary for strongly typed languages.
Actually, dynamic typing users write very few additional tests. Users of static typing seem to imagine a lot of additional tests being needed, but dynamic typing users don't write them. They are usually for scenarios that are important enough to test.
> Furthermore, compilers for static languages catch (or warn about) a lot of problems that would otherwise have to be reproduced by the kinds of test cases nobody is likely to think of -- this results in bugs making it to production when you use dynamic languages.
Not my experience. Almost all bugs caught by static compilers are ones that a single execution of the relevant code would also catch.
As a matter of fact, I have developed applications in Python, C++, Java, Javascript, GWT, Javascript, and Coffeescript, for myself, at small companies, and at Google. I don't think you can simply dismiss me as having limited experience unless you've got more extensive experience shipping apps written in both dynamic and static languages.
Now, I'm not saying that dynamic typing is better: there are tradeoffs to either approach. But I am saying that your original post gives an overly simplistic appraisal. The pros and cons of static and dynamic typing and much more complicated then that.
That would postpone the point at which that assembly program reaches 400kloc. In the end, it would probably grow as fast as a C program of the same functionality.
Of course, they also would have to develop their own tooling around that DSL. That, I think is the problem lies with assembly. Higher level languages imply that more stuff gets shared between projects (even across companies; everybody will use the same C language and standard library), and that means time spent to develop tooling around and libraries on top of the shared stuff is useful for more people. Hence, tooling and libraries will generally be of higher quality. that home-grown GUI library built on top of a home-grown set of assembler macros using a home-grown ABI may be better than using, say, QT on top of X11, but it isn't that likely.
On the other hand, the smaller the system, the more important memory usage, and if the system is small enough, that can and will swing the advantage to the home-grown system. That happens less and less, though.
On your own? If yes, then that's pretty impressive. My previous team maintained 350,000-line Perl application with at least 5-6 programmers. If you're handling that C++ system alone, I'd assume it's pretty self-contained, since the complexity in large systems regularly comes from all the other systems that you interface with (at least for the aforementioned Perl app).
Which is one valid viewpoint. But he couches it in the socratic method against a strawman (presumably himself) which ultimately leaves the reader feeling dirty.
So if someone were saying to me "stop developing new languages and environments; it makes things hard", it'd be like saying "stop developing new culture or human relationships, because life is easier when we're all bland and homogenous." I find it a little bit offensive and at least a failure to recognize the important problems (due to obsession over the easy ones.)
So, to simplify, I think the reason a lot of developers roll their eyes at another new language is because the problems the languages are solving are less important than the fracturing they make in the engineering community.
If you think a language is missing something, why not contribute to the language with an RFC. C++, Java, and PHP have all been advancing significantly over the years. Or why don't you get together with 100 other language-makers and come up with a unified solution?
The problem is still in understanding what the words mean. It's easier if you have standard languages that everyone speaks, but if you want to have computers do amazing things, they'll have to be able to handle that problem, regardless of how many languages exist.
And it would be wonderful if computers could learn to understand languages regardless of who designed them and why. That problem isn't going to be solved by running away from it.
> I need a number
> The number?
The article places annoying attention to quantifying "workload estimates", whatever that even means. Because the written interview is so verbose, we know that the author just brushes away the interviewee's remark that 'other important factors effect that workload', and then proceeds to cryptically dance around those factors with very opinionated questions.
People are always saying "Look at me I can write a web server in six lines of code." No, you can call a web server library that someone wrote for you in six lines of code.
Aftbit (some place in this thread) has a good point, some languages are better with some subject matters. I'd like to also posit that some languages have better libraries written for a subject matter which makes it appear they are better for that subject. We've seen lots of people port libraries to a different language to help them.
While I do agree that some languages give you a boost up because the compiler is doing some heavy lifting in the background, it's the huge collections of libraries that we can call that lets us stand on the shoulders of giants.
(Right now, we have to explicitly embed one runtime into another, creating programmer's turducken, if we want anything close. This, though, is an artifact of the way we think about efficiency as requiring address-space cohabitation. And while it's easy to set up a runtime-heterogenous melange using IPC, the default for that is inefficient serialized streams on sockets. Where's my zeromq-like zero-copy message-passing IPC as a batteries-included part of every runtime? Where's my binary wire-type-encoding standard aimed at producing "toll-free-bridged" native types in multiple runtimes? Where's my "managed" OS with malloc-time kernel-side type-tagged memory-buffers[1] that all runtimes for that OS support loading? Where are my multi-runtime application servers with jail/lxc-like application domains to isolate mutually-untrustworthy clients?)
[1] Speaking of, whatever happened to capability-based operating systems? Hardware support for capabilities would basically let us get rid of the "process" abstraction altogether, and just have a big OS-wide heap with various units of concurrent execution holding capabilities on various memory-objects.