Comparing the Same Project in Rust, Haskell, C++, Python, Scala and OCaml
thume.ca
thume.ca
Having the whole project - design, architecture, all implementation details - in one head is not a trivial advantage. Even ignoring communication overhead, there might be subtle duplication of code simply because different people choose to do similar things in slightly different ways.
While the ratio of Rust to Python code kind of matches my expectations, I wonder how much of it might be due to the difference in team structure vs the difference in chosen language.
There were two Rust teams. One had 3x the code and passed less tests than the other. This is our only reference for how much noise is owed to team rather than language; no other language was used by more than one team.
Python did best (least amount of code, yet also the most features) less because of Python, but more because the best programmer was using it.
Would the other students have been able to take advantage of the duck typing and metaprogramming in the same way, or ended up following different designs? I'd have my doubts about the second Rust team.
Although that Python allowed these is a feature, but I think we're really just looking at noise.
(To be clear, the optional portions in which I implement SSA, various optimizations, a Hack-style allocator and some ad-hoc codegen is much less readable.)
It sounds like you and your classmates are top notch and will go on to some pretty freaking cool careers (ex: I'd work at Jane Street if I was not a parent and a lot smarter :)). Out of curiosity, what are the typical places your classmates go upon graduation?
Could you elaborate on how this works?
In a growth company with massive scale, a system always has some risky red-line, and so it is better to spin up some competing efforts with different perspectives to tackle different needs in different ways.
The key is to have redundancy of people over time and to create multiple thought leaders within a company; this makes it interesting. Then, at a future date, a "re-org" will happen to condense efforts and the real product is having a number of people familiar with the ideas spread across the company.
This makes zero sense for start-ups, but when you are a risk taking company with massive budgets... strategies are interesting. You see this with VCs having stakes in similar investments as well, and the idea is the same... diversify over people rather than perfect minimal code.
It's also a big disadvantage if that one person ever wants to move on. I write this from personal experience; don't be a solo developer on a large project if you can help it.
The haskell standard library is tiny. Libraries like lens are not optional. In practice you won't understand any open source Haskell without rudimentary understanding of lens. I get why parser libraries were banned, but excluding lens, vector, and text?
I like Rust a lot, but haskell minus it's more advanced type system is just Rust plus GC. Lets not pretend this is a fair comparison of languages when it's primarily a comparison of standard libraries.
To me it looks like an impressive proof of concept for a future programming language based around it.
If I were to start a project with Haskell the use of lens would be explicitly forbidden.
Most of us, including myself, are biased towards languages like Java, C, C++, Javascript, because those are what we learn first - and so our expectations of what errors (or syntax) look like are shaped by our early experiences.
So I don't think it's fair to say that Haskell's compiler errors or quirks are fundamentally less intuitive than something that GCC/G++ spits out even on a sunny day. Just odd when we expect errors to look a particular way, but Haskell is playing a totally different (not exactly harder) game.
C++ also had this problem with the standard containers. However it is much easier to get what is a dictionary compared to a random "optic".
This is exactly what I disagree with. We come from a prior understanding of mutable/imperative dictionary/shared_ptr/std::pair, because that's what we started out with.
Had we been initially been trained on monads, functors, lenses, those would be the familiar tools, and we'd go "Huh, that's an... interesting way to write code" when faced with C++ for the first time.
The main problem with lenses is that common lens libraries look extremely complicated at first glance and seem to be solving a very simple problem. That rightfully puts most people off of learning what all the fuss is about.
Name your records like "data Prefix = Prefix { prefixFieldName :: ... }" call "makeFields ''Prefix" once at the bottom of your file and use "obj ^. fieldName" to access and "obj & fieldName .~ value" to set.
That's it. You now have 100% of the capabilities of record update in any other language. This doesn't get any simpler in any other language. It even pretty much looks like what you would do in other languages.
I'll grant you, Haskell and lens do a terrible job of explaining subsets of functionality that are simple and let you get the job done before jumping in the deep end.
* I don't need to import a module to make available the syntax for getting and setting fields of an object.
* I can use the same syntax for any object, and don't have to worry about doing a bunch of code generation via a badly-designed metaprogramming hack.
* I don't have to worry about adding prefixes to all my field names.
* The syntax uses familiar operators that I won't have to look up again on hackage if I stop writing Javascript for a few months.
* No-one modifying my code can get "clever" and use one of ~50 obscure and unnecessary operators to save a couple of lines of code.
What bugs me is when Haskell advocates try to use all the additional esoteric features of the lens library as an excuse for this fundamental baseline crappiness.
Haskell really just needs proper support for record types. Then people could use lenses when they actually need lenses (never?). At the moment, they're using lenses because they want something that looks almost like a sane syntax for record updates.
In practice, the main reason people use it is to work around the deficiencies of Haskell's built-in record system:
>I never built fclabels because I wanted people to use my software (maybe just a bit), but I wanted a nice solution for Haskell’s non-composable record labels.(http://fvisser.nl/post/2013/okt/11/why-i-dont-like-the-lens-...)
The other features of lenses don't strike me as particularly useful. YMMV. I'd also question the quality of the library. It's full of junk like e.g. http://hackage.haskell.org/package/lens-4.17.1/docs/src/Cont..., which is just an invitation to write unreadable code.
For example, if I had a list of records with a field named 'categories' holding a list of objects with a field named 'tags', and I wanted to get all of these names in one list, without nested loops, lens makes it easy 'record ^.. categories . each . tags . each' or I could update them all, etc. It's just so easy to do this kind of data munging with lens that writing fors, whiles, etc in other languages is painful.
Yes, but not from programming, but from general life experience. Everyone knows what an actual dictionary is, and even non-programmers can easily grasp how a one-way 'map' works.
Mutation is also how the real world works. If you want to record something, you write it down—you've just mutated the world, not encapsulated your operation in the WorldState monad.
You need to build a pile of mathematical abstractions in your head before you can really get off the ground with lenses. Not everyone has that aptitude or interest.
You 100% do not need to build a "pile of mathematical abstractions in your head" to use lenses. It's a handful of types and functions. Do you need to build a pile of abstractions in your head to use `std::unordered_map` or getters/setters in C++?
For context, I was introduced to FP (SML in this case) around the same time I learned Java, and I still think for the vast majority of coders, an imperative map is much easier to grok than lenses.
The former only requires understanding how values are manipulated and mutated. You're going to need to understand this anyway to write software, since your machine is mutating values in memory.
Lenses however require complex type-level reasoning, so now you must learn both the value language and the type-level metalanguage at once, and your language also deliberately obscures the machine model. That might be powerful, but it is still an additional mental model to learn.
I mean, just look at the Haskell wiki reference: https://en.wikibooks.org/wiki/Haskell/Lenses_and_functional_...
The route to understanding them goes through Applicative and Traversals, which means have a solid understanding of typeclasses.
But is it, though? Perhaps you just appended something to the world-log in a purely functional way. Time seems to always go in one direction (at least in my experience, YMMV), kind of like a DB primary key that is monotonously increasing. It really depends on how you look at this.
I'd say my point stands pretty well.
Good luck with that :)
There are some proposals which address this (e.g., concepts), but none of them are part of the language standard yet. Concepts in particular made it into the C++20 draft, but they also made it into a draft of the C++17 standard and were ultimately rejected. Somewhat vexingly C++ concepts actually come with the same problems only at the level of concepts instead of at the level of templates.
This isn't normal. This is just using a tool that sucks. Those who consider this normal are just masochists.
Rust, elm, etc. have great error messages. That took a lot of time and effort to achieve. The fact that it is impossible to implement a C++ compiler that produces good error message is just proof about how broken the language is. The fact that some people find this normal is just Stockholm syndrom at work.
It's like lens, but with the design goal of being easier to use and producing better error messages.
I’ve written tens of thousands of lines of Haskell, and I’ve never used lens. Also, putting it in the same category as text and vector doesn’t make sense — these are indeed unavoidable, and practically all my projects use them.
- TAGS work like a charm to access field definitions
- compile times are ok
Of course, if library's API needs lens, they're used.
For example, my early Fortran programs looked like Basic. My early C programs looked like Fortran. My early C++ programs looked like C. And my early D code looked like C++.
It takes much more than being able to write a program in X to be able to write one that makes proper use of the language.
Of course an expert of a given programming language can write much better in it than a novice. But a comparison like this is not necessarily about comparing top-programmers in every language, but average programmers, because we want to know results that are true "on average" .
The author does note he "knew (they) were highly competent". So they were not exactly novices in their language of choice. Writing a compiler is not a task for novices in general.
I think it is actually very likely that people would chose a programming language or system for reasons other than how competent they are with it. E.g. to seem "smart" because you wrote your compiler in Haskell, even though you actually have much more experience with Java or Python.
FTA:
> Another interesting thing to note is that at the start of every offering of the course the professor says that students can use any language that can run on the school servers, but issues a warning that teams using Haskell have the highest variance in mark of any language, with many teams using Haskell overestimating their ability and crashing and burning then getting a terrible mark, more than any other language, while some Haskell teams do quite well and get perfect like my friends.
Good point these were students so they were eager to learn new things. Can't blame them.
At the same time much of programming is learning new things continually. Some things are harder to learn and master than others. Seems like Haskell might be one such thing, based on what the professor says.
In particular, some languages with a good average may contain pitfalls that could lead to abysmal outlier results (e.g. C++ or Scala, in entirely different ways), some would keep you from doing really dangerous things but also would not let you achieve spectacular things (e.g. Go).
The Haskell team had "maybe a couple thousand lines of Haskell each" at the start. This one project ended up being 9.7k lines, so it constitutes half of their collective experience with the language. I'd say that counts as "novice" in terms of prior Haskell experience. Under the circumstances I think they did remarkably well to produce a thoroughly tested and maintainable end product in just twice the lines of code of the quick-and-dirty Python implementation.
It's low if you're comparing languages based on the skills of experienced programmers, but most programmers are not terribly experienced. A comparison of languages by programmers that are novices to the language is still meaningful. A language that is easier for a novice to pick up and write good code is at the very least one good measure for the quality of the language.
"If you look at the GitHub contributions that I've made, I've made 2967 of about 3000 commits to the compiler source over that time frame. In that time I've added roughly 4,062,847 lines of code to the code base, and deleted roughly 3,753,677 line of code. [..] It means that for every one of those 750 lines, I've had to examine, rework, and reject around 5400 lines of code."
Yet a 750 loc codebase sounds like someone with very little experience and a few days or a long weekend.
I'm not saying only the trivial "some languages are denser than others", but also that it would be interesting to compare projects in total lines of code written including all commit history, and that it would be interesting to compare people's experiences and project designs in terms of "how many times something got implemented in several ways before settling on a final version", or "writing once and the design got set in concrete because it was too big to bother changing", how many prototypes that work differently were explored before deciding - and what that does to people's skills, and to project designs.
I'm guessing thousands of lines of F# would make you more skilled than the same thousands of lines of C#, even moreso if the F# was higher-abstraction than the C#, would you agree?
For the purpose of evaluating experience you count the total amount of code written, not the final codebase size. Four million lines of code is a fair bit of experience in any language, even if most of those lines were later deleted or replaced.
Only having a few thousand lines of code written in Haskell very much makes you a novice. With that said, writing a compiler in Haskell is actually pretty trivial. It's at the very least considerably easier than most other languages.
It doesn't have good pattern matching or the borrow checker, but it does have much better metaprogramming than Rust. I think a lot of the metaprogramming used in the Python project could also be done basically as easily in D, which is a big accomplishment.
D won't get you hired (probably), but D is designed with hindsight from a C++ compiler writer and a C++ template wizard: It shows, D is objectively better than C++ is many ways. It's worth checking out, at the very least (It's also not hard to learn, so I say go for it)
An example of the power of D: The Pegged library for D can generate a parser, D code which gets compiled, directly from a grammar specification in a text file [inside a D program, e.g. mixin(Grammar("Your Grammar Here"))]
On the contrary. Many members of the D community have managed to leverage their D expertise into well-paying jobs. Many industrial D users recruit from the D community.
I was aware of that but I meant in comparison to (say) Java or JavaScript etc.
D compiler can compile itself in under 5s (+ 10s for stdlib).
I was a former C++ full-time programmer (4 different jobs in high performance teams, high maintenance etc) and now I'm a D full-time programmer for 4 years.
It's easy to underestimate the difference, but to me _as a user_ those languages are night and day as an experience.
D is learnable, in the sense that your learning will have some sort of ending at one point. D is surprisingly stable too, the front-end doesn't change too much and all 3 compiler share the front-end. And it's somehow forgiving. So the whole experience is tilted towards what you do with it: you feel like "programming", not "programming in C++".
C++ has a few things going for it: it has some sort of mathematical completeness, you can find jobs maintaining C++ codebases for your retirement, and it has an aura of legitimacy. But overall I fear you would live in a much more complicated world, for reduced productivity.
Specifically the "by virtue of the language" part:
Seems to me like it's unreasonable to claim the languages are on equal footing because fancy parser libraries aren't allowed to be used for the project. The fancy parser libraries exist for certain languages specifically because the languages enable them to be written. (For example in Haskell: monadic libaries, libraries that take advantage of GADTs, etc.)
I think if any library could make a real difference for Haskell it's most likely to be http://hackage.haskell.org/package/lens, which a Haskeller friend of mine claims could likely make a lot of the AST traversal and rewriting much terser.
My experience with using both PEGs and parser combinators is that there isn't a huge difference in the total number of lines of code. On the other hand though, the syntax of PEGs would be easier to understand for someone who is familiar with BNF style notation.
Building up the knowledge to get to this point however… nope, those students were better off going hand written recursive descent (or Lex/Yacc, since an equivalent was allowed).
https://github.com/LoupVaillant/Monokex/blob/master/src/pars...
http://loup-vaillant.fr/projects/metacompilers/nometa-haskel...
More anecdotally, I’d argue parsing libraries are common, just look at the prevalence of attoparsec and others. But most parsing libraries in the ecosystem are parser combinator libraries which don’t support as performance and nice error messages that compilers need
It also makes the language comparison useless. Python has a standard library that is continuously improved and people reach to that when writing programs. Haskell, like C, ossified it’s standard library when it was created and people use the external packages for equivalent up to date libraries.
Relative to what? Haskell and OCaml are important languages for PLT but not in the context of "production", or as I understood "production" to mean: shipping products with features. To call them anything but niche players in this context is not accurate in my opinion.
I know less about OCaml, but at least Jane Street is using it.
It's somewhat niche, but it's not like it's only used for hobby or toy applications.
[1] https://code.fb.com/security/fighting-spam-with-haskell/
There's a lot of OCaml in the program verification space: https://frama-c.com http://why3.lri.fr https://alt-ergo.ocamlpro.com
And for mysterious reasons, OCaml is now kinda popular for.. web frontends. Bloomberg created an OCaml-to-JS compiler https://bucklescript.github.io and Facebook (again!) created an alternative syntax https://reasonml.github.io and this combination is apparently a new hipster way of writing web apps.
If you look at Java for instance, generics were made by one of the Haskell creators, and it has been implementing functional features for years now.
Building Chromium atm, and to be honest I'd be happy if it were written in a trillion lines of BASIC if that would somehow achieve even a 10x build time speedup.
Lets see how C++20 will improve the situation.
A dynamic language like Python is better here, 2x better. I assume similar results would apply to other dynamic languages like JavaScript, Lisp, Smalltalk, Groovy etc.
This does not say that static typing should not be used but I think it shows unequivocally that there is a considerable extra development cost associated with static typing.
You might say that surely that additional cost would be compensated in reducing the cost of maintenance later. Maybe but I'm not sure.
Any development effort of significant size (like writing a compiler here) is a combination of writing new code and adapting and modifying code already written. "Maintenance" is part of development.
This is quite a quantitative study which gives credence to the claims of advocates of dynamic languages.
A caveat is that I'm pretty sure my friend intentionally sacrificed code quality to do it, I don't think you'd find that project as readable and understandable as the others. Another caveat is that you have to be okay with your codebase being extremely magical and metaprogramming-heavy, which many industrial users of dynamic languages avoid.
As I mention, I'm personally into statically typed languages mostly for the performance and correctness benefits. I think it's plausible that on larger projects with teams the correctness benefits save enough debugging time that overall implementation time is lower, but I'm less confident in this now than I was before this comparison.
My hunch is however that what is often overlooked in development with statically typed languages is that it takes considerable time and effort to come up with the right set of types. Many examples are written showing how types almost magically make programs easier to understand. But when you read such an example what is not stated is how much effort it took to come up with just those types.
One way of thinking about it is that type-definitions are really a "second program" you must write. They check upon the primary program and validate it. But that means you must write that second program as well. It's like building an unsinkable ship with two hulls one inside the other. The quality will be great but it does cost more.
To be sure, having to write down those data structure invariants in a rigorous way that fits into the type system of your programming language has a cost. But the hard part really is coming up with the invariants, and it's dangerous to think that dynamic languages obviate the need for that.
A good example of this is matrix operations - there are plenty of invariants and contracts to check (e.g. multiplication must be between m x n and n x p matrices), but I don't believe there's yet a particularly convincing Haskell matrix library, in part because the range of relevant mathematical invariants don't cleanly fit into Haskell's type system.
For those cases, checking the invariants at runtime is your escape hatch to utilize the full expressive power of the language.
Surely. But if you have to write them down it becomes hard to change them because then you will have to rewrite them, and you may need to do that many times if your initial invariants are not the final correct ones.
The initial ones are likely not to be the final correct ones because as you say coming up with the invariants is ... the hard part.
What I'm trying to think about is that in a language that requires you to write the types down they have to be always written down correctly. So if you have to change the types you use or something about them you may have a lot of work to do because not only do you have to rewrite the types you will also have to rewrite all code that uses those types.
That does allow you to catch many errors but it can also mean a lot of extra work. The limitation is that types and executable code must always agree.
Whereas in a dynamic language you might have some parts of your program that would not even compile as such, if you used a compiler, but you don't care because you are currently focusing on another part of your program.
You want to test it fast to get fast feedback without having to make sure all parts of your program comply with the current version of your types.
A metaphor here could be something like trying to furnish a house trying out different color curtains in one room. In a statically typed language you could not see how they look and feel until all rooms have curtains of the same new color, until they all follow the same type type-constraints.
I've written once here before, this is one of the 'accidental advantages' of TypeScript: you set the compiler 'loose' when you're hacking away, writing quickly, and then 'make it more strict' as you start to consolidate your classes.
I almost don't bother to type something until I have to. Once I see it sitting there for a while, and I know it's not going to change much ... I make it a type.
It's an oddly liberating thing that I don't think was ever part of the objectives of the language, moreover, I can't think of any similar situation in other (at least mainstream) languages.
Add type hints at any time, check types at any time. Type hints can also serve purely as hints to programmers and not checked at all.
That aside, speaking off the cuff totally separately from my post, I'm extremely dissatisfied with the performance of current compilers. The fastest compilers written by performance-oriented programmers can be way faster than ones you generally encounter. See luajit and Jonathan Blow's 50k+ loc/second compilers and the kind of things they do for performance. One example of a compiler task it's really difficult to do quickly in a language like Python/Ruby is non-regex parsing, I've written recursive descent parsers in Ruby and compiled languages and the Ruby ones were 1-2 orders of magnitude slower, non-JIT dynamic languages are really bad at tight loops like that.
Lua and Jai are lot less complex than say C++: sure, LLVM isn't necessarily built to be the fastest compiler in existence, but I don't think it's fair to compare it to compilers for much simpler languages and bemoan its relative slowness.
So it's the output of ONE Python programmer vs teams of other languages programmers?
It is well-known that teams cause overhead. Think of the Mythical Man-Moth.
But in the end the other teams had 3 people that were able to maintain and develop their project further, if needed. The single-person team had only one such person.
While writing the previous paragraph, I wrote 'programmer' where I had previously put 'implementation', because I would guess that this study's outcome is better explained by human factors than language differences.
I share the attitudes you state in your last paragraph, but I would add that we should be skeptical of concepts of quality that seem plausible, but which lack empirical evidence for their effectiveness.
> A dynamic language like Python is better here, 2x better.
I was surprised by how small the LOC benefit was from using the dynamic languages. As someone who typically reaches for Python, I'd use a statically typed language (Go or Java, most likely) much more often if I expected only twice as many lines of code. In practice I feel the same project takes 3-10 times as many LOC and that pushes it to where it is more difficult to maintain and understand.
> I think my overall takeaway is that design decisions make a much larger difference than the language
After all, Scala was 0.7x the size and is one of the most strongly statically typed languages. So you could almost invert your conclusion and say the big result is
"Python only saved 20% code lines over fully statically typed language"
Scala has a very rich standard library. Python doesn't. An example is: https://stackoverflow.com/questions/363944/python-idiom-to-r...
Ruby would have been a better example of how succinct a dynamic-typed language can be.
(I made a similar comment on the parent level.)
This is not something I could have expected someone to say about Python's standard library…
Another example is that Scala offers a lot of ways to process a list like foldLeft, foldRight, unzip, headOption, lastOption, flatMap, groupBy, and many methods around Map, Set, and etc. Python probably doesn't offer many of these methods.
Of course, this comes with the cost of higher learning curve.
Actually, that seems to be the direction/principle of Python, where it is less inclined to add a helper function.
"It has been discussed ad nauseam on comp.lang.python. People seem to enjoy writing their own versions of flatten more than finding legitimate use cases that don't already have trivial solutions." from https://softwareengineering.stackexchange.com/questions/2542...
Not that this is better or worse. It's just that, on the brevity aspect, Python code is gonna be longer.
next(iter(your_list), None)
This is 100% standard library.The degree of richness and/or the height of abstraction seem lower in Python. (Not that this is a bad thing. It depends on people's taste, of course.)
Like dragonwriter says, it shows that "Python expresses the concept fairly compactly in the core language without even resorting to stdlib".
It depends on your taste whether you like richer stdlib, and I do. But some don't.
We are talking about how short the code can be in this post, and `my_list[0] if my_list else None` (Python) is longer than `my_list.headOption` (Scala) or `my_list.first` (Ruby).
I'd appreciate more if you elaborate why my example isn't a good illustration on the brevity aspect.
The interesting takeaway from this study I think is that it does not show that statically typed (even "pure") functional languages are not obviously better than plain old Python.
The interesting thing is not what this study proves, but what it does not prove.
Could you elaborate more on this statement? Not a native english speaker here, so I don't quite understand the sentence. Thank you.
(1) It shows Python expresses the concept fairly compactly in the core language without even resorting to stdlib,
(2) It ignores an option (also in the core language) using a ternary and the truthiness of lists: my_list[0] if my_list else None
And we are talking about the brevity of a language here.
The results say something about the greatness of Scala, not of statically typed languages in general. The other ones did not do quite as good as Scala.
From what little Scala I've read it looks very terse indeed. That can be a benefit but at the same time makes it harder to understand code written in it, in my opinion.
For example, it's not completely clear from the post but it seems like the 0.5x figure is from wc -l, which means Python wins a line every time there is a conditional or loop just because it doesn't need a closing brace. That alone might eat up a lot of the 20%, but you would be hard pressed to say that is a meaningful difference.
The reason I think this is "big big news" is I thought the general consensus had already been reached in academia if not the programming community that "statically typed functional languages are much better". There's little or no evidence of that in the results of this study.
If there's a surprise, it's that Scala is able to snuggle up to Python so nicely, while being one of most strongly typed languages.
More to the point: while the numbers nominally show differences in languages, everyone with programming experience recognizes that unmeasured and uncontrolled human factors probably had a big part in the outcome.
I like that the title frames it as a language shootout to pull people in to see if their favorite language wins (and I'm partial to Python having rewritten tens of thousands of lines of Java into numpy). Still, it would be foolish for people to come away from this brilliant analysis by ignoring the more important conclusion.
It's either that or assuming that your data will contain exactly what you want to, ignoring every possibility outside the happy path, which is a recipe for disaster.
That said, I have to say that modern JS produces the cleanest looking and less verbose code I've ever worked with so far. I wish there was a way to work with types without adding verbosity and harming readability.
I would agree, if and only if I thought a representative sample of Python programmers would all produce something of a similar size and just as correct, but I suspect, in this case, it's the result of one especially talented person.
> You might say that surely that additional cost would be compensated in reducing the cost of maintenance later. Maybe but I'm not sure.
I am sure. 100%. From many years of experience.
Yes, static types come at an initial cost at initial development time. But they pay that time back in spades. This is exponentially true the larger the code base is (more to keep in one's head), the longer the project lives, and the more people are on it.
Having worked on very large C/C++, Scala and Python projects, when it comes to add a major feature or perform a serious refactor, I always want the static typing and the compiler to inform me when I've missed something. Far too many times has code been checked into a large Python code base that breaks something (completely unbeknownst to the programmer), because there's a code path that's rarely executed or arguments to a function flipped, etc.
That all said. There are major benefits to being able to prototype very quickly in a dynamically typed language, too.
That layer is there anyway, except for your interpreter of a dynamic language only knows about problems at run-time, while the compiler that checks a static type system will tell you at compile-time.
You can of course add explicit checks and tests, eventually paying the same amount (or more) in LOC as the typed implementation's initial cost, but then you're also tasked to keep those up to date, without the compiler's aid.
Duck types are great for writing new code, but they're very troublesome for refactoring; automated tools have a much harder time automating that process.
Refactoring data structures and implementations in static type systems is bother considerably easier to implement than in dynamic languages, and resultingly, more robust.
Certainly the refactoring tooling these days is pretty sophisticated with type inference, but... well, I've refactoring large python and javascript code bases, and my experience has been that absolutely a static type system makes that process easier, even if you have a comprehensive test suite.
I think it's worth acknowledging that there is a place for static type systems; certainly, it's not a silver bullet, and it results in (usually) a higher lines-of-code count, which is significant; but its naive and wrong to suggest that it has no value at all.
Specifically, for refactoring, it has a lot of value.
You either have a type-checker and compiler, or you don't.
This can be exemplified using Crystal, which is close to Ruby in both terseness and APIs, but statically typed (and with static dispatch).
You have to be careful with that.
In a dynamic language your test system is your compiler. It is essentially a domain specific compiler. With a statically typed language, half your tests are already written, and you just have to activate them with a type signature.
For my money the trade off is down to whether forcing structures into a type system and getting the 'free advanced tests' is more advantageous than constructing a domain specific test harness 'compiler' and getting the flexibility of duck typing.
And you only learn that when you run up against the limits of the type system and have to start complicating structures to work around it.
In terms of a rough metric I'd suggest you have to include the test code in both cases to get a fair comparison.
Having said that, the largest point I took away was that the difference between languages was smaller than the difference between programmers and approaches.
> This does not say that static typing should not be used but I think it shows unequivocally that there is a considerable extra development cost associated with static typing.
Are you saying that the bottleneck in software development is typing in text on a keyboard?
If so, I thoroughly disagree. In my experience, inputting text is a small factor in development cost, with the main cost being research (figuring out what to type in), and debugging (figuring out why something you typed in doesn’t work as you thought it would).
>>> Abstractions may make things easier to extend in the future, or guard against certain types of errors, but they need to be considered against the fact that you may end up with 3 times the amount of code to understand and refactor, 3 times the amount of possible locations for bugs and less time left to spend on testing and further development.
Choosing when and how to abstract is key. Abstraction in a fashion extends the language so you are trading off a burden on the reader (who have to learn and trust the abstraction) for increased expressiveness. Don't overdo it. (And don't abstract idioms).
addr = (addr + PAGESIZE - 1) & ~PAGESIZE;
contrast with addr = P2ALIGN(addr);
The former is idiomatic and immediately readable. For the latter I have to go lookup the definition of P2ALIGN (and make sure I got the one actually used as there may be multiple!), check that it does what I expect, and memorize it.The value of abstracting the idiom is debatable. If it _is_ used sufficiently often, then it might be worthwhile, but often it isn't.
I had a similar experience in university. Class had to implement a modified Turing machine. We could use whatever language we wanted. On person did it in C++ and it was several hundred lines. Another in Java which was slightly smaller. I implemented mine in Python and it was small enough to print on a single piece of paper. I think it was something like 30 lines or so. I exploited break statements to make it terse but readable. It did mean I was marked down a bit for them but it was by far the shortest program in the class.
I ended up using raw lines for comparison because for relative measurement it didn't matter for the above reason, and I knew everyone had the same `wc` and I could get them to send me the output of the `wc {glob}` version that lists lines and bytes of all files so that I could drill down into which parts were different.
They are also support for more languages and are updated way more often. Very much second generation tools that learnt from the first.
Your use cases seem to prioritize language support, update frequency, and speed (what do you mean by the accuracy part?). For this, Scc and Tokei would of course be better than UCC.
The (admittedly niche) use cases I described require understanding the counting rules very well, and keeping those rules stable. For this, scc and Tokei are as useless as anything else, while UCC does exactly what's needed.
I see your point. I’d argue however the rules for counting should be language rules not some higher level generic set.
Each language does have its own counting rules. There's a separate PDF for each one describing the rules for that language, which is the cool part.
And even that only tells some of the story e.g. do code counters count separators ({ or } alone on a line) as blank or as code? Are multiline strings (e.g. python docstrings) counted as code or comments?
Great post; it echoes all the experiences I/we’ve personally had, especially the alternative Rust section: on the spectrum of how much intelligence one needs to understand fancy abstractions, all of us programmers, good and bad, are pretty much lumped to the lower end. I’ve seen plenty of skilled folks “decompensate” their expertise by reaching for overly complex abstractions. The definition of a “strong” programmer, as per the post and usually in real world, should probably be rectified to include not only “the person knows how to use this abstraction”, but also “the person knows when to stop”.
In the same vein of idea, it’d be interesting to know how Go would fare in this project. Go’s philosophy is pretty much the opposite of a language that’s usually used for compiler research; but I’ve seen indicators that using it could be surprisingly effective (and in light of this post, maybe less surprising).
More importantly, it’d be nice to know the perf characteristics of each project =)
Also I commented somewhere else in this thread re perf comparison. Short answer is that since there's no incentive towards performance the signal would be swamped by differences in how much people avoid O(n^2) algorithms even if you don't need to.
I would say a bit worse than the mentioned alternatives, which all have better type systems and thereby e.g. make it easier represent and manipulate the trees that are everywhere in compilers. But most likely it's still fine, and the line count metric would be more influenced by the fact how experienced the author is than the language.
Things will get worse in Go if one wants not write a lexer/parser manually but use tooling for that. Parser combinators and other tools benefit a lot from generics.
On the other hand e.g. writing a network server with decent performance will be in Go a lot easier and more straightforward than in any of the other mentioned languages. While all those languages are general purpose programming languages they definitely have their strengths in different areas. Some are better for some tasks (e.g. compilers), other are better for others.
(I don’t advocate dropping static types or ADT; just that in the spirit of this blog post, it might be worthwhile to examining our assumptions.)
if (x) {
y
}
else {
z
}
if x:
y
else:
z
Assuming x and y are 1 line but long enough expressions you don't want to use a trinary operator (available in both) the python is 2/3rds the length of the c because of bracketing. Obviously cherry picked, but I bet these differences add up.I wrote the style of C that I actually write, but it's probably not fair for me to label it as a property of just the language.
x = y ? 1
: 2;Something like
Screen.oled ? "black" : "rgb(21,21,21)"
is easy enough to read. Beyond that, if statements win for me.It's a tricky problem to decide which idioms are most expressive/readable/maintainable as it's has a group dynamic. My rule of thumb, if I feel I've written clever code it's time to rethink approach.
var myvar = foo < bar
? bar
: bar < baz
? baz
: 0 const myvar
= foo < bar ? bar
: bar < baz ? baz
: lark < 10 ? lark
: 0 var myvar =
foo < bar ? bar :
bar < baz ? baz :
0
or: var myvar =
foo < bar ? bar
: bar < baz ? baz
: 0It's only the Algol-syntax-family languages (C, C++, Java, JavaScript, etc.) that have the inscrutable ternary operator and if/else as a statement.
One thing that people writing this sort of code seem to miss is that it's not just about expressing the code as concisely as possible - other people including oneself in future need to be able to read it easily.
What I would recommend in a code review for anyone using nested ternary operators is just to break them up using meaningful variable names so that you have one ternary operator per statement. It'll be easier to read and the names will help understand what's going on more easily.
Would using ternary operators as a terser switch statement be fine?
int a =
b == 1 ? 2 :
b == 2 ? 3 :
b == 3 ? 5 :
-1;I also tend to format them thus:
int a = b == 1 ? 2
: b == 2 ? 3
: b == 3 ? 5
: -1;
Feels much more "case analysis-y" and clearer.Still, a language having proper support for expressive conditionals is probably better.
Also the way Python's ternary was defined means it should never ever be nested, it looks nasty.
I don't think there's ever a good excuse for that much nesting of ternary operators.
* Lines of code
* # of keywords/operators
* # of non-standard keywords/operators (i.e. 'go' versus 'if')
* Use of std. library vs. 3rd party libraries
* Depth of call-stack, i.e. is it passing through 3 parent classes
I wish you would use Ruby instead of Python. Python is strangely inconsistent. For example, Python doesn't really offer a rich standard library; people have to resort to ugly solutions for a simple problem (here's an example: https://stackoverflow.com/questions/363944/python-idiom-to-r...)
Ruby would be better at an example of how a dynamic language can be more succinct.
But I had to source from friends in the class, and my group used Rust because I'm all about that performance and correctness nowadays.
Python is usually considered one of the richest standard libraries out there.
Yes, the is no standard way to get this one edgecase, but every language has warts and things like that. It sounds like you are just looking for an excuse to hate on Python.
Sure, but we are talking about the brevity of a language. The richness of the standard library directly impacts brevity.
So, Ruby would have been a better representative (for brevity) of a high-level dynamic-typed language.
> the is no standard way to get this one edgecase
I don't think `.first` is an edge case. It is used fairly often. One example that I can think of right now is when you want to fetch the first row from MySQL. MySQL returns an array, and you would need `.first` to get the first row or null.
> It sounds like you are just looking for an excuse to hate on Python
Not at all. While I don't prefer Python, I recognize there's a downside to a richer standard library. The language becomes more complex; harder to learn.
Richest is relative. If we consider Python against every other programming language, then, yes, it's ONE of the richests.
If we consider Python vs. Ruby vs. Scala, I doubt Python would be considered as richer or richest.
For example, Scala offers a lot of ways to process a list like foldLeft, foldRight, unzip, headOption, lastOption, flatMap, groupBy, and many methods around Map, Set, and etc. There are many examples where Ruby version would result in shorter code like `array.delete_if` and etc. Python probably doesn't offer many of these methods.
That's a big claim to make based on a couple of assertions that are neither about the quality of Python as a language nor the quality of its standard library, merely observations and comparisons.
Are you sure it's not you who is upset to see Python 'attacked' (for lack of a better term) in any way?
Proposed solution - `a[0] if a else None`
Your reaction - Python doesn't offer a rich standard library.
I disagree, and I suspect most programmers would too. We'd much rather have ergonomic libraries to make http requests, parse command line arguments, datetime, itertools, data structures like heaps, filesystem access, data archiving, data serialization and deserialization and a million other nice-to-haves. It's actually at the point where I've heard criticism of Python's stdlib doing too many things, rather than too few.
If you don't want to write 19 characters to find the first item in a list, pick another language. But don't mischaracterise python's standard library.
The sad part is that it totally didn’t have to be this way. But instead of evolving, Python is just stuck in the past.
There's no linked lists. There's no sorted maps, no sorted sets, no maps that preserve insertion order, no queues, no priority queues, no bitsets. And there's no immutable collection in Python besides strings.
Meanwhile, Scala has all of that and more: https://docs.scala-lang.org/overviews/collections/overview.h...
Moreover, scala has synchronized collections that can work over multiple threads. I guess all collections work that way in Python, but that's because Python doesn't even support true thread parallelism in the first place!
Also, if we're talking about the number of methods on the collections, Scala has way way more. map/foreach/filter/foldl/foldr/option/drop/take/first/last etc etc.
frozensets, tuples?
In Python, it's `import itertools` and `list(itertools.chain.from_iterable(list2d))` or `[item for sublist in list2d for item in sublist]`. In Scala, it is `list2d.flatten`.
In fact, Python is against making a richer stdlib in general.
"It has been discussed ad nauseam on comp.lang.python. People seem to enjoy writing their own versions of flatten more than finding legitimate use cases that don't already have trivial solutions." from https://softwareengineering.stackexchange.com/questions/2542...
And, intuitively, when you don't want to provide a helper function, the user code gets longer.
Not that this is better or worse. It's just that, specifically, on the brevity aspect, Python code would generally becomes longer. Because, as you see in the quote, "People seem to enjoy writing their own versions of flatten".
My main point is Ruby's stdlib is richer than Python's.
(And that's not either better or worse. Some people prefer lighter stdlib, and that's fine.)
> If you don't want to write 19 characters to find the first item in a list, pick another language.
Yes, if Python supported `.first`, I wouldn't want to write the longer version of it.
Since the article focuses on brevity, I did propose another language here, which was Ruby. It would serve better at how succinct a dynamic-typed language can be because of its richer stdlib, especially when we compare a dynamic-typed lang with Scala, which has an extremely rich stdlib. Using Ruby would be a fairer comparison.
I gave one small example (`.first` vs `a[0] if a else None`) to illustrate my claim. Two more examples (from https://ruby-doc.org/core-2.4.1/Array.html) are `.rotate` and `.transpose`. I'm sure there are more examples around Hash and other data structures.
Python don't have these methods, and we have to make them ourselves. To make code even longer, we need to maintain and write unit tests on them.
How does this work? What’s stopping me from using some hyperpowered esolang I cooked up for the competition?
The project was just complex enough to require maybe 100-200 lines of code at the end of the day, thanks to liberal use of libraries. Not huge, but not atypical for a "scientific" programmer who uses code as a way to solve problems rather than to create software for others to use. It's pretty representative of my life as a programmer.
The exercise forced me to learn enough of each language to get a feel for it, and then I could look at the programs alongside one another to assess their strengths. I also imposed some rules, such as that the language had to run on multiple platforms, and be FOSS. In addition to comparing the languages, I was also implicitly comparing libraries and even access to online help. This was also my first real exposure to StackOverflow.
I tested Javascript, Python, GNU Octave, and wxMaxima. Ultimately Python won out and is my language of choice today. This was just my own little exercise, and not worth publishing, but has made me a believer in learning multiple languages.
For scientific coding I usually use either Python or Julia and Fortran or Octave at home sometimes.
However, to me it seems the comparison is more of a comparison between specific implementations rather than languages. The high variance in LOC between the two rust implementations makes me wonder if there's a similar variance for other languages and the samples we have lie somewhere between e.g. 0.5 and 2 times the average size for a given language.
Therefore, the python implementation could just be a really compact one, even when compared to other python implementations. It could be interesting to ask the Professor if he'd be willing to collect some stats over the years.
If we go by what you say, we would never have any comparisons at all, as there are always unknowns.
Software developers' expertise varies more than expressiveness of programming languages.
Because the implementations vary so much, that's the source of a lot of the LOC-difference. They effectively delivered more or less (if you count more stages as "more" which I would as it will make it easier to reason about/debug, and count a typesystem as "more", since it gives you more guarantees).
python - solves the problem brutally fast, some ugly shortcuts haskell - solves the problem quite slowly/delivered the most rust/c++ - intermediate scala - solved fast, took shortcuts ocaml - this is the one that surprised me I'd have expected it to be the shortest with python
I got that Boost Spirit, a powerful parsing library, was forbidden. Were all the Boost libraries similarly embargoed?
Also note that it's not only the sum types that are important, powerful pattern matching facilities are part of what makes them so valuable.
There is an active project to have a powerful pattern-matching primitive ready in time to adopt into C++23. In the meantime, C++17 supports "structured bindings" that are often helpful.
I regularly use Boost Spirit as an example of "sounds good but actually a nightmare", and am always surprised to see it mentioned in the wild. It is truly a modern horror.
Boost is like Apache -- a collection of libraries of various quality and stage of development, not all alike. A lot of stuff in there is designed to prove out experimental language features ahead of the next standard revision. A lot of that stuff is convenient, but a complexity nightmare under the hood.
For your own sanity, and of those you care about, don't accept all of Boost equally. Be suspicious, and treat each package as if it were some random library you found on the internet.
(There are of course tons of great things in Boost, but tread carefully)
In general, it is rarely a good idea to rely on a library you don't understand.
But Boost has serviceable variant, option, and result types.
huh, I have written a few parsers (half a dozen maybe ?) with spirit and absolutely don't regret it - why would you say that ?
In other words, how easy it is to adapt the language to the problem you are trying to solve.
So, in order to do that you need to have a good understanding of the principles each language is based on. Once you've got that, then you look at the resulting code and "measure" how easy it is to understand.
As was already said, this requires having someone with a very good understanding of the underpinnings of each language, which is not really going to be reasonable for most people.
The problem is that if you simply tried to port an idiomatic solution from e.g. C++ or Python into Haskell or Scala, or whatever, then you would probably end up with something very ugly, because you didn't adapt the language to your problem. You tried to force it to do something it wasn't necessarily designed to do.
"I purposely broke the code somewhere in these 20 lines. Find the bug and fix it."
You can use the compiler, you can use the test suite. You cannot diff the code against the original working code.
Static or dynamic typing doesn't matter that much if you can verify your software, and there are many different reasons to choose a particular programming language.
One of my best experiences was dovetailing that happened on a project that had to be c or c++ where I prototyped fully working system in ruby first, then rewritten it in c. Everything went almost too smoothly.
Of course, I can think of examples where that isn't true.
P.D: I'm building a relational language and my ideas mimic closely "writing-a-compiler-in-rust" ie: using pratt parsers and similar, but not see much of how do that on rust...
The bit about "no parsing helpers even if they’re in the standard library" makes me wonder about using DCGs, but if disallowed re-implementation would be straightforward and add only a small constant to the code volume.
I would like to see metrics on speed of compilation and size of resulting asm.
Rust_1 1.0 using recursive descent, visitor
Haskell 1.0-1.6x depending on how you count for interesting reasons
C++ 1.4x for mundane reasons
Python 0.5x fancy metaprogramming, dynamic typing, single author
Rust_2 3x different design decisions
Scala 0.7x LR table generator, online Java grammar
OCaml 1.0-1.6x depending on how you count, similar to Haskell
> AST visitors and recursive descent [...] weren’t taught in the course Similar implementations in Rust(1) and C++ are roughly the same
Similar implementations in Haskell and OCaml are roughly the same
A similar implementation in Rust(2) to one in Haskell/OCaml is roughly 2x-3x
Don't underestimate dynamic typing, metaprogramming, and individual effectiveness
But for real world, long lived projects I still promote static typing with the exception of startup development (MVP/product validation, early iterations) of new product ideas.Second: intriguing how important meta programming becomes once you nailed the problem domain.
On the other hand, in languages like C we couldn't exclude header files because they include macros. (Although I suppose a fancier comparison could count macros and exclude function signatures.)
And would you be allowed to use Racket's existing facilities for representing and implementing macros?
(You might want to use the macro stuff for your AST, IR, and transformations. Though you could still win with normal data types, but the macro stuff can help you win even more.)
> Since my team had all interned at Jane Street the other language we considered using was OCaml, we decided on Rust but I was curious about how OCaml might have turned out so I talked to someone else I knew had interned at Jane Street and they indeed did their compiler in OCaml with two other former Jane Street interns.
Relatedly this is why I get annoyed at people that don’t believe 10x or Nx developers exist - they absolutely do!
He was a lead programmer, who issued two-week assignments to other engineers; if one wasn't done come Friday, he would do it himself on Saturday.
He doesn't consider himself especially fast, because he knows someone else who codes ten times as fast, and wears out two keyboards per year.
This project was also interesting because they took blood samples, by which they got objective measurements of stress. Everyone's stress level increased right up to the deadline -- except his. And their stress levels did not start falling until many months after.
It also predicts that the N lines by the remaining people would be half done by about 22 of them and half done by 4xx of them; I wonder if that pattern was seen?
Even if the original engineer was 10x as productive, that would leave 12 people doing half of it, not 1. It makes me think that if the average team worked twenty days a month for six months the project should have been 60,000 units of goodness, and if he was as productive as all of them, 120,000 units of goodness. If, instead, he wrote rushed low-quality code, they spent a week trying to untangle and integrate with it, then he ignored their work and rewrote it himself, demoralizing the team and refusing to play nice with them, it might have reduced the overall units of goodness way below what it could have been. And at the same time he gets the boost of writing it all himself his way and not having to care about making it stable for others to work on, so he looks disproportionately better for that as well. What did the other team members say about it? What happened to the project after?
> He was a lead programmer, who issued two-week assignments to other engineers
You ought to expect a lead programmer to be better, otherwise why would they get and deserve that position? You'd also expect a leader to be way better than assigning work people can't complete, not working with them, not stopping doing that, then taking over from them. What would a 10x exceptional leader who wasn't a worker, get out of a team of 500 average workers?
So a couple of students cranked out 15K lines of code, in basically a subset of their study time, for a single class?
I don't doubt they did it, what I doubt is the quality of what they wrote. Because it's 'just a project' it doesn't have to be fully debugged ...
But that is a vast amount of code to hand-write over a short period of time.
At that level, I think it's basically just 'code like the wind' with not much consideration for whether it works, architecture etc. etc. - which in a way might put the conclusions of the 'mini study' at odds.
At 15K LOC dashed out very quickly ... these are not products, they're comparing 'rapidly written out lines of code', which is something else entirely from working code.
Also, given that everyone is in Uni, and likely may not have been exposed to proper idiomatic code for the language ... this presents another issue. Ideally we'd want to compare reasonably idiomatic code for one language, to another.
Kudos to the author for this, but we should be aware of some caveats.
Also note that UWaterloo's CS and SE programs are different than other universities. Everyone on all teams had at least two years of full time work experience at 6 internships. The people I talked to were also programming enthusiasts who read online a lot. The only teams that may not have written the most idiomatic code are the person who used Python alone, and the Haskell team because I've heard idiomatic high-end Haskell involves an insane amount of knowledge of abstractions like lens, way more than all the other languages.
This compilers class is also well known for requiring lots of work, so students often arrange their schedules to have a low load from other classes while taking it, which is possible since the CS program requirements have a lot of flexibility.
But - '6 internships' is not quite enough to do this comprehensively, as exhibited by someone blasting out 15K LOC (I'm still reeling at that).
I've worked on a number of projects in C++ and I'm pretty sure that I'm not very good at it, even with many years of experience in other languages, for example.
The authors deserve a lot of credit, but I don't think much can be concluded from this ... it's just not the right situation.
FYI - this kind of comparison is very difficult to do even for the most experienced.
The eval function leveraged heavily by the Python version should probably also have been off-limits, for the same reason that parsing libraries were prohibited. Using eval amounts to embedding an existing fully-developed parser and runtime environment into the project as a library.
Then we'd see what truly idiomatic solutions to this look like.