- Quality (How many bugs)
- Dev time (How fast to develop)
- Maintainability (how easy to maintain and adapt for years, by others than the authors)
The argument is often that there is no formal evidence for static typing one way or the other. Proponents of dynamic typing often argue that Quality is not demonstrably worse, while dev time is shorter. Few of these formal studies however look at software in the longer perspective (10-20 years). They look at simple defect rates and development hours.
So too much focus is spent on the first two (which might not even be two separate items as the quality is certainly related to development speed and time to ship). But in my experience those two factors aren't even important compared to the third. For any code base that isn't a throwaway like a one-off script or similar, say 10 or 20 years maintenance, then the ability to maintain/change/refactor/adapt the code far outweigh the other factors. My own experience says it's much (much) easier to make quick and large scale refactorings in static code bases than dynamic ones. I doubt there will ever be any formal evidence of this, because you can't make good experiments with those time frames.
I think one of our problems is that people have downgraded the importance of this. Much code nowadays (rightly or wrongly) is considered "disposable" - people think that the likelihood of any given piece of code they are writing as surviving more than a few years is negligible. It is a natural assumption when you see the deluge of new technologies, hype cycles, etc. It is further reinforced by the fact that people's empirical experience is that a huge amount of their software work is abandoned, rewritten, outdated, obsoleted, etc.
I think these views are horribly mistaken, because at a deeper level even if 90% of code gets abandoned, the quality of the 10% that survives is still going to determine your maintenance cost. And half the reason we keep throwing code away is because it was created without consciousness of maintainability - it is so easy to say that the last person's code was garbage, so we are going to rewrite it because that is faster than understanding and then fixing the bugs in what they wrote.
I observe this in myself: my favorite language to code in is Groovy - a dynamic, scripting language with all kinds of fancy tricks. But my favorite language to decode is Java. Because it is so simple, boring, there is almost nothing clever it can do. Every type declared, exception thrown, etc. is completely visible in front of me.
Well, most code is written by relatively-inexperience developers, who have not had to retire a system or support a legacy one, and don't know what should be sought out & what should be avoided when designing a system. Thus, they make decisions with limited information to solve the problem at hand, and only later find out the implications of those decisions when someone wants to (say) deploy it as a dockerized service on k8s.
It's one thing to read The Mythical Man Month, and another to write a replacement system that stops providing business value after 30 months and needs to be rewritten to support the current needs.
> it is so easy to say that the last person's code was garbage, so we are going to rewrite it because that is faster than understanding and then fixing the bugs in what they wrote
There's no black and white answer here: sometimes the code is so convoluted (or in the wrong language) that it has to be rewritten; sometimes the design of the system strongly resists changes in behaviour & so much of it needs to be made more flexible that an incremental improvement would cost about the same as a full rewrite.
Well, this has nothing to do with static vs dynamic typing. You can write unmaintainable code in static languages very easily. In startups, developers often overlook maintainability, I completely agree but that's because everyone knows that the code you are writing today might not be needed 2 years down the line, you are mostly iterating to find PMF.
one of my favorite things about groovy is that it's easy to start strongly typing things as your code shapes up, because it allows for totally dynamic types, but it also allows for strong static typing. haven't really had the chance to use groovy since 2012, though.
I think younger devs think this. Once you get a decade or more experience, you grow wiser and realise that code never dies, and especially the code you wish would die is particularly tenacious. And this is pure speculation, but I would wager that the number of lines of legacy code that is kept alive with maintenance is much greater than the number of lines of code that gets abandoned/rewritten/obsoleted.
What both type annotations and DbC are is self-enforcing documentation (of an interface) that doesn't go out of sync with the actual code. But for that, you don't necessarily need static type checks. Now, type checking of type annotations that happens exclusively at runtime is an option that hasn't been explored much (after all, if you already have type annotations, why not let the compiler make use of them?), but an option that has sometimes been used successfully is having a mixture of static and dynamic type checks. You can often greatly simplify a type system by delaying (some) type checking until runtime (examples: for covariance or to have simpler generics).
For example, if you add a case to a variant or sum type, or change the parameter or return type of some function, in a static type system, the compiler can tell you all the locations you need to change. In a runtime system, you have to find them yourself, or wait till you see an error at runtime.
Now, this is still better than the alternative of having the error propagate until it crashes 10 functions down, but the compiler finding all the places that need to be changed is something I've found to be really useful, especially in early development when there's a lot of refactoring happening. Presumably, this is probably useful in later stages as well, when the system is large enough that you can't expect to find all the uses of a function or type manually.
I personally find dynamic languages allow for easy replacability, as there's less explicit references of types. However this is highly dependent on the system being somewhat modular I suppose.
My second big love in languages was Python. It's also the language in which I wrote my first major software product. It was this product that taught me to hate Python. Not because it was hard to create, or because quality was low. In fact, I was VERY FAST to produce 1.0. Took about a week. But after that, I had to work with other developers. That's where everything went to hell.
Then I got a new job a few months later where almost 100% of my time was spent doing maintenance on aging codebases written in Java, a language I never worked with before then. I won't say I fell in love with Java, but, I did fall in love with the ease of inspecting "the world" in each project. As soon as I had it setup in my IDE properly, it was so ridiculously easy to explore how everything related, and then to make refactoring changes? So much easier than it ever was in Python.
Now, at that point I didn't directly make the connection with the type system, but, in retrospect, I know that all of the value I derived from working in Java vs. Python came from having a descriptive, static type system. And frankly, I never once felt slowed down by the need to specify my types up front. In fact, the opposite is true. It taught me to put more thought into my data structures and vastly improved the quality of my software design before I even started writing logic.
Sadly now I'm moving into the data science/data engineering field and everything is Python and I don't know. I don't want to go back to this nightmare. It's like I spent the last decade in first class establishments with the best tools and now I'm going to have to work in the mud with sticks and shovels. I am interested in the field in terms of the capabilities it enables, and I have no problems working in Scala or whatever decently typed language is around, but, the reality is the lion's share of people in this field are doing everything in Python or R and I hate them both.
I figure I have two choices: help advance the capabilities of "better" platforms, or pursue some other direction in my career. It's too hard to know how much better life can be, then go back.
Now there are (optional) type annotations and mypy [0]. I've been using them in my latest projects and I found them useful/helpful.
Multi-decadal longitudinal studies are not too uncommon in medicine, epidemiology and psychology. Why there is no will to conduct, or fund, this kind of research in computer science, I am not sure.
People live for ~80 years... doing a 2-4 decade study isn't out of the realm of possibility.
Computers on the other hand... While there are a few mainframes that live to be 10 years old - the vast majority of the internet, program languages, apps, etc... Hell, even the iPhone just hit 10 years old.
How can you have a 20 year study when the majority of "code" is less than 10 years old?
The previous application I worked on is over a decade old (and it shows). The current application I'm working on is about 8 years old.
Neither applications has any sign of being replaced. Which would be insane, as they both have roughly a decade of laws & regulations and business lessons embedded in them. Despite the state of especially the older application, I don't see how rewriting the entire application would fix anything.
At best parts would be rewritten. And the parts I'm thinking about wouldn't be rewritten because of technical reasons, but because of the way they work. The prime example is a part that only 1 person, a business user, understands.
Ah the HN perception bubble.
Good code last longer than that. Bad code gets replaced.
About best you can hope for is a new "epoch" that forces a rewrite. In the MS world we went from classic VB and VC++ to .net, a lot of companies went through rewrites to keep up with that and some of that software is now nearing 20 years old. There has been a few other epoch like changes, terminal -> GUI, c++ -> java, desktop -> web, except for maybe the last one it's been quite a while since a new epoch has begun.
Parasolid (written in a C dialect) was a rewrite of Romulus (written in Fortran) and that goes back to 1974. And that was a rewrite of Build that originated from Ian Braid's PhD thesis. [2]
I know people who are still working on the same Parasolid code after 30 years. Some of them
Disclaimer: Parasolid dev 1989-1995
[1] https://en.wikipedia.org/wiki/Parasolid
[2] http://solidmodeling.org/awards/bezier-award/i-braid-a-graye...
The CSS/JS on the frontend rarely lasts more than a few years, usually changed to due to design trends (flat, responsive, mobile-first etc).
Most business critical software like SAP for example is also based on decade old codebases.
It's probably a good reference project, when talking about maintenance (nightmares).
Some of our internal infrastructure systems are 10-20 years old - some could definitely do with a complete rewrite, but in the meantime, they're mission critical systems.
As for our products - some of them have even longer timeframes than 20 years.
Another little thing is (from my own point of view) is that many application don't live in a vacuum : they use json schema, or WSDL; and database with types and constraints. So what the language does not "type", the context does.
I know "fun" is highly subjective but still important none the less.
What is interesting is using the type system to specify invariants about data structures and functions at the type level before they are implemented. This has two effects:
The developer is encouraged to think of the invariants before trying to prove that their implementation satisfies them. This approach to software development asks the programmer to consider side-effects, error cases, and data transformations before committing to writing an implementation. Writing the implementation proves the invariant if the program type checks.
(Of course Haskell's type system in its lowest-common denominator form is simply typed but with extensions it can be made to be dependently typed).
The second interesting property is that, given a sufficiently expressive type system (which means Haskell with a plethora of extensions... or just Idris/Lean/Agda), it is possible to encode invariants about complex data structures at the type level. I'm not talking about enforcing homogenous lists of record types. I'm talking about ensuring that Red-Black Trees are properly balanced. This gets much more interesting when embedding DSLs into such a programming language that compile down to more "unsafe" languages.
I have a large code base. I want to replace a fundamental data structure to support more operations/invariants/performance guarantees. I change the type at the roots of the code base. My instance of ghcid notifies me of the first type error. I fix it. This repeats until the program compiles again. I run the tests. All the tests pass.
This is insane in Python/C/Ruby. I've had to do it in C and Python. In Haskell I do it with impunity.
The type system doesn't just check what my program does, it is the compass, map, and hiking gear that gets me through the wilderness.
Personally, I find it pretty workable in Python with a big codebase (but you have to respect the rules like having a good test suite -- you change the time from compiling to running your test suite -- which you should have anyways)...
I find that the current Python codebase I'm working on (which has 15 years and around 15k modules -- started back in Python 2.4, currently in Python 2.7 and going to 3.5) pretty good to work in -- there's a whole class of refactoring that you do on typed languages to satisfy the compiler that you don't even need to care about in the Python world (I also work on a reasonably big Java source codebase and refactorings are needed much more because of that).
I must say I'm usually happier on the Python realm, although, we do use some patterns for types such as the adapter protocol quite a bit (so, you ask for a type and expect to get it regardless of needing a compiler to tell you it's correct and I remember very few cases of errors related to having a wrong type -- I definitely had much more null pointer exceptions on java than typing errors on Python) and we do have some utilities to specify an interface and check that another class implements all the needed methods (so, you can do light type checking on Python without a full static checking language and I don't really miss a compiler for doing that type checking...).
I do think about things such immutability, etc, but feel like the 'we're all responsible grown ups' stance of Python easier to work with... i.e.: if I prefix something with '_' you should not access it, the compiler doesn't have to scream that it's private and I don't need to clutter my code -- you do need good/responsible developers though (I see that as an advantage).
It's true that static type-checking proves the absence of an entire class of errors. But it doesn't prove that the code does the correct thing; it could be well-typed but completely wrong. On the other hand, tests prove that the code does the correct thing in certain cases. ...Of course, it's up to the developers to actually write a good test suite.
The faster we can all accept that there are pros and cons to both, the faster we can come up with a solution that takes advantage of the best of both worlds. That's the whole point of this OP.
I, personally, have always wondered about ways to dial in to the sweet-spot over time as a project matures. At the start of a project, shipping new features faster is often more important. But if the project survives, maintenance (by new developers) and backward compatibility become more and more of a priority.
It's only insane if you don't have test cases with good coverage, in which case you are very-very-very screwed, statically typed or not.
I really wish more languages took this to the logical conclusion and implemented first-class contract support. It seems work on contracts stopped with Eiffel (although I've heard that clojure spec is _kinda_ getting there).
For my money, I work in a primarily dynamic language and I already have a set of practices that usually prevent relatively simple type mismatches so I very rarely see bugs slip into production that involve type mismatches that would be caught by a Go-level type system, and just that level of type information would add a lot of overhead to my code.
But if I were already using types, a more expressive system could probably catch a lot of invariance issues. So I feel like the sweet spot graph is more bimodal for me: the initial cost of switching to a basic static type system wouldn't buy me a lot in terms of effort-to-caught-bugs-ratio, but there's a kind of longer term payout that might make it worth it as the type system becomes more expressive.
Exactly. The author of the article implicitly equates "statically verified code" with "bug-free code". But that's not correct. It's quite possible (and even, dare I say it, fairly common) to have code that expresses, in perfectly type-correct fashion, an algorithm that doesn't do what the user actually wants it to do. Static typing doesn't catch that.
For example I can prove my my string reverse works in Idris (https://www.stackbuilders.com/news/reverse-reverse-theorem-p...). Or I could prove that my function squares all elements in a list. Etc.
Now a big part of the problem is expressing with sufficient accuracy what the properties of the algorithm you want to prove are. For example for string reverse I may want to show more than that `reverse (reverse s) = s`. Since after all if reverse does nothing that would still be true. I would probably want to express that the first and last chars swap when I just call reverse xs.
Not at all. First, the statements as put here are discrete (boolean even) while I present both "statically verified code" and "bug-freedom" as living on a continuum. Secondly, I don't equate them. If anything, I assume a monotonic, positive relationship between them (strictly speaking not even that. I make pretty clear that the curves could also have whatever shape. But I yield that I am very suggestive in this because I do strongly believe it to be the case). In fact, one of the main points of the argument is that the two are not equal - otherwise, the blue curves I drew would all be straight lines from (0,0) to (1,1). And lastly, none of this is done implicitly. I mention all of this pretty explicitly :)
It's also possible to grab a knife with your hand on the blade edge and cut yourself, but that doesn't diminish safety the value of knife handles.
E.g., my experience is that poor library design can sometimes be exacerbated in statically typed languages if the type logic is poor and doesn't match the problem domain. Dynamic languages sometimes inadvertently "correct" for this by smoothing over these sorts of issues.
I prefer static languages (or at least optionally typed ones) but there can be big downsides of the sort you're mentioning, that are exacerbated by third-party libraries.
Care to share those practices? I also primarily work (this year, at least) in dynamically typed languages.
Simple type-level errors come up the most when you have types that are easily conflated. That tends to happen when you have functions that accept more than one type of thing or output more than one type of thing - avoid that. Avoid polymorphism and OOP patterns that set up a complicated type hierarchy or override methods - you don't want any instance where you end up with something that looks a lot like one type of thing but isn't. Type hierarchies can often be factored out into behaviors provided by modules that supply functions that operate on plain data structures. For variables and parameters, stick to really simple types to whatever degree it's possible, e.g. language primitives, plain old data-container objects and "maybe" types (e.g. things that could be a primitive or null) when absolutely necessary (and check them whenever you might have one). Use union types extremely sparingly. Assignment/creation bottlenecks are useful: try to have only one source for objects of a certain type that always constructs them the same way (so you don't end up with missing fields).
A lot of programmers coming from a language with a stronger type system (especially when transitioning from OOP languages to functional or hybrid languages) tend to be nervous about writing functions without a guarantee about what kind of inputs they'll see, so they try to compensate for the lack of type safety by building functions that can cope with whatever is thrown at them. The idea is that this makes the function more robust but ironically, this tends to make bugs a lot harder to track down. In my experience it's better to write functions with specific expectations about their input that fast-fail if those aren't met, instead of trying to recover in some way - garbage-in-exceptions-out is better than garbage-in-garbage-out. If you send the wrong kind of thing to a function, you want it to throw an error then and there, and you'll likely catch it the first time you test that code.
A lot of the idea of this kind of advice is to shift the work that would be done by the compiler's type system to the very first pass of testing - if your program is basically a bunch of functions that only take one sort of thing in each argument slot, only emit one sort of thing as a result and fail fast when those expectations are violated, you'll typically see runtime type errors the first time those functions get executed, which is a lot like seeing them at compile time.
It really isn't as much about languages as it is about the people who use them. The key ability is to prove things about programs. Powerful type systems, especially those that have type inference, merely relieve the programmer from some of the most boring parts of the job. Sometimes.
Can you give an example of this overhead?
"I don't see the benefit of typed languages if I keep writing code as if it was PHP/JavaScript/Go" ... OF COURSE YOU DON'T!
This is missing most of the benefits, because the main benefits of a better type system isn't realized by writing the same code, the benefits are realized by writing code that leverages the new possibilities.
Another benefit of static typing is that it applies to other peoples' code and libraries, not only your own.
Being able to look at the signatures and bring certain about what some function _can't_ do is a benefit that untyped languages lack.
I think the failure of "optional" typing in Clojure is a very educational example in this regard.
The failure of newer languages to retrofit nullabillity information onto Java is another one.
I am sorry, but I don't really see how you stating more benefits of static typing really counters either of them.
I recommend reading the article again. But this time, try not to read it as defending a specific language (I only mentioned my blub language so that it's a more specific and extensive reference in the cases where I use it - if you are not using my blub language, you should really just ignore everything I write about it specifically) and more as trying to talk on a meta-level about how we discuss these things. Because your comment is an excellent example of how not to do it and the kind of argument that prompted me to this writeup in the first place.
The point is to explore a comparative difference in value, and that is realized through mastery of the tool, not merely living in a world where it exists.
The inverse is also true; you don't really get the benefits of dynamic typing until you start doing things differently to take advantage of that difference. If you still code like you're in a static language, you'll miss the benefits of a dynamic one.
Camp A: Languages with mediocre static typing facilities, for example:
-- C (weakly typed)
-- C++ (weakly typed in parts, plus over-complicated
type features)
-- TypeScript (the runtime is weakly typed,
because it's Javascript all the way down)
Camp B: Languages with mediocre dynamic typing facilities, for example: -- Javascript (weakly typed)
-- PHP 4/5 (weakly typed)
-- Python and Ruby (no powerful macro system to
help you keep complexity well under control
or take fulll advantage of dynamicism)
Both camps are not the best examples of static or dynamic typing. A good comparison would be between:Camp C: Languages with very good static typing facilities, for example:
-- Haskell
-- ML
-- F#
Camp D: Languages with very good dynamic typing facilities, for example: -- Common Lisp
-- Clojure
-- Scheme/Racket
-- Julia
-- Smalltalk
I think that as long as you stay in camp (A) or (B), you'll not be entirely satisfied, and you will get criticism from the other camp.What exactly does it mean to have "good dynamic typing facilities"?
To quote Peter Norvig on the difference between Python and Lisp, but you could apply it to most other mainstream dynamic languages vs Lisp :
> Python is more dynamic, does less error-checking. In Python you won't get any warnings for undefined functions or fields, or wrong number of arguments passed to a function, or most anything else at load time; you have to wait until run time. The commercial Lisp implementations will flag many of these as warnings; simpler implementations like clisp do not. The one place where Python is demonstrably more dangerous is when you do self.feild = 0 when you meant to type self.field = 0; the former will dynamically create a new field. The equivalent in Lisp, (setf (feild self) 0) will give you an error. On the other hand, accessing an undefined field will give you an error in both languages.
Common Lisp has a (somewhat) sound, standardized language definition, and competing compiler/JIT implementations that are much faster than anything that could ever possibly come from the Python camp because the latter is actually too dynamic and ill-defined ("Python is what CPython does") and making Python run fast while ensuring 100% compatibility with its existing ecosystem, without putting further restraints into the language, is akin to a mirage.
> What exactly does it mean to have "good dynamic typing facilities"?
The ability to change the structure of your program at runtime will be at the top of the list for me. You can't do that with Ruby/Python.
Picking Common Lisp as an example:
(NOTE: Some of the features are also present in good statically typed languages as well, so what I advocate is to use good, well-featured languages, not really static vs dynamic.)
(NOTE 2: I'm sorry for being such a fanboy, but that thing is addictive like a hard drug...)
0. Code is a first class citizen, and it can be handled just as well as any other type of data. See "macros" below.
1. The system is truly dynamic: Functions can be redefined while the code is running. Objects can change class to a newer version (if you want to), while the code is running.
2. The runtime is very strong with regards to types. It will not allow any type mismatch at all.
3. The error handling system is exemplary: Not only designed to "catch" errors, but also to apply a potential correction and try running the function again. This is known as "condition and restarts", and sadly is not present in many programming languages.
4. The object oriented system (CLOS) allows multiple dispatch. This sometimes allows producing very short, easy to understand code, without having to resort to workarounds. The circle-ellipse problem is solved really easily here. (Note: You can argue that CLOS is in truth a statically typed system, and this is partly true -- the method's class(es) need to be specified statically, but the rest of arguments can be dynamic.)
5. The macro system reduces boilerplate code to exactly zero. And also allows you to reduce the length of your code, or have very explicit (clear to read) code at the high-level. This brings down the level of complexity of your code, and thus makes it easier to manage. It also reduces the need for conventional refactoring, since macros can do much more powerful, automatic, transformations to the existing code.
6. The type system is extensive -- i am not forced to convert 10/3 to 3.333333, because 10/3 can stay as 10/3 (fractional data type). A function that in some cases should return a complex number, will then return the complex number, if that should be the answer, rather than causing an error or (worse) truncating the result to a real. Arbitrarly length numbers are supported, so numbers usually do not overflow or get truncated. (Factorial of 100) / (factorial of 99) gives me the correct result (100), instead of overflowing or losing precision (and thus giving a wrong result).
So you feel safe, because the system will assign your data the data type that suits it the best, and afterwards will not try to attempt any further conversion.
7. The type system is very flexible. For example, i can (optionally) specify that a function's input type shall be an integer between 1 and 10, and the runtime will enforce this restriction.
8. There is an extensive, solid namespace system, so functions and classes are located precisely and names don't conflict with other packages. Symbols can be exported explicitely. This makes managing big codebases much easier, because the frontier between local (private) code versus "code that gets used outside", can be made explicit and enforced.
9. Namespaces for functions and variables (and keywords, and classes) are separate, so they don't clash. Naming things is thus easier; this makes programming a bit more comfortable and code easier to read.
10. Documentation is built into the system - function and class documentation is part of the language standard.
11. Development is interactive. The runtime and compiler is a "living" thing in constant interaction with the user. This allows, for example, for the compiler to immediately tell you where the definition of function "x" is, or which code is using such function. Errors are very explicit and descriptive. Functions are compiled immediately after definition, and it can also be dissasembled (to machine language) with just a commmand.
12. Closures are available. And functions are first-class citizens.
13. Recursion can be used without too many worries -- most implementations allow tail call optimizations.
14. The language can be extended easily; the reader can also be extended if necessary, so new syntaxes can be introduced if you like. Then, they need to be explicitely enabled, of course.
15. There is a clear distinction between "read time", "compile time" and "run time", and you can write code that executes on any of those three times, as you need.
16. Function signatures can be expressed in many ways, including named parameters (which Python also has and IMO is a great way to avoid bugs regarding to wrong parameters / wrong passing order.)
Maybe it's simply hard to have both a good type system and a friendly learning curve?
To be honest, i love Common Lisp, it might be the most powerful programming language out there, but it's not easy to learn at all. In part because, being a truly multi-paradigm language, you should better make sure you are well versed in most programming paradigms first, otherwise you won't leverage the full power of Lisp. Not to mention the paradigm of meta-programming and DSLs, something that is usually new to programmers foreign to Lisp.
However, languages like Clojure and Smalltalk can be rather easy to learn, and they are fairly powerful.
Smalltalk was designed to be taught to kids!!
Ruby has some of the best meta-programming facilities out there. Yes you can't manipulate syntax in the same way as lisp, but the fact that all methods are message passing and first class blocks make tons of very powerful meta programming possible. Basic features that look otherwise first class are based on Ruby's meta programming facilities like `attr_reader` and friends. Funnily enough, the meta programming facilities of Ruby are precisely what turns a lot of people off. The wtfs per minute of using something like ActiveRecord is super high for people with only passing familiarity because there's so much that that's defined through Ruby's meta programming facilities.
Smalltalk has worlds better dynamic and metaprogramming.
That said, ruby does have a lot of power, but it's not of the same order as self/smalltalk etc.
Ruby has meta-programming facilities, but they pale compared to the easiness of doing meta-programming in Common Lisp. In Ruby, meta-programming is an advanced topic (see for example the implementation of the RoR ActiveRecord). In Lisp, meta-programming is your everyday bread&butter, and one of the first things a beginner learns. Because it isn't too different from regular programming!!
[The same comment applies, mostly, also to Clojure, Racket, Scheme, and the other Lisps]
You ain't gonna to find any sane way to combine macros with a powerful type system in a way the doesn't make a 140+ IQ a requirement for any programmer touching the code using these features in a real world project...
Problem with programming language design is that the ideal/Nirvana solutions lie at the edge, or beyond, the limits of human intellect. If you want something that can be learnt and understood with reasonable effort (like in not making "5+ years experience" a requirement for even basic productivity on an advanced codebase), you're going to have to compromise heaviliy! The most obvious ways to compromise are throwing away unlimited abstraction freedom (aka "macros"), or type systems.
Sorry to break it to ya, but we're merely humans, and not that smart...
I'm very well versed in Python (i've delivered two financial software systems done in Python, written entirely by yours truly). However its features and facilities pale in comparison to the languages i listed in camp "D".
While, yes, top-quality dynamic code will have documentation and test cases to make up for this deficiency, it's often still not good enough for me to get my answer without spelunking the source or StackOverflow.
I feel like I learned this the hard way over the years after having to deal with my own code. Without types, I spend nearly twice as long to familiarize myself with whatever atrocity I committed.
Can you give an or some example(s) of this?
The main use case of generics, making collections and datastructures convenient and readable, is more than enough to justify the feature in my view, since virtually all code deals with various kinds of "collections" almost all of the time. It's a very good place to spend a language's "complexity budget".
I wrote an appreciable amount of Go recently, with advice and reviews from several experienced Go users, and the experience pretty much cemented this view for me. An awful lot of energy was wasted memorizing various tricks and conventions to make do with loops, slices and maps where in other languages you'd just call a generic method. Simple concurrency patterns like a worker pool or a parallel map required many lines of error-prone channel boilerplate.
I feel the same way going from languages with HKTs back to Java/C#...
Not sure why you think they're not as useful, it sounds like you're making the same argument as OP but just moving the bar one notch over...
Subjectively, I use ordinary generics all the time, but see the need for HKTs only occasionally. It's entirely possible I'm not experienced enough to see most of their possible use cases, but then I'd wager most programmers aren't.
Neither Python nor Java programmers have to do that.
If Go is annoying with how little power it provides, that's fair, but other type systems can be just as annoying then, because when given the ability to, type astronauts will blast off into space, purely as a matter of honor or instinct.
Besides, code generation isn't all that bad. Java programmers will eventually find some kind of code generation in their build setup (serialization/schema tools).
In a hypothetical world where the designers never added the specific containers they did, you'd get a whole lot more value out of generics for containers. But it turns out, the designers used what seems on the surface like a kludge to get most of the benefits, while saving most of the cost. It's a perfect embodiment of the kinds of tradeoffs I'm talking about.
Architecture astronauting can be prevented with best practices and code review, not with language limitations. It’s a fools errand to try, code generation allows you to get all the complexity and more of generics.
I have come to see type systems, like many pieces of computer science, can either be viewed as a math/research problem (in which generally more types = better) or as an engineering challenge, in which you're more concerned with understanding and balancing tradeoffs (bugs / velocity / ease of use / etc., as described in the post). These two mindsets are at odds and generally talk past each other because they don't fundamentally agree on which values are more important (like the great startups vs NASA example at the end).
Though I am not a type theorist (I only dabble in compilers and language design), I have noted that many people conflate static typing and dynamic typing with other additional ideas.
Static typing has certain benefits but also has certain disadvantages, dynamic typing has certain benefits but also has certain disadvantages.
What I find interesting is that few people fall into the soft typing arena, using static typing where applicable and advantageous and using dynamic typing where applicable and advantageous.
Static typing has a tendency in many languages to explode the amount of code required to get anything done, dynamic typing has a tendency to produce somewhat brittle code that will only be discovered at runtime. The implementation of static typing in many languages requires extensive type annotation which can be problematic.
But what is forgotten by most is that static typing is a dynamic runtime typing situation for the compiler even when the compiler is written in a static typed language.
Instead of falling into either camp, we need to develop languages that give us the beast of both world. Many of the features people here have raised as being a part of the static typing framework have been rightly pointed out as being of part of the language editors being used and are not specifically part of the static typing regime.
Many years ago a similar discussion was held on Lambda-the-Ultimate, and the sensible heads came to the conclusion that soft typing was the best goal to head for. Yet, in the intervening years,when watching language design aficionados at work, they head towards full static typing or full dynamic typing and rarely head in the direction of soft typing (taking advantage of both worlds).
S, the upshot, this discussion will continue to repeat itself for the foreseeable future and there will continue to NOT be a meeting of minds over the subject.
Then there's the whole tooling aspect of trying to mix type systems. It's different lifestyles. Dynamic programmers aren't going to start compiling their code to run it, static programmers aren't going to switch to a language with weaker tooling around the IDE-ish features, which are mostly built on the type system.
My conclusion is this: New languages should all be statically typed, because we shouldn't need new languages at all. We should be fine. The reason we need new languages at all, is because the trifecta of C++/Java/C# basically encompassed the entire statically typed world, but they're all infected with this fully overblown OOP obsession, and the null pointer bug--which newer languages have fixed, through more static typing. Basically we need to replace those languages with similar ones and then just stop making languages for a few decades, until whatever we're doing now looks as dumb as OOP and null pointers. In the long run, Go/Swift/Kotlin/Rust will take over the statically typed world and it's going to be great.
A simple example of this is a list. Now in statically typed languages, list are homogeneous (this is includes type unions). In dynamically typed languages, list can be heterogeneous, essentially anything can be added at runtime.
In soft typing, we can indicate that a list is homogeneous and the compiler will ensure that this is true or we can specify no type checking (as such) and this will be done at runtime.
Contrived yes, but I regularly use other aggregates (tables and sets) into which I do not want them to homogeneous.
One of the aspects that I like about functional languages is the polymorphism available, but in all that I have come across, there is no way to make a tree or list heterogeneous without declaring union types before hand.
My problem with C#, C++, Java, and their ilk, is that code is multiplied with their generics.
How the IDE and compiler and type systems interact is a design function and is not inherent to any type system.
One of the reasons I don't use specific main stream languages such as C#, C++ or JAVA is that they don't provide the specific programming features that I desire.
I have looked at Go, Swift and Rust and I am not at all impressed by the "relative stupidities" within those languages. For other programmers, what they consider to "relative stupidities" is entirely up to their experience and outlook.
Start-ups decide not to write MVPs in languages like Haskell or Idris not because those languages aren't "rapid" enough, but because it's too difficult to find programmers experienced in those languages on the labor market. It's already difficult enough to find competent programmers - no founder wants to make their hiring woes even more difficult.
You write "Why then is it, that we don't all code in Idris, Agda or a similarly strict language?... The answer, of course, is that static typing has a cost and that there is no free lunch."
I take it that you wrote "of course" here through assuming that there must be some objective reason for the choice, and that it depends solely on strictness, but languages don't differ only in their strictness, so choices may be made objectively on the basis of their other differences, and we also know that choices are sometimes made on subjective or extrinsic grounds, such as familiarity. I don't know what proportion of professional programmers are familiar enough with Iris or Agda to be able to judge the value proposition of their strictness, but I would guess that it is rather small.
Now, to look at the sentences I elided in the above quote: "Sure, the graph above is suggestively drawn to taper off, but it's still monotonically increasing. You'd think that this implies more is better." As the graph is speculative, it cannot really be presented as evidence for the proposition you are making. I could just as well speculate that static program checking does not do much for program reliability until you are checking almost every aspect of program behavior, and that simple syntactical type checking is of limited value. That would be consistent with the fact that there is little empirical evidence for the benefit of this sort of checking, and explain why most people aren't motivated to take a close look at Iris or Agda. In this equally-speculative view of things, current language choices don't necessarily represent a global optimization, but might be due to a valley of much more work for little benefit between the status quo and the world of extensive-but-expensive static checking.
I've been thinking about the trajectory of C++ language development recently and the emphasis has definitely been on making generics more and powerful. You watch CppCon talks and see all this super expressive template spaghetti and see that while it's definitely a better way to write code - the syntax is just horrifying and hard to "get over"
Just like when "auto" took off and people starting thinking about having "const by default" - I'm starting to think that generic by default is the way to go. The composability of generic code is incredible powerful and needs to be more accessible
However the other end of the spectrum: dynamic code leaves a lot of performance on the table and leads to runtime errors
Especially when it comes to GUI programming, I really don't care if a BlueButton.Click() got called instead of RedButton.Click().
So year, static typing doesn't buy you much, but in some languages it's at least cheap.
I think this is key. The benefit of static typing isn't that they provide safety, it's that they provide _low-cost_ safety. For a large class of problems, types are cheaper than tests are. For other classes, tests are cheaper than types. The main downside of nonstatic languages is that you have to use tests for everything, even that class where types are a better choice.
Or, type can be specified when setting the variable:
[String]$myString = "Hello World!"
This would generate a type error:
[Int]$myString = "Hello World!"
Often, typed and untyped variables will sit together:
[Int]$EmployeeID,[String]$FullName,$Address = $Input -split ","
[xml]$someXmlDocument = Get-Content "path\to\file.xml"
And you get a deserialized version of the XML text.Also the fact that you can use types when declaring function arguments, removing the need to manually test if an object of the desired type was passed.
Powershell definitely strikes a good balance on type safety for a scripting language.
If no, then what is the use of the typesystem?
If yes, isn't that cumbersome, since I suppose most library functions have typed arguments?
Now this codebase was written with a high degree of quality (it's pretty good but not perfect), but the lack of compile (and of course runtime)-time checks has caused waste.
The second phase of my project to convert all promises to RX Observables :)
Pity you! I fear such tasks.
As mentioned,take advantage of async/await. Also, make sure you wrap everything in modules and access from outside through module exports.
And I disagree with the barrier to entry argument. Static typing, by enabling rich tooling, helps a beginner (like it helped me) a lot more by giving live feedback on your code, telling you immediately where you have a problem and why, telling you through a drop down what other options are available from there, etc. Basically makes the language way more self-discoverable than having to RTFM to figure out what you can do on a class.
For four days, I spent debugging a python production script because in one place I had typo'd ".recived=true" on an object and just couldn't understand why my state machine wouldn't work.
And very quickly, the whole team became fans of __slots__ in Python.
I still write 90% of my useful code in python, but that one week of debugging was exhausting & basically wouldn't have even compiled in a statically declared language. Even in python, the error is at runtime, after I got the __slots__ in place.
Not just, "who the hell uses this", but "where the hell is this defined" as well.
You would think by looking at the code that the creators had a 30 word vocabulary, because 80% of the code uses the same six nouns and four verbs to pass data around and what you use those for depends on the context of who is calling it.
Oh, but the entire thing is written using promises, so most of your function calls have no context. It's hell, and I'm starting to worry that Node has dug itself a reputation hole it will never get out of.
I think the best thing I've found for this personally is coccigrep, which works but I've only used it a couple of times. I'd like something I'd reach for about as often as I reach for grep. (Also I think these days you'd want it to be based on clang or something.)
One thing that does seem to be true is that the requirement to name types means that a textual grep is way more reliable than it is in C. If I want to find all places where a Python class is used in a large codebase, I might as well give up.
People were complaining about static typing for the dumbest reasons (it's too 'wordy'). It started as a backlash against old-school enterprise Java development (which was fair, EJB2 world sucked) but then it went completely off-the-rails. Typing in Java could be better, sure, but even with its quirks it's way better than the nothing you get with dynamic languages. There are a class of bugs you just never need to worry about when the compiler does some compile-time checks for you ... like worrying that you passed the wrong type into a function, or the wrong number of arguments.
Thank God people are coming to their senses.
In my experience, the cost of static typing feels roughly constant per line of code, while the benefits of static typing feel roughly O(N log N) in lines of code or O(N) in number of cross-type interactions. These are just wild guesses based on gut feelings, but they feel about right. The constant factors are affected by individual coder preference, experience, and ability, but also specifics of thy type systems involved and the strength of type inference in the tools being used.
In any case, I think often times dynamic typing proponents and static typing proponents have vastly different notions of what a large code base is, or at least the size code bases they typically use.
One problem is that many code bases start out where the advantages of static typing aren't readily apparent, but re-factoring into a statically typed language is often not realistic, even/especially when a project starts groaning under its own weight.
I'd love to see more mainstream use of gradual typing/optional typing/hybrid typing languages, especially something like a statically typed compiled subset of a dynamically typed interpreted language, where people could re-factor portions of their quick-and-dirty prototypes into nice statically typed libraries.
I disagree; that Java was the standard example of statically typed languages is what convinced me for a long time that I didn't want anything to do with it. Having to pollute my code with all that crap, deal with a lot of dumb restrictions and still have NPEs left me with a sour taste.
Only after I discovered Haskell (and more recently Idris), did I realize that static typing can actually be worthwhile.
There are no senses to come to. Static and dynamic typing each have their own benefits, and there are genuine tradeoffs to choosing one over the other. That we are even having this debate in 2017 shows that the world of typing is not a "solved problem" and there are still good reasons to use one over the other for various reasons.
There is a different problem with the more powerful and useful type systems in more modern statically typed languages though: learning curve. Haskell is dysfunctionally hard to learn and other languages do a little bit better but there's still friction in the learning curve that gets in the way of widespread adoption in projects that want to be able to hire rapidly.
This is the #1 reason I leaned away from static typing. Typescript and Swift have changed my mind. I now see typing as a helpful tool that can solve a lot of problems.
People always say this and it baffles me. Bugs like that should be caught immediately by your test cases. You shouldn't rely on the compiler to catch them for you.
In general though, the more you can formally reason about the program, the more you can automate program transformations (refactoring). Programmers in dynamic languages will argue that the amount of code is far less than the equivalent code in a mainstream statically-typed language, so while the cost of refactoring may be higher per unit, the number of units is less, so the overall cost is the same (or less).
I believe in using the best tool for the job...some use cases would benefit more from static typing, while others would benefit more from using a dynamic language. One of the most important factors is the team and its engineers' backgrounds, preferences, styles, etc.
There are no diminishing returns. Defining types is easy and enhances code readability.
Large Python code bases are really hard to understand and work in (here).
Of course there are cases where dynamic languages do worse, but it balances out I think.
I always thought dynamic typing is a feature for situations where the code needs an extreme amount of flexibility to adapt to a wide variety of data; even at the expense of performance.
This isn't to say that static typing isn't good at this. Just, even with that, there is a lot of effort that goes into making the rich tooling. A lot of very smart and capable folks work hard to make Visual Studio.
vscode seems to figure out the types in javascript without any static typing.
>But it is great for refactoring
Searching for strings isn't that much worse. Also, when it comes to web development, you cross into the client-side and suddenly you can't refactor. So you can only refactor the server-side and end up with a mismatch.
>finding all references to a function or a property or navigating through the code at design time
You can do that without static typing in many cases as well.
>Basically all the features visual studio excels at for .net languages.
When I was working in c# on the server and javascript on the client, I really hated having to go back into c#.
>telling you through a drop down what other options are available from there
vscode seems to be able to figure this out most of the time as well.
I think static typing is necessary when you need performance because all of the fast languages are statically typed.
I think a lot of these things are about organisational complexity and making sure new and average programmers don't screw up the software. It is about large companies trying to manage their organisation, it isn't about the complexity of the code itself.
There are a ridiculous amount of tech companies that have used dynamic languages to go from nothing to the biggest companies in the world and only switched to static languages well and truly after that occurred.
I use Cursive https://cursive-ide.com/ for working with Clojure, and it can do safe refactoring for symbols by doing static analysis of the source. It can show all usages of a symbol, rename it, do automatic imports, and so on.
Another piece of tooling that's not available in any statically typed languages at the moment is REPL integration with the editor seen here http://vvvvalvalval.github.io/posts/what-makes-a-good-repl.h...
I find that the REPL driven workflow found in Lisps is simply unmatched. When you have tight integration between the editor and the application runtime, you can run any code you write within the context of the application immediately. This means that you never have to keep a lot of context in your head when you're working with the application. You always know what the code is doing because you can always run and inspect it.
Having the runtime available during development gives you feedback much faster than the compile/run cycle. I write a function, and I can run it immediately within the context of my application. I can see exactly what it's doing and why.
The main cost of static typing is that it restricts the ways you can express yourself. You're limited to the set of statements that can be verified by the type checker. This is necessarily a subset of all valid statements you could make in a dynamic language.
Finally, dynamic languages use different approaches to provide specification that have different trade offs from static typing. For example, Clojure has Spec that's used to provide runtime contracts. Just like static typing, Spec provides a specification for what the function should be doing, and it can be used to help guide the solution as seen here https://www.anthony-galea.com/blog/post/hello-parking-garage...
Spec also allows trivially specifying properties that are either difficult or impossible to encode using most type systems. Consider the sort function as an example. The constraints I care about are the following: I want to know that the elements are in their sorted order, and that the same elements that were passed in as arguments are returned as the result.
Typing it to demonstrate semantic correctness is impossible using most type systems. However, I can trivially do a runtime verification for it using Spec:
(s/def ::sortable (s/coll-of number?))
(s/def ::sorted #(or (empty? %) (apply <= %)))
(s/fdef mysort
:args (s/cat :s ::sortable)
:ret ::sorted
:fn (fn [{:keys [args ret]}]
(and (= (count ret)
(-> args :s count))
(empty?
(difference
(-> args :s set)
(set ret))))))
The above code ensures that the function is doing exactly what was intended and provides me with a useful specification. Just like types I can use Spec to derive the solution, but unlike types I don't have to fight with it when I'm still not sure what the shape of the solution is going to be.I think the key is not to confuse both approaches and leverage the strengths of each to the max.
Disclaimer: Python user scarred by email header RFC violations
I think Go with its lack of algebraic type is more of the first, helping the compiler, so I wouldn’t use it as a good example of static typing.
Haskell, OCaml and Rust would make excellent case studies, but we have nothing to compare against.
So IMHO the best way to compare static typing vs dynamic typing is by comparing Typescript against JS. And in my experience the difference when writing code is huge. It completely eliminates the code-try-fix cycle during development.
This is a basic intuition behind all good practices, including CI, QA, etc.
Types allow one to discover program defects (even generalized ones, when using some of the programming languages) in (almost) shortest possible amount of time.
Types also allows one to constrain effects of various kind (again, use good language for this), which constraintment can make code simpler, safer and, in the end, more performant.
I love everything about Swift except the compile times and occasionally inscrutable compile error messages.
I love the interactivity of Javascript, but despise the lack of types, it's like I'm sketching out the idea for a program instead of directly defining what it is. And the lack of types burns me occasionally.
So the trade off is: static typing gives you more compile-time certainty, but at a cost of spending more time developing your code. Dynamic typing gets you to a working product or prototype typically much much faster, but with added run-time debugging.
Each has its benefits and costs.
In my experience, there is no doubt that dynamically typed languages are faster-to-production than statically-typed. This doesn't mean that I don't admire static typing, though, because most developers appreciate some degree of purity in their work.
https://github.com/fthomas/refined
not only for the static checking,
scala> val i: Int Refined Positive = -5
<console>:22: error: Predicate failed: (-5 > 0).
val i: Int Refined Positive = -5
but the expressive descriptions of a domain model.All static typing means is that type information exists at compile time. All dynamic typing means is that type information exists at runtime. You generally need _at least_ one of the two, and the benefits each gives you is partially hobbled by the drawbacks of the other, so most dynamic languages choose not to have static typing. I also feel that dynamic languages don't really lean into dynamic typing benefits, though, which is why this becomes more "static versus no static".
One example of leaning in: J allows for some absolutely crazy array transformations. I don't really see how it could be easily statically-typed without losing almost all of its benefits.
The key is balance. Pure static does create a lot of extra up front cruft at the expense of long term safety. Pure dynamic does create a much faster path to features at the expense a lot of long term confusion.
The reason we have this conversation is because of web applications where everything is travelling over the wire as a string, consumed by the web server as a string, converted by whatever language the server is in...into something that it can use...9/10 times validated to make sure it reflects what we need and then stuff into a database.
In the case that you're using a SQL database, a huge number of people are enforcing types at the database layer and the validation layer. Since so much is "consume and store" followed by "read and return" the types at that server layer end up creating a ton of extra work that in many cases shows little to no benefit.
At the point that you're doing more in server layer, suddenly it becomes a lot more useful. At the point you're working on desktop, mobile, embedded, console, computational and graphics...static is going to provided more value.
At the point you're working on web in front of a database, the value is much more questionable.
This is really one of the reasons I'm such a huge Elixir fan because IMO it strikes that perfect balance where I live...on the server in front of a database. You get static basic types with automatic checking via dialyzer and you can make it stricter as necessary.
In such a case, the line between these two type environments narrows.
> In such a case, the line between these two type environments narrows.
Not really. Static types still offer you total proofs of the properties you encode as types, not just experimental results of tests.
> "Go reaps probably upwards of 90% of the benefits you can get from static typing"
That 90% number is totally made up as well. I don't see evidence that the author actually worked with Haskell, or Idris, or Agda these being the three static languages mentioned. Article is basically hyperbole.
If I am to pull numbers out of my ass, I would say that Go reaps only 10% of the benefits you get with static typing. This is an educated guess, because:
1. it gives you no way to turn a type name into a value (i.e. what you get with type classes or implicit parameters), therefore many abstractions are out of reach
2. no generics means you can't abstract over higher order functions without dropping all notions of type safety
3. goes without saying that it has no higher kinded types, meaning that expressing abstractions over M[_] containers is impossible even with code generation
So there are many abstractions that Go cannot express because you lose all type safety, therefore developers simply don't express those abstractions, resorting to copy/pasting and writing the same freaking for-loop over and over again.
This is a perfect example of the Blub paradox btw. The author cannot imagine the abstractions that are impossible in Go, therefore he reaches the conclusion that the instances in which Go code succumbs to interface{} usage are acceptable.
> "It requires more upfront investment in thinking about the correct types."
This is in general a myth. In dynamic languages you still think about the shape of the data all the time, except that you can't write it down, you don't have a compiler to check it for you, you don't have an IDE to help you, so you have to load it in your head and keep it there, which is a real PITA.
Of course, in OOP languages with manifest typing (e.g. Java, C#) you don't get full type inference, which does make you think about type names. But those are lesser languages, just like Go and if you want to see what a static type system can do, then the minimum should be Haskell or OCaml.
> "It increases compile times and thus the change-compile-test-repeat cycle."
This is true, but irrelevant.
With a good static language you don't need to test that often. With a good static type system you get certain guarantees, increasing your confidence in the process.
With a dynamic language you really, really need to run your code often, because remember, the shape of the data and the APIs are all in your head, there's no compiler to help, so you need to validate that what you have in your head is valid, for each new line of code.
In other words this is an unfair comparison. With a good static language you really don't need to run the code that often.
> "It makes for a steeper learning curve."
The actual learning is in fact the same, the curve might be steeper, but that's only because with dynamic languages people end up being superficial about the way they work, leading to more defects and effort.
In the long run with a dynamic language you have to learn best practices, patterns, etc. things that you don't necessarily need with a static type system because you don't have the same potential for shooting yourself in the foot.
> "And more often than we like to admit, the error messages a compiler will give us will decline in usefulness as the power of a type system increases."
This is absolutely false, the more static guarantees a type system provides, the more compile time errors you get, and a compile time error will happen where the mistake is actually made, whereas a runtime error can happen far away, like a freaking butterfly effect, sometimes in production instead of crashing your build. So whenever you have the choice, always choose compile-time errors.
Now if we are to accept all of this, that opens up a different question: If we are indeed searching for that sweet spot, how do we explain the vast differences in strength of type systems that we use in practice? The answer of course is simple (and I'm sure many of you have already typed it up in an angry response). The curves I drew above are completely made up. Given how hard it is to do empirical research in this space and to actually quantify the measures I used here, it stands to reason that their shape is very much up for interpretation.
The line charts are there to illustrate the point, not as proof. Like not all arguments are axiomatic proofs, not all charts are plotting data.
>That 90% number is totally made up as well of course.
Yeah, we got that from reading TFA already.
>This is a perfect example of the Blub paradox btw. The author cannot imagine the abstractions that are impossible in Go, therefore he reaches the conclusion that the instances in which Go code succumbs to interface{} usage are acceptable
The author actually not only can imagine them, but plots them (e.g. how for some languages/uses cases the sweet spot will be 100% type help from the language), and explains why he thinks that interface{} can be acceptable in some cases.
>This is true, but irrelevant.
Irrelevant for you maybe. For others (and for prototyping/early exploratory use cases in general) the quick feedback cycle beats the guarantees from Haskell like types. See Bret Victor.
>In the long run with a dynamic language you have to learn best practices, patterns, etc. things that you don't necessarily need with a static type system because you don't have the same potential for shooting yourself in the foot.
I'd say that most people's experiences with typed languages like C++, Java, C# etc run counter to that. Most that I've seen anyway. Same, or even more so, for Haskell -- there are literally tons of stuff to learn, to the point it throws people off.
Wouldn't it be great if we can use the computer to figure out what the types should be by a runtime evaluation of the code and save precious human time for things only humans can do?
I don't have to think or decorate my speech with types of noun, verb, pronoun, adjective etc. when I speak, but I'm still able to communicate very effectively, because your brain is automatically adding the correct type information based on context that helps you understand what I'm saying, even with words that have multiple types. Granted, natural language is different than programming language but there was once a trend to try and make programming languages more like human language, not less so.
How is that? I'm not seeing the increased utility.
> It's better to trade a fast and quick runtime type error....
What if the runtime type error crashes your app in production and loses your company money? What if it's something that slipped through your end-to-end integration testing because certain unlikely conditions never got covered, but they happened in production?
> ... than a lengthy compile-time type checking process,...
There are several modern compilers which are quite fast: D, OCaml, Java.
> ... because less code needs to be evaluated at run-time to expose the type error.
With static type checking, no code needs to be evaluated at runtime to expose a type error. Does dynamic typechecking offer a reduction over that?
> Wouldn't it be great if we can use the computer to figure out what the types should be by a runtime evaluation of the code and save precious human time for things only humans can do?
Wouldn't it be great if the computer would figure out the types at compile time and save us from having to manually input them? Well, the computer can do that, thanks to type inference. Several popular languages offer full, powerful type inference.
Software failures are failures of understanding, and of imagination.
The problem is that programmers are having a hard time keeping up with their own creations.
dynamic typing simply doesn't scale.
I would not consider a language to be modern unless it has Type Providers I consider this to be such an essential feature. I believe Idris and F# are the only languages that have it. People are trying to push TypeScript to add it - who knows if it will happen.
Many are saying that if you have a dynamic language you just need to be disciplined and write many tests. With good static typed languages like F# you can't even write tests on certain business logic since the way you write your code you make "impossible states impossible", see https://www.youtube.com/watch?v=IcgmSRJHu_8
1. performance dominates (like 80:20)
2. tooling
3. doc (becomes crucial on large projects)
4. correctness
Formal correctness doesn't really matter. Anecdotally (since that's really all we have), I find in practice, very few bugs are caught by the type-checker.Further, code is usually not typed as accurately as the language allows. i.e. the degree of type-checking is a function of the code; the language only provides a maximum. In a sense, every value has a type, even if it's not formally specified or even considered by the programmer, in the same sense that every program has a formal specification, even if it's not formally specified.
Upfront design is the price. Which is difficult to pay when the requirements are changing and/or not yet known.
By adding types (and in the extreme, dependent types), you're allowing compiler to prove more things about the code (to check correctness or generate more optimal code). If you actually need to prove more things, then it's better to leave that for a compiler rather than human.
Of course, if you're writing e.g. web scraping script, you don't need these guarantees and then you don't have to care about types. But the better engineering you want, the more static typing will help and there is no diminishing returns.
It makes the higher level types seem more transcendental than they are, and also seems to put actual validation on a second rate level. End of the day if an argument is the right scalar or interface you'll get the same result on runtime whether you hinted it -- for one's quality of life improvements -- or checked it with some boilerplate validation. Worst case scenario people will forgo encoding known stricter constraints after generally hinting the expected type.
The state of the art is not up to proving every desirable property of every program that we would like to build. But that has nothing much to do with computability. And some extremely impressive things have been done, like the seL4 separation kernel, which has static proofs of, among other things, confidentiality, integrity, and timeliness, and a proof that its binary code is a correct translation of its source.
What’s old is new again, though one can hardly imagine cat-v touting the merits of Java.
> upfront investment in thinking about the correct types
being a cost. Surely you have to do this whether the compiler will check your work or not, and if you just don't do the thinking you'll end up with bugs? Isn't this a benefit?
Just ONE study, so don't take too much heed. That said, apparently:
* Strongly type, statically compiled, functional, and managed memory is least buggy
* perl is REVERSELY correlated with bugs. Interestingly, Python is positively correlated with bug. There goes the theory about how Python code looks like running pseudo-code... Snake (python's, to be more precise) oil?
* Interestingly, unmanaged memory languages (C/C++) has high association with bugs across the board, rather than just memory bugs.
* Erlang and Go are more prone to concurrency bugs than Javascript ¯\_(ツ)_/¯. Lesson: if you ain't gonna do something well, just ban it.
All in all, interesting paper.
(I'm not talking about systems which just infer types automatically).
Can someone explain this?
A type system doesn't only describe the behavior of the program you write. It also informs you of how to write a program that does what you want. That's why functional programming pairs so well with static typing, and in my opinion why typed functional languages are gaining more traction than lisp.
How many ways are there to do something in lisp? Pose a feature request to 10 lispers and they'll come back with 11 macros. God knows how those macros compose together. On the other hand, once you have a good abstraction in ML or Haskell it's probably adhering to some simple, composable idea which can be reused again and again. In lisp, it's not so easy.
A static type system that's typing an inexpressive programming construct is kind of a pain because it just gets in the way of whatever simple thing you're trying to do. A powerful programming construct without a type system is difficult to compose because the user will have to understand its dynamics with no help from the compiler and no logical framework in which to reason about the construct.
So, a static type system should be molded to fit the power of what it's typing.
The fact that every Go programmer I talk to has something to say about their company's boilerplate factory for getting around the lack of generics tells me something. This is only a matter of taste to a point. In mathematics there are a vast possibility of abstract concepts that could be studied, but very few are. It's because there's some difficult to grasp idea of what is good, natural mathematics. The same is in programming: there are a panoply of programming constructs that could be devised, but only some of them are worth investigating. Furthermore, for every programming construct you can think of there's only going to be a relatively small set of natural type systems for it in the whole space of possible type systems.
Generics are a natural type system for interfaces. The idea that interfaces can be abstracted over certain constituents is powerful even if your compiler doesn't support it. If it doesn't, it just means that you have to write your own automated tools for working with generics. It's not pretty.
The catch there, as is often the case, is hidden in the word "good". Working with text data in Haskell is almost as painful as working with text data in C++, and for much the same reason: the original abstraction is far from ideal for most practical purposes, but became the least common denominator. Everyone and his brother has written a better string abstraction or more powerful regex library or whatever since then, but they're all different.
Consequently, even with the power of generics or typeclasses, you still often see developers just converting to and from the primitive default representation for interoperability. Static typing will at least stop you from screwing that up, which certainly is an advantage over dynamically-typed languages in some situations. However, it apparently hasn't made it any easier for the developer community as a whole to migrate to a better abstraction as the default.
In short, we often don't know what will turn out to be a good abstraction until we've gained a lot of experience, and in the face of changing requirements on most projects, we probably never can know from the start because what works as a useful abstraction might change over time. So while types are useful for checking whatever abstractions we have at any given time, until we've also got techniques for migrating from one to another much more smoothly and on much larger scales than anything I've yet encountered, I think we shouldn't oversell the benefits, particularly in terms of composability.
What a ridiculous stereotype. Clojure community typically maintains the belief that macros are the last resort for things that genuinely justify them. You really shouldn't spread hyperbole like this.
There are languages that enforce termination. They only accept programs that can be shown to terminate through syntactic reasoning (e.g., when processing lists, you only recurse on the tail), or where you can prove termination by other means.
Coq is like this, as is Isabelle, as is F* , as are others. They also provide different kinds of escape hatches if you really want non-terminating things, like processing infinite streams.
This "we can never be sure of anything, because the halting problem" meme is getting boring. Yes, you cannot write the Collatz function in Coq. No, that is not a limitation in the real world.
Many. That's the whole point -- to let you choose "the way to do something" that applies the best to your circumstances (development time, performance, allowable complexity, etc.)
So you are limited by your own mind and skills -- not by the language.
To me, it's right tool for the right job. I have no problem spinning up a static language for performance and outsourcing the scripting to a dynamic language like Python for the best of both worlds in terms of speed, and rapid development.
That's not really true, just a belief. I give you an example to start understanding these things: the exact same program written in a very high level and very expressive language, like Perl, instead of Go, is going to have at least 3 times less code and since defect rates per line of code are comparable, you would end up with at least 3 times less bugs. Suddenly reliability argument of static typing doesn't make any sense. That's because in PL research there is a huge gap in understanding of how programmers actually think.
And I'm not sure you should expect the number of bugs per line to remain constant across languages. Extra lines required because you have to do your indexing by hand as you're iterating over a list certain increases the chances of an error but the extra '}' required to end the block in some languages increases line count with very little chance of causing an error.
I was in favour of dynamic typed, but lean more and more towards static typing, like ocaml.
Given a million line codebase written in Perl vs a three million line codebase written in Go, which do you think most engineers would prefer?
That's not really true, just a belief. A naive belief, if you ask me.