> The older I get, the more I appreciate dynamic languages.
Exactly the opposite for me. I just can't stand hovering a variable or a parameter and not getting its exact type, or typing "." after a variable and not having my editor gives me all the available methods on that variable, or running my code just to discover that it instantly crashes because I made a typo or forgot an argument or passed the wrong argument or tried to call a method that doesn't exist on that variable or whatever other issues that happens only with dynamic languages. What a waste of my time.
As I said in another ranty response in this thread, I do not understand how people enjoy spending time debugging trivial issues, that even a simple static type system would just plain tell them at compile time.
I've been programming for over 20 years too, and I like dynamic languages. I like them a lot more when they're properly tested and well architected, but even the tire fire codebases are at least debuggable. The compiled stuff helps with types catching the trivial bugs, yes, but it's way too complicated to quickly debug things like seg faults. Dynamic languages let you introspect and modify things way more easily, and this makes things like fakes and mocks for testing way easier. It makes debugging easier. And not having to wait an hour for something to compile is nice.
That said, I love the speed of compiled languages. I once converted a simulation from Python to Cython and saw a 10000x performance boost because of CPU caches and all that. Usually the gains are closer to 10x to 20x, but in some rare moments it's like a rocket ship vs a hang glider.
EDIT: Reading again, I think you are comparing interpreted with compiled languages, not so much static with dynamic type systems.
Seg faults are a prime argument for static typing. The more static and stricter the type system, the less things like seg faults can even happen. Compare Rust to C (both are static, but one more than the other), and at the extreme end handwritten assembly (extremely dynamic). Those are languages used by kernel developers, where errant memory accesses of all kinds are a constant concern.
I don't know why dynamic languages would make introspecting things easier, or debugging in general. I agree that mocking can be easier with dynamic types. Compilation rarely takes long nowadays (incrementally it's usually just a few seconds), so the time saved in knowing that the code is still correct at least within the confines of the type system is well worth it.
As an kernel developer, would you want to take some of the many shell scripts that the kernel has and rewrite them in C?
Different tools for different purpose. Unless there ecosystem was really designed for it, I would not write a driver in a dynamic language. At the same time, I would prefer not to write all the bootstrap scripts during booting in C. If all I am doing is calling other programs, I use shell. If all I am doing is calling a bunch of low level system calls in a restricted environment I would use C. If I am doing a bunch of string editing, process flow management, data compiling with some calls to external programs, I would use a dynamic langue like Python.
I want add a personal opinion in regard to C. Every function gives out an return code which is a kind of "type" that does not get enforced by the compiler. The return code is defined by the manual page and it is up to the programmer to catch it and react correctly to it. If the wrong code occur and the program explode during runtime its the fault of the programmer for not write a program that manage the return code. I would claim that the wast majority of crashes that occur in programs written in C is because programmers failed to realize the full list of possible return codes and what they mean. Here I do prefer dynamic languages because they usually do not leave it up to the manual to defined what return code -42 means compared to -41, and debugging errors when the errors themselves have class names and inheritance tend to be a bit easier in my experience.
The kernel itself does not consist of any shell scripts. It may have them for building the kernel, but just like the shell scripts at boot, the problems solved there are much simpler (mostly call compiler and linker on a set of files). So I agree: For simple high level problems a dynamic language is sufficient.
As to your second paragraph, I do agree that C has a vastly insufficient type system from the 70s (even though I think better type systems were already available at the time, but the inventors of C might not have known or cared about that). Rust solves the problem you described, and what you complain about is actually that errors in C tend to be represented not statically enough.
That is an interesting aspect about rust I have not heard, and since over 50% of my C code tend to be about managing return codes with proper tear down, rust suddenly do look a bit more interesting. However doing a surface look, it seems that rust simply terminate the program when encountering some of the more critical return codes from syscalls, which does exactly feel like it solves the problem. I guess it is also a reason why the kernel might not switch to using rust any time soon, as oom should not cause drivers to just terminate hard. From my surface look, it also seems rust simply uses Result<> where in C you would store the value in an int, and both leaves it up to the user to read the manual and interpret the number into actually meaning. Of course I could be wrong.
In a way it also demonstrate a other line between when I would use shell, Python or C. With shell everything either existed OK or did not, and the return data is always strings, and so the programs written there is built with that in mind. With python I work with structured data, but I don't spend much work or thought on syscalls and what state the machine is in. With C, syscalls management and the state of the machine is the majority of the code. As such one pick the language based on what the code will be focusing on. Dynamic vs static basically becomes a shorthand for that focus.
What's a seg fault? I jest, but static languages have come incredibly far since C++ (where they are already less common than in C) and I truly haven't dealt with a segmentation fault in the past many years working with static languages.
Haskell, Kotlin, Scala 3 (with a compiler flag) will all remove null from the set of values acceptable by type. (There are others as well) So a String can’t be null ever, you have to do ‘Maybe String’, ‘String?’ or ‘String | Null’ as a type respectively.
If this is what you are asking.
> but it's way too complicated to quickly debug things like seg faults
I know you've listed the common argument for static vs dynamic (dynamic -> so fast to code but slow to run, static -> way too complicated)but after a decade in SE I still have yet to see some good evidence of this.
Yes some static languages (like Java) will make developing certain things slower vs JS but is Java a good statically typed language ? Maybe these statements are "true" today with the current implementation of one or the other but there are a lot of languages that I just can't see getting in the way.
A new example of this: Kotlin and Swift are statically typed and I would love love to see where it slows these mythical developers that are so fast in a dynamic language but would be slowed down using them. There's obviously going to be a cost for the actual compile time but that should be minimal.
Unfortunately I'm starting to believe that this is just another case of certain developers are used to certain languages.
The trend of the JS move to TS also points to this. Basically JS looks very similar to Kotlin and Swift (TS is basically identical).
To look at your specifics
> Dynamic languages let you introspect and modify things way more easily, and this makes things like fakes and mocks for testing way easier. It makes debugging easier. And not having to wait an hour for something to compile is nice.
> Dynamic languages let you introspect and modify things way more easily
In what way ?
> and this makes things like fakes and mocks for testing way easier
Fwiw this is what that fake/mocks look like for a static language
`val x = mock<User>()`
To be fair that's using a library and maybe that's part of your criteria ?
> It makes debugging easier
? how, I can see the argument for the other way (one less thing the developer has to worry about - typing issues) but how is dynamic easier to debug ?
> having to wait an hour for something to compile is nice
Completely Fair. Now whether or not the thing you're working on would take an hour to compile I highly doubt. If you're working on a project that would hypothetically take an hour to compile then I really hope it's not written in a dynamic language.
Not trying to pick on you at all, I believe a lot of developers would agree with you but I'm starting to think that there are developers that are just used to one or the other. I have to point out that I could be thinking this way with respect to statically typed languages but I really have a hard time seeing this point (as I would be if I was falling into the same trap I'm "accusing" you of).
Also if you live near to high quality, it is easier to keep quality high. I worked in one place with a lot of C servers. Any time they segfaulted, the developer got an email with the back trace and a link to the core. Counts were kept and managers made sure people knew to fix them. For my code, it was always easy to fix each segfault. They were rare and usually the stack trace showed all that was needed.
I also worked in a place that was far from quality and they had totally given up on memory leaks and most segfaults. If the segfault happened deterministically enough, it might be fixed. infinite loops would be fixed. But sporadic segfaults were just ignored. It was too hard to get close enough to quality to make it worth fixing.
Read what they wrote again, but this time replace "static" with "compiled" and "dynamic" with "interpreted", and suddenly it made sense to me.
It's true that overall, compiled languages tend to be more static, and interpreted languages more dynamic (and there are good reasons for why they end up that way besides mere convention), but nevertheless that's not what this discussion is about.
As a Swift dev, the reason why it slows me down is that I often end up fighting the type system.
I start with a concept, design my data structures on the whiteboard in a way that makes sense, then I try to code it and because of some detail in the type system it doesn't work, and I end up spending huge amounts of times wondering how to translate my concept into code.
I mean by now it should be clear to everyone that there are certain trade-offs in the choice dynamic vs static typing.
I do like clarity of static type declarations, also the absence of weird polymorphism like functions returning a number or al ist of numbers depending on their parameters, etc.
But then, many statically typed code bases are just tested abysmally. It is as if the type signatures would constitute proper tests. I realise that you don't need to write as many tests in a statically typed setting, but in many cases - and esp. in underpowered type systems - the types won't test the program's logic.
You do need to write less tests with a static language. The types in your program are proof that your program is correct within the confines of the type system (literally, even in the mathematical sense).
The stronger the type system, the more properties can be proven through it (at the extreme end there are, unfortunately not Turing complete, languages where you can prove every single property--those are more used as theorem solvers however).
Back to "common" statically typed languages, there is still heaps and loads to test, as you say. Not writing those tests is not really the fault of the language...
I am bringing up this point that there are tons of miserably tested Java/C++/etc applications because its a problem correlated with their usage, just like runtime type-errors correlate with dynamic languages. A fair comparison mentions both.
Of course a strong type system can drastically reduce the unit test coverage you need. But last time I checked, the strong type systems that allowed for this were all not used in our corporate code bases.
I think it depends on what you’re doing. The author of the the Reddit post mentioned he’s primarily working with data systems. I can see the appeal of someone running quick, ephemeral data analysis not wanting to deal with static typing. But for long term use cases the static typing guard rails is definitely nice.
> I can see the appeal of someone running quick, ephemeral data analysis not wanting to deal with static typing
I've seen and heard this, (Data science field definitely loves their Python and numpy) but I really believe the common problem of non-reproducible research is partly due to the language choice (and probably more to the root cause - this sentiment in research).
Early in your career, you lean into one or the other. And then years later, after you're confident you're right, you find yourself trying the opposite paradigm and liking things about it.
Both have pros and cons, and if there was a correct answer we'd all just go with that one!
Yeah, this is something I’ve been pondering about. I’ve been doing 10 years of C++, and after a 6 months affair with Haskell fell in love with LISP. Now I’ve been doing Clojure professionally for about 5 years, and now am in a phase where I realize the grass is not green anywhere. I’ve been shocked at some of the bugs in my Clojure code that went unnoticed for way too long, and at the same time I remember the amount of “compiler fighting” that C++ or Haskell required.
It’s just a trade-off, in the end, and depends on what poison you can digest.
I've switched between the two several times – not out of choice, just because that's what was needed. The order was BASIC, C, Python, Java, Go, Python, with Javascript mixed in the for the past few. I mostly prefer dynamic languages because static typing is just redundancy. The point of programming is to express high-level concepts which can't be captured quickly with types - if you don't understand the concept of the arguments and return types, you are missing the contract. Types can be helpful as the beginning of docs, but that's it.
Yes, types are redundant, and that is their entire point. Just as much as giving your functions and variables names is redundant, you could just number them. So is splitting up your project into multiple files, all comments, and even structural keywords themselves--you don't need "for" and "while", you just need "goto".
Take all that redundancy away and what you get is not even assembly, it's exactly machine code. We used to program computers that way when they were invented. We still do sometimes in extreme situations. We got away from it for almost all of programming because it's incredibly error prone (and tedious).
Yeah, same here. I started my career as a huge dynamic languages fan, and Python was my favourite language for over a decade.
But now, after 20 years, I appreciate a static language with proper IDE support and code completion. Offload the work to the computer, that's what we do for a living after all.
However, after spending a year working in Rust, I think this can be taken too far. The safety guarantees in Rust are amazing, but the overhead for contorting programs to a form the borrow checker will accept, and the mental overhead related to async/await compared to goroutines is too much.
My favourite language is now Go, and I find it strikes a good balance between static checks and productivity. Rust is still a more elegant language in many ways with things like generics and iterators and their enum types (algebraic types I think is the term?) and zero-overhead abstractions and clean error handling. Go feels a little hacky by comparison. But it's simple and way more productive for me personally, so I prefer it.
Interestingly Evan Wallace (constexpr here on HN) implemented esbuild in Rust initially, and switched to Go and stayed with it for much the same reasons, but also noted that the Go version performed better: https://news.ycombinator.com/item?id=22336284
> But at a high-level, Go was much more enjoyable to work with. This is a side project and it has to be fun for me to work on it. The Rust version was actively un-fun for me, both because of all of the workarounds that got in the way and because of the extremely slow compile times.
After a year of working with Rust and switching back to Go, I second this. I'm enjoying programming again and finding it easier to put in long hours.
Agreed. Try going from "I think this is a callback that returns a promise which can return a string or an int?" to "The compiler/IDE are telling me this future can only return an int and won't let me advance until I make my code comply"
Agreed. I did this journey twice. It was all static types when I was in high school and early college. Then I thought I was too smart to need the computer to do all that type checking for me ("I know what my program does, I don't need a compiler's help!") later in college and early in my career. Then I got incredibly sick and tired of working on really big projects in dynamic languages lacking the ergonomics of good static analysis.
I started programming with .Net languages via Visual Studio (which is quite a good IDE), and I disliked dynamic languages exactly for the reasons you list. But nowadays I mostly prefer dynamic, optionally typed languages ala Julia.
Typing a '.' and seeing the members is very nice, except when the type is not concrete and it's not clear what type is actually being returned. Then you'd have to do trial-and-error using a slow compile cycle. In a dynamic language like lisp, you could just `(inspect x)`. In Python, you can just `embed()` and run, e.g., `x.__dict__`.
The IDE telling you about syntax errors and non-existent functions etc is very nice, except when you use macros and meta-programming and now your stupid IDE won't just shut up (I have this problem even with Python in VSCode).
I've been doing TypeScript for 4+ years and been a web developer for 20+ years. And I experience literally zero benefit from TypeScript. Never has it given me anything useful. To me, it's a massive pain in the ass that slows down myself and my team, even if they think it doesn't. They just don't know JavaScript or have shitty quality of code to begin with.
That, and TypeScript generics can get so freaking complex that the code does NOT become simple to read at alllllll. It's a massive waste of time.
In response to that article: Well, probably the gold-standard for "native JavaScript" tooling is actually provided by TypeScript itself lol. In VSCode it's the TypeScript compiler and language server that's providing the excellent JavaScript autocomplete.
So I'm in agreeance then? Well, no, and that's why I air quoted "native JavaScript". It's extra good because TypeScript/VSCode is silently utilizing the ".d.ts" files that third party libs ship with in the background! I believe as well, at least at one point, it would auto-fetch existing "@types" packages for libs that don't ship their own.
TypeScript, PHP, and Python have support for typing and the commiserate IDE benefits, and I'd imagine these languages account for a super majority of software written in dynamic languages.
Fair. My point was that I was able to make python "behave more like a statically typed language" to make it bearable, but you're right, it ultimately is still a dynamically typed language at runtime, with the type ultimately bound to the value, and any untyped code still getting away from the "compiler" (which is just a type checker here), to wreak havoc at runtime.
Especially the typos part. In Ruby and plain JS sometimes you feel you need unit tests even for the dead simple stuff cause there might be a typo in there... and such tests are an awful chore to write and much more efficiently caught by static typing
Yeah, I loved it when I started because I could easily try things out in JavaScript. Eventually I've come to love Typescript because I don't waste time on dumb things anymore.
The ideal for me would be some strong static typing mode for the main code providing all the guarantees you want and some dynamic typing mode for the tests which lets you test everything well.
The main downside for me of the static typing is that it's close to impossible to provide a good testing experience, DSLs, mocks and spy objects kind of require some form of dynamic typing to be usable.
That’s exactly what the decade old Java for code and Groovy for tests is. Though it’s not used too often nowadays, mostly because Java is enough for testing for most people.
I'm not sure what to make of that. Maybe you just haven't bothered to look? You personally not knowing about something is a reflection of your own knowledge, and not of the state of the world.
No, I've look enough I think. Maybe those better frameworks do exist though indeed, but I have no proof of that. Usually they just have the bare minimum of assert checks and call it a day.
A testing framework should be able to mock and patch any class (or applicable) of the running instance of the program without modifying your code for example, tell me if a method was executed or not, intercepting all HTTP requests without changes, have complex assertion partially matching objects, factories, change the current time... I could add a lot more here.
All of that is necessary harder I think in a static typing environment.
> mock and patch any class (or applicable) of the running instance of the program without modifying your code
Why is it important for the sake of testing to be able to alter the runtime behaviour externally without changing the code? This is contrary to all TDD literature I've read which advises to make production code easy to test, e.g., by coding against interfaces. After all, something being hard to test is exactly the feedback you're looking for when doing Test Driven Design. If it's hard to test, it's probably too tightly-coupled.
> tell me if a method was executed or not
Spies are possible with e.g. the ReaderT pattern.
> intercepting all HTTP requests without changes
My earlier two points are applicable here too, although I'll add type classes as another viable solution.
> have complex assertion partially matching objects
Pretty easy with lenses or just making assertions against record field lookups.
> factories
I don't know what this means. I looked at several articles describing some kind of factory pattern in TDD — all of which were horrifically verbose — and all I can glean from that is we are talking about mocking some function which generates objects.
> change the current time
This is no different from mocking other system boundaries, which I have already addressed.
> I could add a lot more here
You're welcome to, and I imagine my suggested solutions will continue to follow a theme. I'm not sure you're here to have your mind changed though. It seems you've reached your conclusion already.
> Why is it important for the sake of testing to be able to alter the runtime behaviour externally without changing the code?
Because that's additional cruft that you don't want when reading your code. Yeah I get it, you can pass dozen of abstract classes to each constructor for each thing you are mocking (or equivalent if it's not a class-based language) that you replace if needed, the only problem with that is that it's inconvenient, prone to mistakes (you can forget some) and makes the code ugly (you don't want to read testing code on the main code).
I've done it in multiple static languages and at best it's a workaround that you should not have to deal with. I should add that any additional barrier to writing tests like this one also reduces the likelyhood that your app is well tested since some developers of your team might not bother going as far as that.
> Spies are possible with e.g. the ReaderT pattern.
After looking online, I'm not sure how that works, that does not looks very convenient for sure. Is that possible with that to say something like "the method X of class Y was executed 2 times with the parameters Z" without changing your code?
> I don't know what this means. I looked at several articles describing some kind of factory pattern in TDD — all of which were horrifically verbose — and all I can glean from that is we are talking about mocking some function which generates objects.
Exactly the opposite for me. I just can't stand hovering a variable or a parameter and not getting its exact type, or typing "." after a variable and not having my editor gives me all the available methods on that variable, or running my code just to discover that it instantly crashes because I made a typo or forgot an argument or passed the wrong argument or tried to call a method that doesn't exist on that variable or whatever other issues that happens only with dynamic languages. What a waste of my time.