467 comments

[ 3.9 ms ] story [ 364 ms ] thread
It looks to me as if people who started with dynamic types discover static types after many years, and vice versa.

There’s a zen koan about it, I am sure.

I followed a similar trajectory. Types were the bane of my early career. Hideous, extraneous.

But really, they're the light at the end of the tunnel once you've worked your way though the dynamic / weak typing minefield. It took me a lot of Python, Javascript, and Ruby for me to get there, but now I'm way more comfortable on the other side.

The correct type system is actually way more expressive than not having strong static types. Sum types let you combine multiple return types elegantly and not be sloppy. Option types remind you to check for an absent value.

Static types let you refactor and jump to definition quickly and with confidence.

Your interfaces become concrete and don't erode with the sifting sands of change. As an added bonus, you don't need to precondition check your functions for type.

Types are organizational. Records, transactional details, context. You can bundle things sensibly rather than put them in a mysterious grab bag untyped dictionary or map.

Types help literate programming. You'll find yourself writing fewer comments as the types naturally help document the code. They're way more concrete than comments, too.

With types, bad code often won't compile. Catching bugs early saves so much time.

Types are powerful. It's worth the 3% of extra cognitive load and pays dividends in the long haul. Before long you'll be writing types with minimal effort.

The incoherent thinking I see with some people even in typed systems, would make me very scared to let them do the same in dynamic ones.
Nothing good comes easy. The dozens of hours I'd spend staring at 26 lines in R just trying different ideas to shorten/optimize/improve clarity, and that wasn't something I needed to sell that someone else would depend on for business or personal use.

But I can relate to the pressure to deliver quick results. I found myself burnt out when working on a forecast model around three years ago. The constant "how's it goin'?" tore my attention away from the work, and I'm still convinced I could have delivered a better result.

So, in a way, I agree. In another, I understand the other side of the issue, and I think there are so many less time-intensive tasks going on around engineering that there's often little awareness that something like refactoring a class for better efficiency pays in smaller but compounding ways long-term, with most of the time cost and perceived opportunity cost being immediate and short-term. It's still worth it if you really do the math on the long-term benefit.

I mean there's also an argument that those people don't stay in dynamic systems for very long.
> But really, they're the light at the end of the tunnel once you've worked your way though the dynamic / weak typing minefield. It took me a lot of Python, Javascript, and Ruby for me to get there, but now I'm way more comfortable on the other side.

To me that was not the issue. It was, rather, discovering languages with powerful and expressive type systems.

My first job was in Java, most of my career afterwards was in Python. I've been type-curious for a while because of Haskell and OCaml and am very fond of Rust, I'd take a job in any of those happily.

Types in Java are still, today, largely verbose, hideous and extraneous. The cost / benefit is extremely low (or rather extremely high, you pay a very high cost for limited benefit, and the cost generally increases faster than the benefits). You can leverage types heavily, but it creates a codebase which is ridiculously verbose, inefficient (because every type is boxed), opaque, and simply doesn't look like any other Java codebase so will be very much disliked by most of the people you're working with. And the benefits from that will still, at the end of the day, be rather limited.

Same here! I've started with C++ and Java, learned to hate excessive typing, went through a long period of dynamic typing, and now I'm at the point you and the the author are.

I still code a lot of Common Lisp on the side, but my Lisp code now looks entirely different than it looked just 3 years ago. The language standard does support optional typing declarations, and there's an implementation (SBCL) that makes use of it to both optimize code and provide some static typechecking at compile time (with type inference). So my Lisp code now is exploiting this, and is littered with type declarations.

However, the CL type system is very much lacking compared to Rust or Haskell. I'm hoping one day someone will make a statically, strongly typed Lisp that still doesn't sacrifice its flexibility and expressive power. I'd jump to that in an instant.

"statically, strongly typed Lisp that still doesn't sacrifice its flexibility and expressive power"

SML

... with sane (i.e. s-expression based) syntax.

:).

But I'll check out SML. That's Standard ML, right?

FWIW, SML is an old research-focused language that was the progenitor of Haskell and Ocaml and Rust, and not something to program in :)
F# is a modern derivative usable in the real world.
Yeah.

ML syntax is very pleasant, and roughly, sexprs w/o all the punctuation noise.

I don't believe it has a macro capability like lisps though, but you gain a sophisticated type helper.

Definitely worth looking into!

> ... with sane (i.e. s-expression based) syntax.

You can just enclose all your function calls in parens :D

Also you probably want to check out OCaml rather than SML, I don't know that SML has much of a presence… anywhere really.

My experience with strong types is limited-- I'd done the thing of learning some C, doing professional stuff in Ruby for several years and then discovering the ridiculous power strong types can have and doing some professional stuff in Go.

Typed Racket [1] was really a revelation to me in that regard. I'd be curious how developers with more strongly-typed language experience feel about it.

[1] https://docs.racket-lang.org/ts-reference/index.html

> However, the CL type system is very much lacking compared to Rust or Haskell. I'm hoping one day someone will make a statically, strongly typed Lisp that still doesn't sacrifice its flexibility and expressive power. I'd jump to that in an instant.

https://github.com/stylewarning/coalton looks promising, and stylewarning has recently said he's still working on it.

> I'm hoping one day someone will make a statically, strongly typed Lisp that still doesn't sacrifice its flexibility and expressive power. I'd jump to that in an instant.

This part inspired me to look up the wiki page "Haskell Lisp [1], because I somehow remembered that some people were trying to make a Haskell that could be written in Lisp. But this page reveals even more interesting efforts:

> Shentong - The Shen programming language is a Lisp that offers pattern matching, lambda calculus consistency, macros, optional lazy evaluation, static type checking, one of the most powerful systems for typing in functional programming, portability over many languages, an integrated fully functional Prolog, and an inbuilt compiler-compiler. Shentong is an implementation of Shen written in Haskell.

> Liskell - From the ILC 2007 paper: "Liskell uses an extremely minimalistic parse tree and shifts syntactic classification of parse tree parts to a later compiler stage to give parse tree transformers the opportunity to rewrite the parse trees being compiled. These transformers can be user supplied and loaded dynamically into the compiler to extend the language." Has not received attention for a while, though the author has stated that he continues to think about it and has future plans for it.

But this page does not list everything and there is Hackett [2], which introduces itself with "Hackett is an attempt to implement a Haskell-like language with support for Racket’s macro system, built using the techniques described in the paper Type Systems as Macros. It is currently extremely work-in-progress." - though it seems that it didn't change since two years.

And finally there is Axel [3] - which introduces itself with "Haskell's semantics, plus Lisp's macros. Meet Axel: a purely functional, extensible, and powerful programming language."

Disclaimer: I never learned any Lisp, went from C/Java/JavaScript/Bash straight to Haskell and am a Haskell beginner for lifetime. Though I love the language and the fact that I will be learning it and surprised by it for the rest of my life.

[1] https://wiki.haskell.org/Haskell_Lisp [2] https://github.com/lexi-lambda/hackett [3] https://axellang.github.io/

Sum types even work when you actually have the multiple of the same return types. (Ie in Haskell `Either String String` works just as well as `Either String Int`; the types don't have to be distinctive.)
That follows from the definition being tagged 'Left'/'Right':

  Either a b = Left a | Right b
Yes, the magic is in the tagging. Tagging also solve the issue of `Maybe (Maybe a)` that the other reply mentioned.
Also, `Maybe (Maybe a)` works correctly, contra possibly-null values in dynamically-typed languages. It's a surprisingly significant issue given how seemingly trivial it can sound.
Me too, but I learned in Turbo Pascal and early Java which had some real limitations to their type systems. Imagine - strings of different lengths being incompatible types, arrays of different lengths being incompatible types, no generics, so no standard collections, no serialization, tons of manual typecasting. Having to write separate methods for every possible set of parameters.

Out of nostalgia, getting so frustrated with dynamic-typed code, I once tried to go back to some of that old code and make it use JSON instead of proprietary formats. That was a nightmare.

In the dynamic languages it would be utterly trivial. Just call json_encode($whatever), or $whatever = json_decode($some_string).

Modern languages with modern type systems, inference, generics, etc. that make things like that possible and relatively clean completely change the picture.

Another thing that bugs me about dynamic languages is of course you have to manually check everything all the time because the compiler can't. We used to complain about the bloat of having to write all those type names and casts, but dynamic code, if it has good checks, can actually be more bloated in addition to being less expressive.

I code without types. My "proof", that types do not pay for themselves goes like this:

Every time I encounter a bug (during coding, testing or in production) I make a note what type of bug it was and how it could have been prevented.

Types are way down on the list of what could have prevented the bug. Especially for production bugs, which are the most important of course. It is so rare, that a bug could have prevented by types that I can say with confidence that they would have been a net negative.

Talking about which tool can prevent the most bugs, integration tests win by a large margin.

The reason is that most bugs are conceptual. Like "Oh shit! We have allowed people to tag items as duplicates of other items. And we have a function that traverses the list up to the original item. But now this new feature over there had a bug where it marks the last original as a duplicate of another duplicate and then when the traversal function in that other module is used, it ends up in an infinite loop".

Another example of a popular bug category: The code contains assumptions about the environment that do not hold true. For example PHP's mb_strtolower() will not always create the same string as MySQL's LOWER(). It is very rare and only holds true for a tiny tiny fraction of the UTF-8 characters. So you might expect them to behave the same until you one day trip over one of those few chars.

Can you share the tally? I wonder about the categories you used.
Given that you don't code with types, how are you certain which bugs you could have prevented?

I'd be curious to see this list. Is it anecdote, anecdata, or is it a spreadsheet you have somewhere?

I find the value of static typing to be more from increasing the ease of exploration and understanding of a codebase than preventing bugs directly. The constraints on how the code you're reading could be being used making building a mental model faster. Not to mention the tooling built on top of the typing that can help with exploration.
> increasing the ease of exploration and understanding of a codebase

Very true and a good thing to remember when discussing static typing.

Can't be beat for refactoring. In languages I can trust the compiler, I can refactor fearlessly
It's interesting that you talk about bugs, when the OP doesn't make that argument. The OP is very clear, actually:

> It didn't mean I was free from logic bugs. (Nothing can do that in the general case!) It did mean that a program which type-checked wouldn't blow up in ways the type-checker said it shouldn't, though.

Other than soundness (which is not the same as avoiding logic bugs, anyway), the points the OP raises are about expressiveness. Types help [you / the OP] organize more of your knowledge in the codebase. That's never going to be the most obvious preventative measure against bugs, because the goal is higher-order: structuring your codebase so you can reason more clearly about the program.

That might be a matter of personal taste. How ones brain is wired. For me, types make code less expressive. I can grasp the structure of a piece of code the easier, the less meta data there is on top of the algorithmic structure.
What are you working on though? What algorithmic structure?
Compare this:

    private static function request(?string $method, ?string $url, array $options): array
    {
        ...
    }
To this:

    function request($method, $url, $options)
    {
        ...
    }
I can grasp the latter much better. It immediately forms a structure in my head that I will remember while I read other parts of the code. To do the same with the former, I think my brain uses up twice the energy or more. And even then, I will not have such a good grasp on it as with the former.
Counter point: with the former, I know exactly how to use it: for item in request(“get”, “google.com”, []) { .... } Whereas if there’s no example for the latter, I’d have to read the code of that function (or sometime the code of the functions it called) to know how to use correctly.
Wishful thinking.

You know it returns an array, but you don't know what the array contains. So you don't know how to use it.

You still need to look into the code to see what it returns. Then you will see that it returns an array and what the array contains.

And now you had to process the information that it returns an array twice. Once in the function definition and once in the function body.

> You know it returns an array, but you don't know what the array contains. So you don't know how to use it.

That's a limitation of your specific example, not type systems in general.

For the example I took a random line from Symfony and shortened it a bit:

https://github.com/symfony/http-client/blob/master/HttpClien...

You are free to link to some other example.

Any example where the type is defined as something more specific than an array (especially in PHP, where the `array` type doesn't even differentiate between numerically indexed arrays and dictionaries).

Regardless, I still think the type hints in Symfony's version are better than nothing. It's also worth noting that `private static` is concerned with scope, not types.

Most type systems also provide information about what’s in the array, like Array<String> or int[]
That is just another piece of metadata. But it does not help either. Now you know you get an array of strings. But you still don't know what is inside of the strings.

It might be ['headers'=>'...','body'=>'...' ] or ['status'=>'...','response'=>'...'] or god knows what.

And you are back to reading the function body.

Is that like a hashed array you’re implying? In that case, the type wouldn’t be string anyway, but something like HashMap, Object, Dict, etc. More commonly though, something like HTTP options would be its own type, with defined properties, which you could browse via autocomplete or your IDE’s hover/peek functionality. That type would then specify the types of its values, so you can be certain that options.status is an integer, for example.

You’d generally only use an otherwise untyped array of strings when you can’t know beforehand what their values can be.

Heck, not only I don't have to look at the code, with proper IDE setup, I even don't have to go to the documentation page, just `item.` and my IDE/editor will present me with the list of things that are applicable to my item.

And that's only the response part of the usage, there's absolutely no way for me to know if the method is an enum/constant or is it a string without looking into the code for dynamic typing, and again, sometime N-level deep to get enough information of "what to pass to this function so it will work".

You're showing an example of a function signature in a specific language, not an example of typing though. These are related but not exactly the same.

For example here is a Ruby version, very dynamic:

    def request(method, url, **options)
    end
    request("get", "http...", foo: "bar")
And here is a completely statically typed Crystal version:

    def request(method, url, **options)
    end
    request("get", "http...", foo: "bar")
The argument types are inferred automatically from the usage and mismatched usage will be caught at compile time. And in the second case the ide can still tell you the types in the signature if you want to know them.

In the third example, here's completely dynamic python:

    def request(method: str, url: str, **options): Dict[...]
No type checking happens here in the language itself.

So what I'm getting at is - if you don't like languages with verbose explicit type signatures, it doesn't mean you don't like static types.

Nitpick: It's -> Dict[...]. Colons begin a function body.
Got that, but was triggered by ‘algorithmic structure’; that sounds a tad ‘over the top’ when you then come with some basic web request.

However, I would not call your example particularly well typed. Array is still basically untyped.

When I talk about types I mean things like;

     Either<Error,Group> AddGroup(string GroupName, User[]? users)
In this case I know what I am getting and I know the users are already, when reaching the webserver, deserialized, validated (or the type would reject them and produce an error and I know I am getting an actual Group back.

in your case that would be;

      function AddGroup(GroupName, Users)
I have no idea how that is better or clearer or less work to write? No idea what it returns; Users probably is an array of users, but is it? Or is it a typo? And I need to validate whatever comes in.

You can add docs but I do that still with the typed version (although thats automated mostly unless I need to explain something).

Worse as well is that when I have this:

      function AddGroup(GroupName, Users)
      function AddGroups(GroupNames, Users)
Now i'm completely lost. I'm not even sure these users as input are the same things? Both arrays? Both the same User 'things'? Output?

But yes, in php I write typeless mostly too (although I luckily do not have to touch it much anymore) as the types mostly suck. But in more advanced type systems, the types will tell you a lot/all about the input/output, so you can do without a lot of docs and trying things out can be automated; as we know the precise input so some generator can generate example input that will immediately work; like swagger on speed.

Etc. But agreed, your example does not benefit too much from typing, however I would define Option[] as something precise than just a void* and hope for the best. Also Method and URL so I know invalid input for those very well defined things cannot be violated.

So your example:

    private static function request(Method $method, URL $url, Option[]? $options): WebResult
would clear up a lot for me. Now, for instance, I can see, and not guess, that you are answering a web request instead of some request with confusingly similar names to webrequests.

But how about some more 'concrete' examples; 'request' is rather a low level / abstract thing (I hope...). But your business logic would contain more concrete functions; any examples from those that are similarly badly geared to types?

And now instead of

    $body = Http::request('GET', 'https://example.com')['body'];
You have to do this?

    $body = Http::request(new Method('GET'), new URL('https://example.com'))->body;
And you have cuttered the global namespace with "Method", "URL" and "Option"?
Why would they be in the global namespace? They would be in imported namespaces (something\Http for instance). That's rather normal.

And yes, now you have to make sure you are passing the proper and valid types, which is obviously the goal here:

    $body = Http::request(Method.GET, URL.Parse('https://example.com'))->body;
In other languages you can make it so that the static URL is validated at compile time even.
For that to work, you have to import those explicitly:

    use something\Http;
    use something\Http\Method;
    use something\Http\URL;
    $body = Http::request(Method.GET, URL.Parse('https://example.com'))->body;
Now you have a bunch of lines instead of one. And have "Method" and "URL" in your current scope.

And all of that instead of this beauty:

    $body = Http:request('GET', 'https://example.com')['body'];
Beauty is in the eye of the beholder, however we were not talking about verboseness vs terseness though.

You can make anything terse with stringy types and you indeed seem to favour that, however, this was about how types change readability and brittleness. This is all easier to read by people who do not know what request() does => request is not a good example, but it still stands. In your case, you have no case what $method or $url are supposed to be; you can do;

    $body = Http:request('rabbit', 'cracker')['body'];
I assume something in Symfony catches that, but I cannot see it from the signature at all, and the validation error is, in my opinion (obviously) in the wrong place; it should be at the call site because that's where you are creating the erroneous input so that's where I want to be lead to.
A better example for a typed function would be something like:

    function Request(string method, string url, RequestOptions options) : Result {...}
or

    Result Request(string method, string url, RequestOptions options) {...}

Here, we know `url` is a string (rather than the parsed object), we know all the supported options (they're members of the RequestOptions object/enum), and every value in the return value. Some might suggest even making `method` an enumeration.

If the function name was better, you could call that function right now without needing any more information.

Also, especially if you're a fan of defensive programming, most of the basic argument checking is performed at compile time, leaving the function body cleaner.

Based on your example alone, perhaps you don't have enough experience with typed languages to appreciate the benefits. I don't mean that in a belittling way, but more of an invitation to learn more about the craft.

For the example I simply took to a random, recently updated file from Symfony:

https://github.com/symfony/http-client/blob/master/HttpClien...

And shortened it a bit. In the real example, as you can see, there are even more parameters, making it even harder to read.

Well, for me it's easier - perhaps because I'm used to reading parameters in type+name pairs.

The data type is right next to the parameter name - and it will be in the pop-up provided by my IDE when I use the function... I don't need to look at the docs or comment (which may not exist).

you could possible write an IDE plugin that hides the types information and only show them on hover etc. I don't think this reason is that strong of a point IMHO.
(comment deleted)
Maybe your style is not fit for types?

So I fixed a bug recently where a url validatior code was falling because the code was old and recently what is allowed in an url changed. Imagine there was a language where url/email/file path was a type that could validate itself when you create it and you don't need to always try to create regex to validate things. Even more cool would be if string would not be something we use daily (like we don't use every day bytes) so we would have always a type for customer name, file name or file path and you would need to explicitly ask a conversion from a customer name to a file name etc. The way I think future languages and types could help is to make it almost impossible co create invalid data structures or state.

I think it takes people who do not start out 'believing in types' (I was taught by pupils of dijkstra in NL so my belief in strict-as-possible has always been quite firm) a same kind of timespan / experience as the OP; experience (very) large weak/stringy typed projects, experience them for at least a few years full time and then, when the frustration sets in, try something which is the complete opposite like Haskell/Rust.

I think a certain frustration needs to be there to try something else anyway, when you come against the billionth cannot call hello() on undefined error in a critical (for the company), 100k+ LoC, multi-team project, you might wonder if there is something else.

You are probably not going to post your 'list' here but proper types prevent many trivial and non trivial bugs. Many of the points on your list will fit there, but you wouldn't know it yet.

> Talking about which tool can prevent the most bugs, integration tests win by a large margin.

Obviously there are ways of catching them without types, but proper type systems catch them at compile time and also; how are these mutually exclusive; we use both. We just need less integration tests.

> Like "Ooooooh shit! We have allowed people to tag items as duplicates of other items. And we have a function that traverses the list up to the original item. But now this new feature over there had a bug where it marks the last original as a duplicate of another duplicate and then when the traversal function in that other module is used, it ends up in an infinite loop".

In some of my favorite languages you can catch this in types at compile time.

Obviously; do what works best for you and your team; I just don't buy your overarching statements and 'proofs'. If it works it works, but it would probably work better with types.

Working on a large scala codebase, I quickly learnt that 'compile time' in one language does not always happen before 'run time' in another language.

What matters is not the phase in which the bug is found but how close in absolute time finding the big is to writing it.

Agreed and we have to continue improving in every way; dynamic/static/hybrid, just saying that I have not seen this dynamic enlightenment in larger projects. I have only seen the pain of runtime errors that other (static language) teams never had. Sure, if you would-have-written a test for it, you wouldn't have had it either, but types rather force you to think about it while writing. So sure, you are 'done faster', but the fall-out, and again ofcourse YMMV, of having statically preventable bugs popping up in Sentry at 3 am in the morning with things you would've prevented (not necessarily directly by the types but you would've thought about it more because you had to define the types, which is I think what the parent poster here misses too; I just tend to think less and try more without types which, again for me, is a bad state, but ymmv) is not great.

But sure, I am biased as my experience with statically typed langs has been good since I moved from asm/basic in the 80s to pascal/c (they were an improvement over asm/basic and my first experience with types, not saying you should use them now, or not).

The view I've heard expressed is that deep thought on a piece of code reduces bugs. Whether that takes the shape of religious TDD, rigorous proofs or detailed type design doesn't make such a lot of difference.

I used to be fully bought into types, but I've since realised that they have a number of downsides that in many cases more than offset their benefits:

1. Ergonomic typesystems require a lot of work to happen at compile time and slow down the iteration time (one of the more important things for programming in my view). In my view, saving the source and seeing the result almost immediately in a browser is one of the big advantages of web development.

2. Types are almost always written in a second, much less powerful DSL and then sprinkled distractingly through the code that actually does the work. I prefer the way Haskell does this- separate the type signature out onto at least a separate line rather than mashing the two different languages together.

3. Higher levels of abstraction tend to become very hairy in many type systems (although not all). This ends up just meaning that people who like types often restrict themselves (unconciously) to less abstract programming. They spot the time they're saving by avoiding some kinds of bugs, but they don't see the time they're wasting by being unable to talk at a higher level of abstraction. Another way this shows itself is that types are very rarely first class objects in strongly typed languages, making it very difficult to create code that operates on types, or understands types.

4. Type systems open up opportunities for type driven architecture astronauting, which is just yet another way you can go down an unproductive rabbit hole. There was an interesting study done on different teams solving problems with different languages. The differences of different teams within the same language was much bigger than between languages, but the team that made slowest progress (and without particularly having an unusually low number of bugs) was the team that leant the hardest into encoding everything in the type system.

5. Type systems encourage code generation build pipelines, which again slows iteration time and makes everyones life miserable.

6. Type systems reflect a incorrect model of the world - user input, network input, file system data is not typed. The misery that I've had with some web server frameworks that refuse to acknowlege that they don't know every possible thing that the web client might send them and are able to slot it into a predefined type. I think this is the same kind of error that we made with OO systems - thinking that we could fit the world into a predefined inheritance hierarchy.

7. Type systems encourage a static view of the world. The types of things can change under you, dynamically, (e.g. the structure of a table in a database), but in most typed languages you can't cope with that correctly without shutting down and deploying entirely new code.

8. Related to that, it's hard to imagine using a strongly typed language with the live image approach of smalltalk or sometimes used by lisp systems. This means that the popuarity of strongly typed languages is killing valuable and interesting approaches to building complex systems that emphasise observability, interaction and iteration as a way of understanding them.

There are genuine advantages to typed languages, but many of the advantages touted as being unique to typed languages can be provided by advanced linting and IDEs (intellij was surprisingly capable on plain JS + jsdoc). You can also ameliorate some of the disadvantages of untyped languages while keeping the benefits by deliberately programming in a fail-fast way.

I'm sure that type systems have their place. The research I've come across on empirical studies suggests that while there may be positive effects they are small, which does not at all mesh with the extreme partisanship I generally observe. Yes, type systems gain you something, but the...

I am positive about a lot of these points for the future. Especially the performance points; that's going forward fast. But yes, that's often pretty slow; not that bothered by it for my work though. Also, linters work well for statically typed languages too; I usually don't have to compile for 100s of lines of code. If the editor does not complain, it'll probably all work fine. Like I said; do what works for you , but I think at least a good mix will get more benefits.

6. People mention this more often, but I just don't see how that works; you cannot program without knowing what data you are getting. Sure the world is not typed, but at the moment you are going to use the data, it is typed; be it in your logic, head or actual types. Any webserver can go lower level and give you a ByteStream, but when you finish parsing that, you still have types. You might not know them upfront, so you use ByteStream for a bit, but once you know, you bake types and the world is nicer. Imho :) Not sure why that's a difference?

7. This is an issue where? I know it's Erlang domain, but microservices/docker/k8s/ci/cd/lambda/functions/.../all modern crap do this (redeploy, killing the previous instance(s)) with any code, always, including dynamically typed code. So sounds like a niche?

8. Agree with this; we should experiment and research these things and continue building them. I work with Lisp/Clojure as well and like it, I just miss types often. I never suggested it's all crap; I'm just looking where benefit comes from.

> you cannot program without knowing what data you are getting

Types almost always overconstrain. Each part of your code relies on some very specific properties of your data, yet most type systems end up restricting your function to only work with data that meets a whole bunch of other properties that your code doesn't actually care about.

> Types almost always overconstrain

Not in my experience; so many, basically, stringy types.

> Each part of your code relies on some very specific properties of your data,

So then you either have a type that exposes those you need or you have different types for different functions.

> yet most type systems end up restricting your function to only work

Again, I don't understand this statement; someone implemented the types to fit the data for some functions they needed. How does the 'type system restrict' anything?

There's a lot of things, some about old-school types (Java, C), other about modern ones. I don't think most are fundamental, even though some are common experiences today.

#1 is fundamental. (Yet people somehow live with the JS ecosystem that's slower than GHCi.) It's supposed to evolve into always becoming a smaller problem, since computers are always getting faster; but I don't think we've put everything we can into types already, so I expect it to get worse in the near future.

#2 and #3 are about old-school types.

#4 Oh, yeah, they can. But they can also help a lot in team coordination. Powerful stuff enable you either way, if you harm yourself or take advantage of it is your choice.

#5 Failures in type systems encourage code generation. Expect that to always improve, but always slowly.

#6 That's why there's always a parsing stage between input and processing. You deal with input errors at the parsing stage, and processing errors at the processing stage. Most communities of dynamic and old-school languages make a large disservice to the industry by mixing those; they explode error handling into something intractably complex.

#7 Hum... You are holding it wrong. Do not state variants into your types. Instead, use the type system to get every invariant out of the way, so the variants stand clear. (And yeah, there are plenty of libraries and frameworks out there that try to encode the environment into types. That deeply annoys me... But anyway, if you do that, take the types as requirements upon the environment, not its description. Those are different in very subtle ways.)

#8 This shouldn't be fundamental. AFAIK there are not many people trying this, and the few there face a Sisyphean task of keeping their code up to date with the mainstream changes. I do hope people make progress here, but I'm not optimistic.

> Yet people somehow live with the JS ecosystem that's slower than GHCi.

I think a lot of people, including the parent, seem to equate speed of ecosystem and iteration with web development and instant reload of web pages. When other systems allow fast iteration, it goes unnoticed unless it's for web dev. Luckily, a bunch of those 'impossible' systems have to now too, like [0].

[0] https://ihp.digitallyinduced.com/blog/2020-08-10-ihp-live-re...

Web development is an example. Fast iteration is the thing that I like. I have so far associated fast iteration with dynamic languages, and it is certainly the case that my experience is that most fast iteration systems are dynamic.

But maybe that simply reflects a concern of the relevant communities. If strongly typed language systems start adding fast iteration approaches to things and are able to achieve a similar level of quick iteration then that will definitely address one of the things I dislike about them. I haven't coded significant amounts of haskell since 2000, but back then what you could do interactively was very restricted.

At the end of the day, the compiler is doing a bunch more stuff in strongly typed languages. It's like taking a bunch of your verification infrastructure and saying 'these must run before you're allowed to see the result of what you wrote'. It will necessarily be slower, although with work maybe it won't be so much slower that it matters.

> Fast iteration is the thing that I like

> haskell since 2000,

Things changed a lot in 20 years.

Thanks for noting down something about your age; I have always been a bit ageist about 'fast iteration' as I never met someone close to my age (been devving professionally for 30 years this year) that cares too much about it. I am not a very good programmer, but a very experienced one and i'm consistently faster at delivering than my 'fast iterating younger peers' as I simply know what i'm going to type beforehand, I don't need too many iterations to get it right and I have enough experience to know that i'm close to what we need after it compiles. The people who just type/run 1000 times/minute get stuff done, but it's not the way I would ever like (or liked) to work.

> It will necessarily be slower,

GHCi is fast but other avenues can be explored as well, like creating a real interpreter just for development , like Miri for Rust. Only for faster iteration of logic, you forgo some of the type benefits, but when you are done iterating, you compile and voila. I guess the merging of incremental compilation, jits, interpreters etc will evolve in something that might not run optimally but gives blazingly fast iteration up to perfect performance after deployment. And anything in between.

> #2 and #3 are about old-school types.

There absolutely are approaches to this that don't fall foul of my complaints, but when you say 'old-school types' I think you're talking about Java and non inferred types.

I was including other more modern languages in my criticism. Scala for example ends up with pretty hairy types very quickly for higher level code. So much so that they made the documentation system lie about the types to make it easier to understand.

And most currently popular languages don't give you runtime access to types and allow you to treat them as first class.

The languages that allow you to deal with types with the same language you write code in are not remotely mainstream. So unless by 'old school' you include all mainstream languages then I disagree.

Yes, I meant types systems like Java's.

I was thinking about unusable code, caused by the need to write way more down in brittle types than anything you save on coding. In fact there are problems with complex types.

I have the feeling you are applying your experience from coding your own project.

And not from a project in a team, where you didn't code everything.

IMO types in Python mostly pay for themselves when you develop in a team.

They greatly improve the developer experience by providing automatically enforced documentation. Allows IDEs to provide code completions etc.

(comment deleted)
I moved (very roughly) from Java to Python to Haskell. When I was coding in Python if you asked me what percentage of bugs I encountered could have been caught by types, I would've probably said about 10%. Because I would've been thinking about types in Java and errors like calling "length" or a string and then accidentally calling "length" again on that int value from the previous call to length.

Now that I am working primarily in Haskell if you ask me the same question I would say probably about 90%. Not only does catching the "calling length on an int" types of errors at compile time but it gives you whole new tools (newtypes, Sum types, mtl-style constraints) for expressing complex concepts in a simple manner in the type system.

And all of these tools result in having to spend much less time thinking about things I had to think about in python, leaving me to spend more time thinking about the business logic which reduces the number of logical errors (which obviously can still occur).

As a concrete example I transliterated some Python code that someone was asking about on reddit into Haskell here: https://gist.github.com/lgastako/f7465339214c6fde0fd75f4e488...

You can see on line 57 "unless (status == Fresh) $ do" -- I originally wrote checkStatus as a function that returned a boolean and the expression was "unless fresh $ do" but I replaced the boolean with a tri-valued sum type and rewrote the check against the value "Fresh" -- this avoids the boolean blindness problem and makes it much harder to get the condition wrong -- and while it's still possible to get the condition wrong, it's much easier to find it when you do.

It's not just about preventing bugs. Types restrict what you can do with each variable, such as what methods/members exist on the variable and what valid operations you can do with it. It makes development easier because you don't have to hold it all in your head. Every member/method access is validated at compile time, without needing to write any tests.

You may not make such mistakes when writing dynamically typed code, but if you ever need to change something (like the name or type of a variable/method/member) it inevitably means trawling through all references in your codebase manually and changing them by hand. Please correct me if there's a better way to do it in dynamically typed languages.

An interesting insight I came across a little while ago is that for mainstream, industrial languages, this way of thinking about types is relatively new. It's not that we're seeing the pendulum swing back to types, it's that we're discovering them for the first time!

In earlier typed languages, the types weren't there for reasons of soundness or productivity at all. The types were there for the compiler alone, as the compiler needed them to know which machine instructions to emit for various operations. Types were just a cost imposed on programmers.

Once computers became powerful enough that we could afford to spend cycles and memory making these decisions at runtime, dynamic languages became viable and we saw industry shift over to them, except in domains where dynamic languages still weren't viable, or where existing codebases or ecosystems made it not economically viable.

Fast forward to the present and decades worth of type theory knowledge is finally filtering through to industry in the form of languages like Rust, TypeScript, Swift, Kotlin, and others. For the very first time we're embracing types for their soundness and productivity benefits. This is an exciting new era.

As a bit of a nit-pick, it's not _that_ new - see languages like ML, SML, OCaml, Miranda, Haskell, Coq, etc. that combined the notion of types from programming languages and types from mathematics. It's more that it's only recently that _industry_ has been learning about it.

That said, I definitely think you're right to point that this is a new thing for industry, and not just a swing back to the idea of types that were previously mainstream in industry. I'm excited too!

I feel like new features of popular typed languages have also helped them catch up to dynamic language it terms of ease of use. Go back before C++11, without "auto" writing generic code sucked! Callbacks without lambda closures also sucked. I'm sure someone will point to some 40 yr old language that had this but those languages weren't popular for whatever reason. var got added to C# in 2007 and it took more releases to let it be used in more places. Apparently added to Java much later.

I'm sure someone will give me a good example but for example std::sort in C++ before closures in C++, if you want to sort one array by another, for example you have an array of indices and an array of values and you want to sort the indices by the values, before closures I'd argue this was fairly painful unless you resorted to global variables or copying all of the data into some intermediate format. You'd end up having to write or generate a class with a sort function solely for the purpose of being able to pass in a member function to sort that could access the values. Today it's trivial because you can write a lambda that closes over the values and pass the indices into sort.

It seems pretty evident to me that most successful and popular dynamically and statically typed languages are converging from different directions on a similar set of solutions. Very much reflecting the phenomenon you describe. Some simple examples: C# has moved from very strong typing of the exact sort OP criticizes (`Person person = new Person();`) to increasingly permitting looser/more expressive typing with `var`, anonymous types, pattern matching, etc. From the dynamic side, optional, loosely enforced typing is starting to grow more common (e.g., type hinting in Python, TypeScript in JavaScript) and provides a static but still flexible form of typing. So there's some happy medium where the language balances the permissiveness of dynamic typing and the expressiveness of static typing.
>[…] increasingly permitting looser/more expressive typing with `var`, anonymous types, pattern matching, etc.

None of these features are related to loosening the type system.

It can still feel that way. Take C++ for instance:

  Foo  f = fooFromElsewhere;  // explicit typing (old)
  auto f = fooFromElsewhere;  // type inference  (new)

Now what happens if we change the type of `fooFromElsewhere` from `Foo` to `Bar`? With the old way, we need to change the code to:

  Bar f = fooFromElsewhere;
With type inference however, you won't need to change that line at all. And if the new type has enough in common with the old type, you may not change the code at all. It's just as strict as explicit typing, but it's arguably more flexible, and thus feels looser.
In cases where Foo and Bar are stricly incompatible, auto is fine here. In cases where Foo and Bar are similar and might cause trouble, auto shouldn't be used here.
Yeah, I was omitting a bias I have: I tend not to write class hierarchies, so my types are generally strictly incompatible. Things would be different if the code involved some big class hierarchy (as is sometimes the case, for instance in widget toolkits).
You're not giving the older generations of programmers enough credit here.

While it is true that strong typing is a requirement for the best performance (and this remains so), the productivity benefits of strong typing have been known for a long time.

I mean, just look at languages like C# and Java. These are well established, extremely popular languages, used mostly in business software. A domain where performance is rarely critical. Yet, these languages are very popular. Not in the least because they make it easier for programmers to understand and work with other people's code, and because they provide good tooling, both of which are hugely valuable in a business/enterprise context. Strong typing plays a major role in enabling these features.

Even when C# was still a brand new language, roughly 20 years ago, Visual Studio already provided features like "go to definition", "find references" and autocomplete out of the box. These were a major reason for people to adopt the language.

It's no surprise that people like Anders Hejlsberg, who created C#, later went on to create TypeScript. They already understood the productivity advantages of strong typing and wanted to bring those to the web.

The heuristic I use for choosing a statically typed language vs a dynamically typed language for a task is whether or not the code will sanely fit in a single file.

If that is the case, then that means I’ll probably be able to keep the structure of the code in my head and therefore I’ll be able to get by with a dynamically typed language. However, once the code starts to span multiple files, typed method signatures in a statically typed language are invaluable. It sucks having to navigate a codebase with tens of thousands of lines of ruby or python code trying to work out exactly what structure of object can be passed to the method you’re working on.

Types for checking things are correct is really important which is what this talks about.

For me though they really come into their own when those types then help to eliminate boilerplate. Haskell's foldMap function is my classic example of handling the accumulation of a result where it does the hard work based on the types. Idris goes even further by supporting the ability to infer obvious code for your code editor like handling each case in a sum type.

Having started out with Pascal and C, I thought I hated statically typed languages as well until I got exposed to SML and Miranda. I guess Rust or Swift have the same enlightening impact to people coming from Java or Go and I'm glad such approach is finally hitting the spotlight, even if a bit hampered.

That being said, and as much as I see the appeal on those and hoped that stuff like ATS or SPARK were more prevalent, for me they lead to dull, boring code bases. Which is great! But when I look back on my career, the most fun I had, the craziest abstractions, cool hacks, the code I'm most proud of, it's the one written in dynamically-typed on untyped languages. And here I'm talking about some flavor of Assembly, APL, Lisp, Forth or Smalltalk. PHP, Python and JavaScript and similar scripting languages just don't cut it for me.

Yeah the article is just "hey, I discovered SML!". I've been using SML, Haskell, then OCaml since the early 90s and the benefits of (proper) types and type inference have always been obvious.
I hated types in college, I thought they were a huge waste of time, and on their way out.

But over the years that opinion shifted. I got experience with the actual types of problems we would see. Again and again runtime errors in production, usually from type issues. This really pushed me to invest my time into static analysis tools, and static analysis tools work best when types are at the very least annotated. This was an lead in to compiled typed languages where these types of runtime errors are rare if not impossible.

As someone in a similar boat, I feel still like it's more fun to knock out a really quick prototype in an untyped language.

For something I actually have to maintain and build tests for, a well typed language is absolutely preferable. I used to quip that no one could build maintainable JavaScript, and I enjoyed writing JavaScript, but now with TypeScript I think it's largely doable.

I think what really changed in me is my desire to knock something out quickly was replaced with the desire to have stable software where components could be built well from the get-go and not need modifications for years.

Is 'Maybe Haskell', mentioned in the article, worth reading? Or can anyone recommend a similar book of the 'just enough to be dangerous' kind on Haskell or maybe Clojure?
I don't know about Maybe Haskell, but you can give Real World Haskell a try for free: http://book.realworldhaskell.org/

I found it a pretty good introduction. Unfortunately at some point it devolves into typical Haskell "how can we turn this clear, readable three-line function into a two-line function by peppering it with obscure operators".

'Maybe Haskell' is a fantastic intro to using types and why you might care about them. It's focused in scope and really meant to give you a taste of some functional ideas and a bit of Haskell, and in that effort it does a great job.

Also, you can't beat the price ($0). If nothing else, probably worth browsing through to see if something resonates with you.

Obviously I’m partial because of my history with the book, but having read a bunch of other introductory Haskell materials, this one is still my favorite. It’s my favorite in part because it doesn’t try to teach you the whole language, it just gives you a taste of some of its powerful abstractions, walking through them with just the `Maybe` type. I read the whole thing on a plane flight, and reread it on the flight back.
I learned Clojure by reading Clojure for the Brave and True and can recommend it as a good intro. It helps that the language itself is simple and consistent. You can get remarkably dangerous with only a few hours of study.

https://www.braveclojure.com/

I once told a JavaScript guru colleague of mine that I was spending my free time dabbling in Haskell. His response was 'lol, why would you do that?'. His point was that spending time learning things that you're not going to be using directly any time soon is a waste of time. My point was (and still is), that learning such things opens up a completely new way of thinking about problems and potential solutions.
Does he use Redux by any chance :-)
Oh, not popular.

To clarify the joke being Redux was inspired by Functional Programming.

"Never learn anything you don't see immediate use of".

Such short term thinking could explain why dynamic typing is so appealing: simpler implementations, more possibilities than most mainstream static type systems, while requiring basically no learning at all.

My experience matches yours. Every time I learned a new language, I gained a new understanding of a different way of doing things, and I was able to bring some of that understanding to my daily job's language. It's absolutely made me a better programmer.

I didn't just study those languages for nothing, though. I always had a purpose in mind for them, with the possible exception of Ruby which just seemed neat.

Limiting what you learn is, by definition, self-limiting.

On the one hand, that's a useless truism - we can't learn everything, so you do have to pick and choose. On the other hand, I still find the framing useful, because it reminds me that, in a sense, the decisions about what not to learn are more impactful.

To the example of programming languages: Only focusing on perfecting my skills at the tools I currently use would leave me less able to understand the limitations of the tools I currently use. And would limit my ability to take my career in new paths where I might have a need to use other tools. On the other hand, learning every single new thing might distract me from properly mastering my current tools, or might eat up so much of my free time that I'm effectively spending all my time thinking about work and never recharging my batteries.

Anti-intellectualism in programming is fascinating to me.

The variance in skill between individual programmers is immense, and languages act as force multipliers on that. There are huge opportunities that emerge from pushing yourself to explore new paradigms and domains.

> There are huge opportunities that emerge from pushing yourself to explore new paradigms and domains.

What are those opportunities? As a Rubyist, to me it makes more sense to become really good in Ruby. As I get older and more expensive, I need to be better than the 3 year experience 26 year old colleague. And if not better at least not noticeably worse. Learning Elixir, Go, Haskell or you name it isn't gonna help me. Not in intellectual terms and not in practical terms. Also there are so many other worthy things a person can do with his time other than learning a new programming language (both within programming and outside programming), we shouldn't judge people by their passion or lack of passion for learning new languages.

I won't speak to opportunities; I agree with you that there's a lot to be said for specialization.

But, to the point about getting older: There's a fair amount of evidence that learning new, challenging (as in, outside your comfort zone) things is a big part of maintaining your mental acuity as you age. Doing crosswords is good for your brain, but only if you're relatively new to them. If you've been an avid crossword puzzler for decades, not so much. Learning a new natural language is another example of something that is supposedly good for keeping the creative juices flowing.

I wouldn't be surprised if challenging oneself with a new programming paradigm behaves similarly. Which would mean that learning Haskell might make you a better Rubyist, not because (as people often like to say) Haskell teaches you specific things that you can't learn while using Ruby, but because the mental challenge of learning to work with such a drastically different programming language just generally helps keep you sharp.

That said, I would not go so far as to speculate that this effect is any greater for learning a programming language than it is for learning Esperanto. Just pick the one that sounds more fun.

> As a Rubyist

Are you a Rubyist, or a developer?

> As I get older and more expensive, I need to be better than the 3 year experience 26 year old colleague. And if not better at least not noticeably worse.

Yeah, I had this line of thinking. I don't recommend it. I eventually realized I don't want to play the same game as people who are willing to throw away more than I am. Specifically, I don't want to be in positions that companies would be asking those types of questions. Younger devs will work longer, not have families, work for cheaper, and be more compliant.

So I decided I'd focus my career in the following way: no more web dev. I chose compilers and systems programming to self-study. It's been fantastic. Along the way, I learned Haskell and Rust, contributing a bit to both communities, and having a lot of fun. Eventually, I started finding my way towards seeing more jobs that were in my wheelhouse, and landing some.

> there are so many other worthy things a person can do with his time other than learning a new programming language (both within programming and outside programming), we shouldn't judge people by their passion or lack of passion for learning new languages.

Agree. But if you want to pivot your career, don't wait for your employer to sponsor you in doing so.

> Younger devs will work longer, not have families

I work in a country where work hours are 8.5 hours (the half hour is for lunch). Period. If you consistently do more than that you'd be seen as someone who can't get his work done in time. So it's just not culturally encouraged to do that. I come from a much more capitalistic society originally, so I know first hand these companies/societies you speak of exist, and I'm hesitant to ever go back also because of this issue. The sad thing is I know for sure I write shit code after 8 hours, and I'm sure it's true for most people. So while some startups can churn out young devs like that and get away with it, I'm sure there are businesses that actually care about code quality. Even in the most capitalistic of societies. But we stray: within web dev, the question is how can I be more valuable to a Ruby team - as someone who did 3 years python, 3 years php and 3 years elixir or as someone who did 9 years Ruby?

> within web dev, the question is how can I be more valuable to a Ruby team - as someone who did 3 years python, 3 years php and 3 years elixir or as someone who did 9 years Ruby?

As your career progress, your value is less the volume of code produced and more how you improve those around you, be it via processes, mentoring, abstractions, and guidance.

That's a bit of a strawman though. My initial premise was that learning on the side something orthogonal to what you use daily is beneficial to ones capabilities in general. Orthogonal being key here.
> I was spending my free time dabbling in Haskell. His response was 'lol, why would you do that?'. His point was that spending time learning things that you're not going to be using directly any time soon is a waste of time.

Just as an anecdote, if you are persistent enough you can learn Haskell and get a job writing it :)

Yeah I agree most of the pain of types comes from the ways they're implemented (especially in early C++/Java etc)

Hence my beef with them. PyLint (as an example) can deduce and type check your program for you. Why do I need to annotate the types for every single thing? Python is not exactly a weakly typed language if you go down the details.

So if the compiler knows (or even worse in the case of Java: the IDE knows), why do I need to tell it that? Then you end up with the cases of several prototypes for the same function for things that are essentially the same.

I can understand type annotations for documenting interfaces. Those make sense.

For me it is somewhat different: I always used typed languages and occasionally have to read and improve untyped code. I must admit, I feel half blind in the latter. When debugging a python web service, trying to figure out the control or data flow, I very often scratch my head and wonder "what is this thing"? This applies both to library functions and code from colleagues. Sometimes it feels like untyped languages are write-only languages.
This is my biggest gripe, too. Discipline about commenting helps, but only so much.

In my case, I have Clojure spec or the JavaScript equivalents at the important boundaries in my code. That makes this issue much less painful and also solves a few other issues to boot.

To be honest, in more advanced type systems, I’d ask: “What is this” only to be shown a convoluted type signature. I then put a println or breakpoint in there just like I would with a dynamic language and poked around.

Types are useful, but they are currently trending, so now, they might often be forced into situations where they might not be needed.

Programming languages and their type systems are tools, at the end of the day.

Occasionally, an overwrought system of types will slow you down, or quite possibly make simple changes impossible.

On another day, some other type declaration could save you hours of debugging, or speed up your program by orders of magnitude...

Simply put, I feel that many Java programs probably could happily be replaced by Python or Node JS.

On the other hand, some safety critical UI projects (or at least, some key portions of them) absolutely would benefit from Typescript or Elm.

I wish there was more discussion about when each is a better fit, rather than talking about how much worse A is than B.

Type hype is real. It almost seems like job-creation propaganda at this stage.

When I judge things, I look at practical outcomes; and the fact is that I produce better software with more features within the same timeframe if I use JavaScript rather than TypeScript and the product in both cases is equally robust. This has been true for me both independently and as part of a team.

With JS, I can write more code and more tests within the same amount of time and there is no drop in quality.

I'm very surprised that nobody else seems to be experiencing the same thing. I've been back and forth many times between the two paradigms and for me it's clear as day.

Sorry anecdata isn't data. You might be able to write more code in terms of LOC but you're also writing tests that a strongly typed system wouldn't need.

You don't know that your code is "equally robust". You don't know what sort of "drop in quality" you have because you're not using strong types.

You are making a judgement that isn't backed by anything other than intuition.

I switched my Javascript code base to Typescript a few months ago. There is a productivity cost that is declining over time as I get more used to Typescript. But the conversion also flushed out some significant bugs in the JavaScript base.

And I only get to work on this code once a week. So when I come back to it I’ve found it’s much clearer how it works and I get productive much faster.

I will bet any coworkers who have to work on your code wish it was in Typescript.

This doesn't conform with any of my experiences in non-trivial JS codebases. Migrating to TS tends to reveal previously overlooked implicit typing issues. Also, I find the "upfront productivity loss" of TS to be overstated: adding in annotations here and there doesn't take much time at all, and pays dividends quickly. Many hours have been lost tracking down some elusive runtime bug stemming from a typo in a vanilla JS property access.
In my career (which spans 15 years at over a dozen companies), I've built many complex front ends, complex backends with different database systems, a popular open source pub/sub server which auto-scales on Kubernetes across a cluster of machines, a P2P networking library with selective message routing/propagation (in a scalable partial mesh configuration), a scalable chat system, a decentralized cryptocurrency exchange (federated 2-way peg), a modular multi-process blockchain framework (which scales both horizontally and vertically) and many other complex projects; all of these written with plain JavaScript/Node.js, in record time and essentially bug-free (no major bugs reported so far on any of my projects since they were shipped). So I'm quite confident when I say that there is nothing wrong with JavaScript and dynamic typing.

I've programmed in many different languages too; ActionScript, Delphi, C/C++, Java, Python, PHP, C#, AVR Assembly (ATMEGA8-16PU microcontroller)... I've also programmed a JavaScript UI front end for a TV set top box which had a custom JS engine.

From my perspective, many among the TypeScript crowd are arrogant junior developers who regurgitate what they were taught by a bunch of academic bureaucrats who never actually built a single fully working system in their lives but who believe that their PhD qualifies them to tell everyone else about the many theoretical (but in fact, imaginary) benefits of functional programming and static typing... Benefits that not a single human soul has actually experienced themselves on a real large complex project.

I've seen real-world complex FP projects; in all cases, the code was spaghetti; it's almost impossible to follow the logic because it jumps around too many modules and files; because all the state is kept in a single centralized place, instances end up getting passed around all over the place; traversing many intermediate modules to reach the target module; buried deep in the code... And there is no clear separation of concerns; it's hard to separate concerns when you fully decouple state from logic.

The focus of industrial development for a long time has been focussed on the processes, or the verbs, not the things, or the nouns.

Focusing on the Nouns and the types of those Nouns lets you understand what states they can be in and more importantly, the ones they cannot be in. By using types as much as possible to express those invariants and constraints, you can make sure that the processes don't do something that they shouldn't.

Proving software correct is a very complicated and, as yet, unsolved in most practical cases. Strong typing and strong type systems are a step in that direction.

If a compiler ensures that a sum type's possible values are always exhaustively matched, then you can be sure that the processing at least considers all of the possible values.

That leads to positive results when programming.

If you use Option types and then ensure that the None type is handled, the possiblity of nulls is dramatically reduced, if not eliminated.

If immutability is enforced, that removes the possibility of inadvertent modification, especially somewhere deep in a call stack. That leads to safer concurrency, which is an absolute requirement for using the compute capacity to its fullest extent.

Strong typing like this isn't "overwrought", it's making sure that your code doesn't do something to a thing that it shouldn't do.

As long we can both agree, that at an elementary level, it's faster to write code in a dynamic language, than it is with a strongly typed one, then there will always be a trade-off to consider. Like the trade-off between using glue and nails.

Being able to change things quickly, or describe things creatively is occasionally going to be more helpful than immutability guarantees in some product domains. That practical focus is the very reason why dynamic languages are so popular. They facilitate rapid iteration, which is a quality which also certainly can lead to positive results when programming.

A focus on 'process' is really an understanding that the 'noun' is subject to change, as requirements so often do in engineering and product development. I suspect the modern dynamic languages are a response to this focus on practicality, with Ruby probably being one of the bests illustrations of this idiom (I don't write much Ruby at all, but respect it for what it is).

However, this purely practical focus is not always the most desirable quality in a programming language, and in those domains where you know your requirements are written in stone, then an effort should be made to describe requirements as formally as possible.

But a myopic preference for only one approach to writing software, will probably introduce some flaw into your program, no matter what language you are using.

Every single language is an abstraction after all.

We moved away from Assembler in an effort to focus attention on expressing what tasks computers should perform. Dynamic languages are a logical result of that focus.

I was wrong about types too. I used to think after I had mastered half a dozen languages, I should start spending time on typed languages, maybe eventually learning type theory, Haskell and what not.

Now I focus on things that matter: deep learning, AI, and robotics.

For my use cases, the vast majority of errors with dynamic languages boil down to being able to run scripts that have undefined variables; I'm prone to (mental) typos, so if I had a dialect of Python that failed before runtime when undeclared references exist, I could probably cut the number of iterations I need to arrive at a working script by at least half.

I've never found a valid use case for allowing undefined references, though I suppose it does make the language implementation significantly easier.

Writing tests is not really a solution either. Tests are also code and suffer from the same problem.

At least in JavaScript, these are flagged by a linter. I imagine Python has a decent linter and code editor integration of said linter?
pyflakes finds these. It has virtually 0% false positives and runs extremely quickly.

Run it on save or in a git commit hook.

(I agree that they should be a syntax error though)

Would be very difficult (or impossible) to make this a syntax error since

exec()

Modifies the global scope

Pycharm warns you when that happens. I'm guessing they are using a combination of the abc module (to parse the abstract syntax tree) and some linter that can be found on pypi.

Would a validator that runs before your main python executable solve your problem?

Oops I meant the ast module, not the abc module
Just make the jump for once: https://github.com/knocte/2fsharp/blob/master/python2fsharp.... (yes you can write scripts with this language).
I most often choose Python because it's what's available, and it's good enough for the things I need to use it for. Other people can generally understand my code, too.

F# probably is a good language, but I haven't had a good enough reason to use it.

To play against the current wave, and give a contrarian pov:

I'm grown up with statically typed languages from C/C++/C#/Java and later and didn't know about dynamic languages till recently.

1. Which types are we talking about?

- In earlier static-typed language, the processor-based types `uint64` looked very strict, optimized the code for hardware architecture.

- Having mathematically and ontological correct types is one approach `integer`/`float`/`string`/`datetime`.

- Each type being well defined and each object-class being used as type is another strict approach.

Look at the types of Rust and Kotlin and see the cacophony of decisions made:

- Rust https://doc.rust-lang.org/reference/types.html

- Kotlin https://kotlinlang.org/docs/reference/basic-types.html

2. When you want to implement a method which applies to multiple libraries, you end up writing `fooString`, `fooInteger`, `fooDatetime` etc (or foo(String), foo(Integer)) etc. DRYing is too hard. So you end up writing 10x more methods

3. No you don't avoid `null` checks at all.

4. Generalizations are harder to implement.

5. Interfaces are uglier compared to dynamic languages.

6. The code becomes less readable, not concise and verbose

7. Every new and old typed language is less elegant compared to the dynamic. Look at typescript vs javascript, typescript code is ugly, verbose, non-readable at all.

>2. When you want to implement a method which applies to multiple libraries, you end up writing `fooString`, `fooInteger`, `fooDatetime` etc. DRYing is too hard. So you end up writing 10x more methods

As discussed in the article, you want a sum type here.

>3. No you don't avoid `null` checks at all.

Entirely language dependent.

>4. Generalizations are harder to implement.

For some definition of harder. Harder to implement, causing you to think harder about what should be allowed under these generalizations, leading to less bugs, leading to things being... easier to actually implement in the long run.

>6. The code becomes less readable, not concise and verbose.

Since you only recently got into dynamic languages, just wait till you try to get into a large codebase without types. Where everything basically devolves into containers of arbitrary things that you don't know what they contain until you've gone through the thing with a debugger.

Maybe your day job becomes more interesting that way. Making CRUD apps is admittedly boring, but this isn't in my opinion the way to keep one's self on their feet.

How many languages have true sum or union types though? For example, in Haskell I can’t declare a function as (foo: (String | Int) -> Int) and then call (foo “bar”) or (foo 123), I need to create some kind of wrapper type (like Either) to contain the possibility of a String or Int.

If this were possible, there would not be such a proliferation of useless types throughout code, and code could be updated incrementally much more easily, since if I want to add the possibility of another type in a functions inputs, I don’t have to go and update every callsite.

Haskell's Either is a 'true' sum type, it corresponds exactly to the way sums have been defined in the literature for decades, and also is the Curry-Howard representation of logical or. It necessarily must be inside a Either-like wrapper in order to be type safe, String and Int are ultimately different and so at some point we must discriminate between them. foo could call further functions with its argument inside itself accepting Either String Int, but eventually a destructor will be hit somewhere in the call stack.

If String and Int do implement the same behaviour under some circumstance, then a typeclass should be used to define that behaviour. Then foo can have type (Bar baz) => baz -> Int, where String and Int have Bar instances.

I’m curious, is the ‘true’ sum types, I mean where true is in quotes, based on the premise that lazy language can not have logically true sum types? Or is it in the quotes for some other reason?
I was just responding to the parent comment's claim that Haskell's sum types are not true sum types.
Right, so I must have used the wrong words “sum type” when I should have said “union type”, but I thought the intended meaning was clear.

It doesn’t necessarily need to be inside an Either wrapper, that is an implementation detail, neither does there need to be a typeclass specifically defined for it. For example the new Dotty dialect of Scala has support for union types as I described them [1]. This is nice because it makes union of types an actual union operator, satisfying actual commutativity (A | B) = (B | A) and idempotence (A | A) = A, which Either does not without some extra explicit isomorphisms.

[1]: https://dotty.epfl.ch/docs/reference/new-types/union-types.h...

I agree with my sibling comment regarding Haskell, but your example essentially as written is possible in typescript. Python’s type system also allows it via the Union type, e.g. Union[str, int]
Dynamic languages don't do away with such types, especially uint/float, etc. They just hide it away from you. The only thing it gives you is false confidence, where one day you end up doing operations on two floats because your untyped function is supposed to get numbers in, and you're left with 36.99999999999994$ on your bank account because it wasn't explicit.

So, you write unit tests to make sure that you only pass the rights types when calling it. Doing a very bad job at just being a bad compiler.

Now, I'll agree, the languages you mentioned are absolutely horrible with types. Types in C are basically nothing more than suggestions because you end up casting const away while laughing, C++ has std::types<template typed<std::frobnicate<T, A, R>>>, C# and Java in the past were excessively verbose and required you to type every single thing. Java/C#/C++ finally got a bit better with that with auto/var.

Kotlin types and type inference are absolutely fantastic and make your code clearer.

2/ No, you don't write fooString, fooInteger, etc. You write foo(value: Int), foo(value: String) and let the type system resolve and tell you which ones are available, rather than relying on documentation and runtime type checkings that are plain bad. And if it makes sense, you can even declare them as extension functions, Int.foo()

3/ Yes you do. Unless you explicitly opt in to null values or you're using platform types, like calling code from Java with @Nullable/@NotNull annotation

4/ Nobody calls for generalizations all the time. You're not supposed to write generics for all your methods.

5/ Explicit definitions are uglier compared to duck typing and just going "yolo it has a .frobnicate() method that means I can call it" ? I agree that over time, they might become unyieldy, but once again, with a proper typechecker and type inference, they're a blessing. Using kotlin and writing

    when(this) { 
        is Frobnicator -> this::frobnicate
        is Zobtrinatex -> this::zobritnex
        else -> this@caller::defaultBehavior
    }.invoke()
gives you safety, checks that you're not mixing return types.

6, 7/ Depends. I've written some true abominations, yes. Typecript gets ugly when the libraries you're using abuse types horribly (React used to be absolute trash for that, declaring props was terrible). But then, you pay the cost once (at declaration), and get benefits through your entire app.

Don't write generics for a component, unless it makes sense. If you're exposing a Container<T> and you can access the data inside and you require its type, okay, it makes sense. But don't do a Page<T>, that's dumb.

Generics are a tool. Like C tells you to not abuse (void*), don't abuse generics. Use them with parcimony and your code will be better, give you more guarantess and literally write itself if you've got a competent IDE.

In Python you'd usually use a `Decimal` for money and it would give you an error if you tried to mix it with floats, not silently throw away information (which I certainly agree would be a dire consequence).

In JavaScript, good luck for now, but a solution is being rolled out: [https://caniuse.com/bigint].

Thanks @ohgodplsno for the detailed and thoughtful response.

One final point is that with statically typed languages you sometimes fight with the language to deliver. The language and libraries becomes a maze of problems. Developer productivity and sense of accomplishment is low.

I sometimes look at my code when I wrote a matlab clone in C++, It seems like half of the code is things which I wrote to overcome or augment the language and the libraries.

I'm not following your answer to 2, do you have an example?
Say you'd like to have a foo method that returns... a number, whatever.

You do not need to have multiple symbols "fooString", "fooDecimal", "fooString", etc. Simply overloading the parameters gives you type safety, and keeps a single foo symbol.

    fun foo(value: String): Int = value.length()
    fun foo(value: Int) = value
    fun foo(value: RemoteDatabase) = value.servers.map { it.connect().executeSql("SELECT 1")[0].toInt() }.sum()
All these define a foo method, that returns an Int. The untyped alternative (what javascript, python, etc do in a naive way, without trying to duck type) is to do this:

    fun foo(value: Any) = when (value) {
        is Int -> value
        is String -> value.length()
        is RemoteDatabase -> value.servers.map { ... }.sum()
        else -> error("Welp, our type system couldn't help us there.")
    }

Additionally, if it really makes sense, you can define foo as an extension function on the type:

    fun Int.foo() = this
    fun String.foo() = length()
    fun RemoteDatabase.foo() = servers.map { ... }.sum()
You can then use it directly on the type, rather than call foo(1):

   val fooResult = 1.foo()
   val fooStringResult = "Well hello there".foo()
> 2. When you want to implement a method which applies to multiple libraries, you end up writing `fooString`, `fooInteger`, `fooDatetime` etc (or foo(String), foo(Integer)) etc. DRYing is too hard. So you end up writing 10x more methods

This is only a problem in typed languages without generics (like go) or without sum types (like go). With generics, the compiler takes care of writing fooInteger, fooDatetime, etc. With sum types, the compiler verifies that you’ve handled all of the cases in your foo function.

I don't really see most of my code being improved that much by types and I also see a lot of extra complexity that people in the sphere I am (indie games) have to deal with when using typed languages. It just doesn't seem worth it. When you have to spend a lot of time fighting your language, and handling numerous extra concepts that don't exist in an untyped language because they don't need to, it feels like the people who swear by typed languages that they simply like creating extra work for themselves as a way to avoid doing what's actually needed to be done, just because they want to feel productive.

I also suspect that a lot of this has to do with people's personalities around the concept of borders. Some people like well defined borders in general in everything they do because they approach life from a more procedural perspective, and for procedures to work they need things to be in the right boxes and in the right places. While others prefer borders to be undefined and more free-flowing because more information can pass through concepts and that allows for a more unstructured design process. It only puzzles me that there's so much energy in indie game development for highly bordered programming environments (i.e. all the energy being put into gamedev Rust libraries) when indie developers tend to be people who value borders less, as do all creative types. But I guess people really like types...

Maybe it's a case of problem complexity. I think of types as a tool to help cope with certain things. If you're building a garden wall you probably don't need CAD. For a 747, you probably do. Though aircraft predate CAD so it's clearly not impossible to do without.

I dislike types in 30 lines of python because they're unnecessary complexity. I like them in 10000 lines of c++ because they do some of the thinking on my behalf.

> I dislike types in 30 lines of python They are already there you just don't want to acknowledge them. You can build the same prototype in strictly typed language just by sticking to some primitive types like int/string and type inference, and the progress toward something more complex as your prototype grows. I personally prefer to use types right away, so type system can guide me further and show me when I'm assuming something in a wrong way.
What languages have you worked with? As per the OP, there are certain languages where types are much more expressive and add something to the programming experience.

Learning haskell changed a lot about the way I think about programs and that is even as someone who primarily writes in java.

Aren't you falling into the same trap that the post explains? That there are nuances around when types are useful and not useful? An indie game developer might spend a lot of time designing methods of gameplay and artwork for the game, but that doesn't mean that types should go out the window for all levels of programming.

Wouldn't it be better to approach this by which problem we are trying to solve? A script that is run during development, where resources are unconstrained, and stability is not an issue, should absolutely value the time it takes to develop and maintain the script. So using a typed language may not be useful here. A situation where small optimizations make large improvements might benefit from a typed language, maybe a physics engine of a game intended for multiple platforms?

In the OP, the author approaches it from a "systems" perspective, that when you need either of the 3 scenarios, then you might consider using types. Type inference, Sum/tagged union types, and Soundness, which I think could easily apply to certain areas of game development. Ignoring the nuance around the issue, and being dogmatic that all scenarios in a given field do not need types is ignoring that what we're really doing is writing in languages that need to be interpreted by both humans and machines.

This seems likely. It took me embarrassingly long to realize the fact that you can't understand the benefit of a feature you don't understand. Seems like an obvious tautology, but it's one I fell for over and over, and one I think grandparent is guilty of here.
Statically typed languages (or at least the good ones) are disciplined so the programmer doesn't have to.

If you can work reliably with dynamic typing, that means you are very disciplined about giving the right data to the right function, in exactly the right form. That you are very disciplined about tests, possibly including fairly stupid-looking unit tests (which aren't actually stupid, at least in a dynamic context). Adding static typing on top of that wouldn't help much of course.

When I write something from scratch however, I found that static typing actually speeds up my development. It's less work, not more. Because I don't have to write as many tests, or even worry about huge classes of errors — the compiler (and if I'm lucky, my editor/IDE) just checks them for me.

I don't know the work you do, but I bet that your style could benefit from some static checks. Perhaps not the mainstream ones, but your scripts work somehow, don't they? That mean they respect a number of invariants, some of which could certainly be checked at compile time, saving you significant time on stupid bugs. The result won't be TypeScript or Rust, but I don't think it would be fully dynamically typed either.

Yes. Try prototyping for a quick POC/casual demo with javascript, then try with typescript. If you get back to your demo two month later (or have one other person to explain your code to), typescript is Infinitely superior.

For quick hacks types are not useful though.

Unless you're quite experienced with them and use as a thinking tool and not as burden.
I can second the experience. I write a lot of Common Lisp, and these days it's typed Common Lisp for me. It adds very little overhead in terms of code writing speed, but continuously stops me from making stupid mistakes (like e.g. forgetting a function I'm calling returns a sequence and treating it as a scalar value). My comfort of writing is much better, because I spend less time in interactive debugger hunting my own typos.
how many times have you not used bash/dos/pwsh scripts because it's not typed?

Maybe there's a place for both?

Javascript is for scripts that fit in one file. Anything else is typescripy.
I can say what I do use Bash for: create files & directories, and simple string replacements in files. Anything more involved goes into a proper program. Usually OCaml, though I can fall back to more mainstream languages (Python, C) if I need a wide audience to be able to read it, and the program is simple enough that types aren't really a problem to begin with.
> I found that static typing actually speeds up my development. It's less work, not more.

It's a point that comes back often, and that I totally agree with so it's worth reiterating. In addition to the improved dev tooling (autocompletion, hinting, refactoring), being able to write large swathes of code without actually running it and being 100% confident that it's all _valid_ (not bug-free of course) just takes a huge load off my mind.

Of course, there's huge differences between languages like Java and languages like Typescript. Talking about "typed languages" as a homogenous concept often doesn't make a lot of sense

> being able to write large swathes of code without actually running it and being 100% confident that it's all _valid_ (not bug-free of course) just takes a huge load off my mind.

I've heard similar things before, e.g. "static typing allows you to find bugs in your code without even running it".

Perhaps the reason I'm a fan of dynamically-typed languages is that I don't see the benefit of this. Maybe my workflow is unusual, but I don't write code without running it - I run tests every time I add a few lines.

I think the same. This is especially true for games where you're absolutely running the game again for everything you change, and in case anything is wrong it's generally very obvious visually.
OCaml has a REPL. I use it all the time to check that a new function I just wrote is correct. Yet I still get huge benefits from the static typing: many of my errors are stupid type errors, and having the type checker tell me about them, rather than a runtime error (or worse, a wrong result), makes early prototyping much faster.

Even if I already have a REPL. I believe the main reason is because the type checker is much closer to the source of my errors than runtime checks or tests are.

When moving from a dynamically typed language to a statically typed one, about the only thing I end up missing is hot reloading.

In gamedev, static types don't help when you have a constant value you have to change that tweaks the gameplay buried inside a compiled class that you want to balance out. Changing that one constant means either putting it in a script, which is usually written in a dynamically typed language, or recompiling the whole program, testing, changing the value, and repeating.

The only real reason I choose dynamic languages is because I spent hours on that last cycle just recompiling the whole program and throwing away all the state for a single small change, then getting the engine back to the previous state I was debugging in. I still don't understand if it was a bad habit or just how my mind wants me to program. I expect to be able to interact with my program and see how changing things affects the behavior very quickly, and a compile cycled shuts down that mode of thinking entirely. I remember Steve Yegge's essay that mentioned this, that "rebooting is dying." [1]

There were a lot of times I could write scripts, but the fact was that most of the time the code I wanted to tweak slightly was compiled, and that required a full module recompile every time. The fact is that if some of my code can be compiled, then I will probably end up changing the compiled code at some point, and that means a lot of waiting.

If C# had the ability to hot reload a class like a dynamic language to cut down the recompile cycle, I would be happy, but it sounds like it isn't possible. The old code will be mixed with the new code leading to instability.

So I've been spoiled by a dynamic language (Lua) while acknowledging I made a trade-off for one single feature. In my case if I used a statically typed language I would lose out on certain things and gain others, but dynamic program rewriting seems to best coincide with how I think, and I'm not sure how if I should change that.

[1] http://steve-yegge.blogspot.com/2007/01/pinocchio-problem.ht...

Hot code reloading and static typing are not incompatible.

On a trivial level, C and C++ can unload & reload DLLs at runtime. On a less trivial level, I believe the Yi editor, written in Haskell, can do hot code reloading. On a practical level, I use the XMonad window manager, whose configuration involves modifying a Haskell source file (the main one, actually), and hitting some shortcut. If my modifications are correct, the whole things reloads without loosing any state (my windows are still at the same places).

I think it depends on size or codebase, how many people are working on things and how long you can afford to develop before releasing.

I do mostly work with python and JS, but last Christmas I learned Rust, and it strongly occurred to me that exhaustive matching, no nulls, borrow checking and strong type inference would be a real boost to development given the initial time to build the codebase up. I'd put money on them removing hours of hunting subtle bugs, and on missing the ramifications of refactors.

I built some small scale game stuff using SDL2 for Advent of Code and I enjoyed rust for doing that a lot.

I think also dynamic languages work best when developers actually are knowledgeable about the underlying types and effectively write code in a typed manner anyway. It's a much worse trade-off when function signatures actually avail of loose typing to do strange things.

I agree. I think the downside of static typing is that it encourages developers to pass around complex types between functions instead of simple types and I think this is a mistake.

If you have the option between creating a function which accepts a string (e.g. ID) as argument or accepts an instance of type SomeType, it's better to pass a string because simple types such as strings are pass-by-value so it protects your code from unpredictable mutations (which is probably the single biggest, hardest to identify and hardest to fix problem in software development). I think OOP gets a lot of blame for this and it's why a lot of people have been promoting functional programming but this blame is misguided; the problem is complex function interfaces which encourage pass-by-reference and then hide mutations which occur inside the blackbox, not mutations themselves. Mutations within a whitebox (e.g. a for-loop) are perfectly fine since they're easy to spot and happen in a single central place.

If you adopt a philosophy of passing the simplest types possible, then you will not run into these kinds of mutation problems which are the biggest source of pain for software developers. Also you will not run into argument type mismatch issues because you will be dealing with a very small range of possible types.

Note that this problem of trying to pass simple types requires an architectural solution and well thought-out state management within components; it cannot be solved through more advanced tooling. More advanced tooling (and types) just let you get away with making spaghetti code more manageable; but if what you have is spaghetti code then problems will rear their ugly heads again sooner or later.

For example, a lot of developers in the React community already kind of figured this out when they started cloning objects passed to and returned from any function call; returning copies of some plain objects instead of instances by-reference provided protection from such unexpected mutations. I'm sure that's why a lot of people in the React community are still kind of resistant to TypeScript; they've already figured out what the real culpit is. Some of them may have switched to TS out of peer pressure, but I'm sure many have had doubts and their intuition was right.

In many langauges it is possible to have complex types that are pass-by-value. Rust also completely solves the mutation issues with pass-by-reference by putting the mutability of references in the function signatures and only allowing one mutable reference at a time.
But still, I think it does not fully solve the architectural issue or encourage good architecture (though it can certainly help reduce bugs)... In this case you may end up with lots of duplicate instances in different blackboxes which may not be a good thing either.

The point of good state management is to ensure that each instance has a single home. As soon as you start passing instances between functions/modules/components, you're leaking abstractions between different components. Sometimes it is appropriate to do this, but most of the time it's dangerous. Components should aim to communicate as little information about their internal state to other components as possible.

It really depends on the language. Some still help you use this case in an amazing way. For example rust will allow you to create an enum which can be a FooId(&str). (Or add extra 4 lines to get an owned String that's immutable)

Now you've got an immutable id string, you can access as easily as the bare one, but now you can't mix it with other types of IDs, so you won't pass it to something expecting BarId by accident. As a result - no black boxes and a clearer design.

A variant of this is the cause of many Linux kernel issues. They basically had to cram it into macros to prevent passing real/kernel pointers to userspace by accident, because pointer is a pointer is a pointer.

The correct thing is imho to pass an immutable SomeType or an interface that only exposes the parts of SomeType necessary for the calculation and doesn’t allow mutation of the object.

Of course you don’t send around references to mutable objects and of course you only send to a function just what it needs - but that’s regardless of type system.

Sometimes this will be the best approach possible but adhering with this principle too strongly can overcomplicate the general design/architecture - It can give developers a green light to start passing around complex types all over the place and harms the separation of concerns principle.

In terms of modularity and testability, the ideal architecture is when components communicate with each other in the simplest language (interface) possible. Otherwise you become too reliant on mocks during testing (which add brittleness and require more frequent test updates). I think very often, static typing can cause developers to become distracted from what is truly important; architecture and design philosophy. I think this is the core idea that Alan Kay (one of the inventors of OOP) has been trying to get across.

'I'm sorry that I long ago coined the term "objects" for this topic because it gets many people to focus on the lesser idea. The big idea is "messaging"' - Alan Kay

It's very clear from Alan Kay's writings that when he was talking about 'messaging' he was talking about communication between components and he did not intend for objects to be used in the place of messages.

(comment deleted)
Abandoning a feature just because it enables a misuse is the wrong way to do it in my opinion. Yes, some inexperienced, stubborn, stupid, or hurried developers will pass around complex types when they really shouldn't. But no, this drawback does not nullify the massive advantages of (good) static typing.

Sure, interfaces should be kept small. Let's to just that, then! Recognise that we want our classes/functions/modules to be deep (small interface/implementation ratio), and frown upon shallow instances in code reviews.

No need to give up static typing.

i think messaging is orthogonal to strong or weak typing (hence small/strongtalk, objc, self etc dont let you automatically coerce objects to different types than you expected) and those systems all use messaging
You seem to have experience only on languages where strings are immutable, and objects are always mutable. Those properties are not universal.

And yet, despite correctly assessing the problem, you insist on fighting objects instead of mutability.

one of the interesting things in cocoa/foundation is the types are all objects, but they make a big distiction between NSArray and NSMutableArray, same with strings, dictionaries and many other objects

to make things mutable you have to clone them as such and i cant really think of a single api in cocoa/foundation that vends a mutable array or string...

i think its a very good point...

If you use String for all your data types than you are no better than a dynamic language. There are many string like things that benefit from their own types, e.g. currency, identifiers, post codes. Such types should only be created from a parse of valid strings, i.e. no empty strings, whitespace, illegal values etc. They do not have to be Alan Kay "objects", despite what your language or thought leadership is telling you. They should be values with value-based equality. A modern statically typed language should let you define such a type in a few lines. This is all done in order to make illegal states unrepresentable, which is what type systems are for.
> If you use String for all your data types than you are no better than a dynamic language.

I once read a Haskell (I believe, may have been SML or OCaml, this was a while ago) tutorial (can't find it anymore) that did this. It was infuriating as it completely hid the benefit of the type system. Essentially, details fuzzy, it was creating a calculator program. Imagine parsing is already done and had something like this:

  eval "add" a b = a + b
  eval "sub" a b = a - b
  ...
Where the parsing should've at least turned those strings into something like an Operation type.

Sadly, I've seen similar programs in the wild at work (not using these languages, but with C++, Java, C#) where information is encoded in integers and strings that would be much better encoded in Enums, classes, or other meaningful typed forms.

Yes, every statically-typed language has a dynamic language as a subset. It is up to the author to use and apply types. One can certainly write Haskell where everything is in IO and everything uses Strings.
In Swift Structs are immutable and passed by value, class objects are mutable and passed by reference.

Using a struct is infinitely preferable to a string.

(comment deleted)
Types are important and necessary.

Can you skip them in a typed language? Yes, just use any, Object or whatever the equivalent.

Can you add them to an untyped language? No.

They are not needed anywhere. But I argue that especially JavaScript module-systems would have benefited greatly from them.

A million lost hours in fixing obscure "undefined is not a function"-errors from output of highly dynamic pluggable build/transpiler-systems like webpack, requirejs, babel, buck etc. could have been avoided.

"They are not needed anywhere." -> This should have been "They are not needed everywhere."
No, they are not needed anywhere. Anything you can write, you can write in an untyped language.

That's a pretty strict definition of "need", though. There are many situations where types can help, even if they aren't absolutely needed.

The benefits of types is not something to be discovered, but something that's taught in school with very convincing arguments, if not too much zeal. It's interesting to see this kind of post. Not to be sarcastic about the late discovery of typed goodness, but the fact that this is not already a concensus in the engineering world.
It’s not a consensus because many of us have found that types are not the panacea we were taught. Erlang is a fantastic language, and it’s arguable that types would not improve it, but rather hinder some of its better features. Same is true for Smalltalk and various lisps. I think there’s a place for various approaches to types.
All lisps I used could be statically typed, I think? Does elisp used typep?

For small projects, hacks or emacs macros types are not useful but the possibility of adding types is there.

[Edit:and it's useful when your finite state machine grows to much]

Interestingly quite similar to my own progression. I think working in modern typed languages ( Rust, Swift, Typescript ) is what primarily changed my viewpoint in a way that C and Java just couldn't.

In the last year I've transitioned from full time JS development to full time TS development and have been pleasantly surprised how easy the transition was for my personal projects. Previously I had been quite adamantly against TypeScript; seeing the type system as an unnecessary level of complexity given I already structured my code in quite strict ways. In most it helped me find 1 or 2 small errors, but it certainly helps reduce the amount of time I spend verifying the behaviour of code. It definitely has its flaws, but most of them are linked to its compatibility with JS and I don't think they can be resolved unfortunately.

I was a statically typed fanboy for most of my early career. C# and later F# were my daily drivers and still hold a fond place in my heart. More recently, I learned TypeScript and a bit of Haskell and Rust.

However, I think dynamic languages have their place. Most of my server side code involves parsing one string and transforming it into another (JSON to SQL or the like). Something like Clojure spec is really, really useful for this, and beyond that the remaining code doesn’t really materially benefit from static types.

At any rate for my typical web application, I now prefer dynamic languages, which is something I never thought I’d say.

One last thought: soundness is just one variable to optimize for, and it’s not as important as I once thought. For most parts of my application, rough edges are not a big deal. For the really important stuff (like payments), I always write tests and also do a fair amount of manual testing. And for that stuff, the bugs are almost always logic bugs that types wouldn’t have caught.

How about a language that supports both strong typed data type and dynamic data types? Not sure about other language, but Free Pascal and Delphi support both of them.
> Most of my server side code involves parsing one string and transforming it into another

But this is the real problem. It's a huge failure that so much programmer effort goes into writing the same broken marshaling code over and over again. It's unproductive, boring, and if you believe in http://langsec.org/ also the major source of security issues.

A good static type system is better than dynamic typing. But, dynamic typing is better than a bad static type system.

A bad static type system will slow you down, forcing you to appease its irrelevant complaints, and pervert your code into a form that no sane programmer would choose to write it in, if it weren't for the type system looking over their shoulder. I won't mention any names, but suffice to say such languages exist.

On the other hand, a good type system recognises that its goal is to allow the programmer to write code first in the form that makes sense to them, and then be expressive enough to describe it.

good is better than bad !
Not quite. Good > none > bad. This relation is not universal, it does not apply to pizza :)
People often think about coding, but that's generally a small piece of it.

Typing is a form of structure, and especially a kind of documentation.

When encountering APIs of various kinds 'typing' is part of how they are expressed.

It's hard to build large monoliths without typing.

When working with 'wide scope' problems, when the typing is more oriented towards the data, that's another thing altogether, which is why I think for many systems untyped JS and Python works well enough there.

What most typed systems lack is a fluent way of mapping to 'data types' which are often external to the system, or at least externally defined.

> It's hard to build large monoliths without typing.

It's hard to build anything larger. In a microservice architecture one of the best things one can do is to define machine-readable schema (which is just a way to "type" RPC calls and messages) for all APIs/messages/events, and then auto-generate generate client code from it.

Most of the people who oppose typing must have been working on tiny projects, in small teams where they were co-authors of most of the code.

Strings, integer and boolean are enough to model the world. Period. Ps: oh, and 640k should be enough for everyone!
For me it was the opposite. I started out with dynamically typed, then switched to statically typed for many years, then I realized that types were almost worthless (and even harmful in some cases in terms of how they influence design/architecture) and I switched back to dynamically typed and never looked back.

Nothing beats good testing and good architecture design.