A lot of people in the comments are saying how they started off in Java, or C, and hated types, but eventually grew to love them.
I started off in PHP. It was wild. Anything could be anything. Refactoring was a nightmare. Our codebase was littered with mystery variables like $hold, $hold1, and $holda, which were re-used all over the place. (Granted, this two other problems entirely orthogonal to types.)
Then I got a job at a Java place. My god, it was beautiful. I could suddenly know what arguments functions were expecting, even if I hadn't seen them before. I could be instantly aware if I was trying to use the wrong variable somewhere, or pass in invalid data.
It was as if someone lifted me out of a dank cellar where I had previously subsisted on whatever mushrooms the rats didn't deign to eat, into a brightly lit dining room full of steak and pastries.
I went a similar path, PHP to C#, and never looked back. When I had to switch to Node for one job I was pulling my hair out constantly (and quite literally) because of stupid things that would never have happened in what I called a “real” language (that being a typed, compiled one). I mean for $deity’s sake, there weren’t even any dependency injection options at the time and many many times it turned out a bug I introduced was because of simple typo in a camelcase property name.
I absolutely love Node for the ease of writing something quick and dirty. No dependency injection, no coding standards, nothing but a tool to quickly churn through a ton of data or to perform a single task really well. I also think there are some frameworks (using Typescript, like NestJS) that do JavaScript apps really, really well. I will still, never, ever, ever voluntarily write any kind of “real” application in a language that is not type safe again. The benefits just aren’t worth the perceived time savings...
Node isn't so bad now with typescript. The JS folks learned from endless fixing hot mess code that you couldn't really do with anything less than a good dev experience (Imagine a 5 minute turnaround from edit to test on JS to find a typo in a variable). With TS you get all the nice dev experience from JS, but also some of the C# niceness.
Having spent the last several years working in Node/TypeScript environments (and being firmly in the “I was wrong about types” camp), I think “isn’t so bad” is an overstatement. It certainly isn’t as bad. And especially as TS improves, the possibility to move more and more toward the “not so bad” ideal is there.
But there’s some truly awful stuff in the ecosystem; in the underlying language and the platform’s DNA; in the compromises TS (rightly) makes to be a productive real world tool; in the commonly used tooling; and just peppered throughout everything you can expect to encounter in common third party libraries.
Overcoming all that awfulness requires a lot of additional effort, is inherently limited, and isn’t common in the community (although that too is improving as TS becomes more popular, and as safer patterns become more idiomatic).
I think if I were building a new greenfield project with my choice of platform today, it would be a difficult choice whether to take all that I’ve learned in Node/TS and accept those trade-offs, or to invest in learning another platform.
Honestly haven’t given it a lot of thought, as I’m not currently starting a greenfield project with my pick of platforms.
But I think off the top of my head, languages that I’d look at as first contenders include F#, C#, Kotlin, Swift. I’m sure there are other good choices, for the kind of space I tend to work, that are equally productive and have enough of a community for me to be comfortable adopting them, but I would need to spend some more time researching options to really say with confidence what else I would consider.
All tied in with one singular company each that calls all the shots - For good and for bad. You kinda get all their other stuff showed down your throat along with the languages and things can get bloated fast. But at least things are streamlined. Nodejs had an uprising with io.js in 2014-15 and is now governed by a foundation. I like that a lot. I sleep soundly knowing that no company can pull the rug from under me on a whim.
At least C# (and Swift too) is pivotal to that one big company offering. Free software and openness are important but they are not a silver bullet against bad management.
If most business-side people actually knew what weak typed languages meant for their company, I can't imagine so many of them going for php/js as often as they do. You're always one expression with a `$contact` instead of a `$contract` in it away from a cancelled weekend trip or humiliating sales demo.
If the business side is making these kinds of technology choices, the business itself and the engineering side both have significantly worse and more important problems. And those problems will inevitably create those cancelled weekends.
Honestly, I think there’s an equivalency there. Someone who is an expert in JavaScript means a couple of years. You can hire a high-end JS Dev for a lot cheaper than you could any other language or platform.
The other issue with dynamically-typed languages is that the language sometimes "helpfully" fixes the types for you. I came across one JS project that said this:
for (var i = 1; i != Math.pow(2, 16); i <<= 1)
Replacing Math.pow(2, 16) with 1 << 16 effected a 30× speedup--and this was in SpiderMonkey, which tends to be a little less finicky about optimizing mixed types than V8.
(It's been a while since I coded PHP, but my recollection is that PHP tries to pull a strings-are-integer tricks a few times).
This doesn't look to me like a dynamic typing problem. All the types are Javascript numbers. The issue probably stems from Math.pow's flexibility; it can even accept fractional exponents. The more general algorithm is probably slower.
Even though I learned C/C++ in school, I started off my career with a typeless language, Perl. I loved it for its simplicity and power to quickly spool up working code, but realized it was problematic to use for large projects for many of the same reasons you state. I then switched to a Java project and was immediately frustrated with types because of how verbose it was, but after a while I came to appreciate just how beautiful and pragmatic it was, especially in its ability to help avoid so many runtime bugs that have been the bane of my existence in the Javascript, Clojure, and Ruby world.
I've often felt that some people dislike types because they expect to be able to write code in a certain way that they know will make some very narrow happy path work now and they get really frustrated when the compiler tells them that there are other paths in the code that don't work. "Why is this stupid compiler slowing me down?!". This frustration betrays the programmer's indifference toward the broader quality of the project and their willingness to trade bugs in other paths for a feature that appears to be working. The cognitive mismatch is that it's intended change the way you think and program so you can move quickly on many paths at once (not only your very narrow happy path)--with a well-crafted type system, we can move fast and have quality. Note also that 'quality' isn't just about bugs, but also about a code base that is maintainable, similar to the GP's and the parent's observations about the unmaintainability of their PHP and Perl code bases.
Yea, I agree and for this reason when I teach type systems to new programming students, I tell them that a type is somewhat analogous to a building material. You have brick, wood, steel and iron. You want to build a house that has solid bedrock and has easy forest fires in the area. What material would you use? Most would say rock.
Then I tell them about strings and ints. You can represent 66 both with strings and ints, but one is usable for computation where the other one is simply usable to write Alice in Wonderland with it, or simpler forms of text. Sometimes I get the question then: why can't a string both be computational and as a means of character display? And then I list the upsides and downsides of such a system, just like the upsides and downsides of mixing brick and wood to build the outer wall of a house.
I'm curious if people can think of other analogies that they use for teaching.
Why do you need an analogy at all? Are your students deeply familiar with the construction characteristics of wood and brick already and ready to draw parallels to software? How do those things relate at all to the subject at hand, strings as “means of display” (data?) versus strings as “computational” (keywords, operators, etc?).
Personally, I avoid analogies because they usually mean I don’t really know how to teach the topic, and I’m “hand-waving” on the fly. I either find a way to build on the student’s existing knowledge or I “park” the topic for discussion when the student has enough knowledge to give a correct answer.
I appreciate analogies when I start learning something. Although it's not a perfect representation of what's being taught, it makes it easier for me to start thinking about the topic I'm learning. I can fill in the details later. I think it's useful for certain types of learners such as myself. Although I'm not deeply familiar with the characteristics of construction materials, I was more familiar with them compared to types when I was learning about them. So it would've been a helpful analogy for someone like me.
I don’t know if this is pedagogically useful, but I think of types as shapes, variables (including struct fields and function parameters) as a shaped hole, and values as a shaped thing that can fit (or not fit) into those holes. Kind of like the children’s toy. You don’t want to pass a circle in to a function that needs a square shaped thing, and the type system helps make sure you don’t do that by accident.
If we think of variables as boxes and types as label on those boxes, does it makes sense to have labels on the boxes if complexity of opening the box just to see what is inside goes up?
Probably a little. I might lack average cognitive facilities, but I find that types pay off for me very early. When I worked in a Python shop, I would prototype in Go because the types helped me move fast, and then I could port it to Python (often for a huge performance loss, not to speak of maintainability) to integrate with the code base.
On the other hand, Python’s repl was nice for a small handful of tasks (although I mostly used it for figuring out what the actual type of some variable was, which is obviously a non-problem in the statically typed world).
In the Clojure world we use clj-kondo, maps, and spec to solve these problems
We get editor time feedback of mistakes with optional type hints + light inferance via clj-kondo and a data modelling system that we can export out as database schema, JSON schema etc
And dynamic enough constraint system to express something like all human names must be "Tony" only on Tuesdays
The same constraint that can be shared server side and client side without writing it twice without learning more syntax
Additionally check what the expected inputs for a function are by checking the function specs I think guardrails pro will make this more ergonomic when released
And finally ask the constraint system to generate valid examples of the constraints great for mocking data
I don't miss type systems but I also understand if you're not using solutions here then you're in trouble
Ya, that's why I think each language kind of benefit to different levels of having static type checkers and of various features as well. You can't just blanket say all type checkers are bad, or all language without one are bad.
Clojure is a good example here, it actually can be used with a very powerful static type checker core.typed, yet its users chose not too for reasons that say in JavaScript maybe a different choice would have been made. The REPL for example catches lots of type errors as you code. Other languages don't have such a coding workflow, so a static type checker feels really great in that it too will catch type errors as you code, etc.
Does anyone still use core.typed? My impression was that Circle CI's article announcing that it was moving away from core.typed was effectively a death blow to its community. One of Circle CI's founders later went on to use OCaml instead of Clojure on his next project (Dark) precisely for its static type system.
None of that is to say OCaml is "superior" to Clojure in some way. I disagree with a lot of the ways that the OCaml type system has evolved and I wouldn't be surprised to see people who have moved to Clojure from OCaml. However, having programmed professionally in Clojure (although it's been quite a few years so I'm not familiar with the latest advancements in e.g. spec) I still think a Clojure-like language could benefit from a static type system.
I don't think it'll work for Clojure itself because of a variety of patterns and choices in the standard library (which is in part why I think core.typed died, we also found core.typed painful to use in some of our experiments at my old job both in how it interacted with Clojure and the tooling around it). And philosophically Rich Hickey would probably kill Clojure before he ever considered designing Clojure around a static type system. However a programming language based off the same data driven ideas could maybe do it.
While a REPL and hot code reloading are absolutely huge productivity boosts, they are more or less orthogonal to the benefits provided by a good static type system (see e.g. hot code reloading with Elm or Purescript which comes quite close).
The thing that static type systems provide over tests and runtime contracts is the ability to constrain users of the API of a library. We use regression tests to make sure regressions in code we write doesn't happen again. Likewise types are effectively regression tests at the API level to make sure certain regressions in code that calls our code doesn't happen again. That is an extremely powerful capability that I consistently miss in dynamically typed languages.
The thing is, core.typed is a really powerful type system, but the ergonomics with the way Clojure works didn't work out, so people prefer not to use it when developing with Clojure.
That's what I find interesting about it. Not all language benefit from a type system in the same ways, some, like Clojure, actually get crippled. Now, it can mean that you need to find the right kind of type checker that provides the correct ergonomics for Clojure and maybe that would work. But it's still quite interesting.
For example, Erlang has a bit of a similar thing, Dyalizer made specific choices to work within Erlang's design. Had it not done so, it probably wouldn't have found adoption. Same with TypeScript.
So what's interesting here is that you have an apples to apples comparison where a language is found to be better without the constraints of static type checking.
When you look at other statically typed languages, they're oftened designed around the static type checker. That's the main focus, and the language itself revolves around that. So obviously in such a language, the type checker would be a necessity, as it's the main draw. So it's interesting to look at Clojure for a counter example.
That said, JavaScript you could argue is also an apples to apples comparison, and people opted for types. It'll be interesting to see also where Ruby and Python go, now that they have type checking features as well.
I still can feel very lost when refactoring a complex clojure system as opposed to something like Rust, because you have very little information about all the places in your codebase where a certain assumption was made.
You can go crazy and spec everything, in fact that can help, but:
- In practise, nobody does it
- Specs come with no guarantees. They could even be wrong.
- The official implementation stubbornly insists on not checking return types, so half of your annotation may just be glorified documentation (although you can use third party libs like orchestra)
Just imagine: Add a new required field to a spec, and get a convenient list of source code locations that you need to review. That's the promise of a statically checked system. It's not a silver bullet, but not having this leads to what I like calling "refactor anxiety" (i.e.: did I handle all cases?)
I still love clojure no matter what. I think in practise you can express so much, so elegantly, and with far less code, that your project size is always sorta manageable.
I did Java dev for a while, after being C before then. I felt like everything needed a pile of typecasting in front of it to work, even though these were often objects for classes you'd think should all play nicely. I realized only after dealing with it for so long that Java wasn't supposed to work that way, but how things can work when well written, vs old apps where half the code is written by a long line of 4-month co-op students, are two very different things.
Ultimately I think people just weren't given any credit for making good, reusable types, because then the next dev who submits a better feature faster using your work gets a raise, but you look like a kook ranting about best practices who doesn't do anything "business".
Type systems limit the set of programs accepted by the compiler. A sound type system will reject every bad program - but also may reject some good programs. Type systems therefore also will have escape hatches to let the programmer overrule the compiler.
Bad type systems need you to use the escape hatches frequently - you can't write much C without using casts.
I haven't yet used a language with no need at all for an escape hatch - but some languages need them far less often than others.
Java's type system is better than it was. The main places it still has weaknesses are around exceptions (you will need to wrap checked exceptions in runtime exceptions in places you'd have preferred to specify an exception type via generics, eg) and the occasional cast after an instance type check.
You can phrase good development practices in business terms: what are the risks to the business due to sloppy code? Are they greater than the risk of being slow to market?
Sloppy code has accumulating costs. A good type system can help greatly with refactoring to address those costs, but it cannot help with the attitude that those costs aren't real and don't need to be paid.
My first dynamic language was python, and the quality of the docs and overall apis of major libraries soft the landing.
But later I note that was parts of MY code that become a mess. The problem of delay of good design? is that good design GETS delayed.
And then fix that later is a problem. Major libraries and core APIs have the time frame and focus to polish them, but the rest of the code most do?, not much, so it stay in the awkward phase of "later will be refactored, maybe. Perhaps..."
Then later I move to F# and rust and can't delay bad design for long.
Is a chore to slow down at the start of the coding phase but later the speed up is huge: i don't need to fix my past mistakes by the truckloads...
Early on at the company I'm at now they decided to exclusively use $params for param passing to functions as the one true way to do things. It supports easy default params, named params, an easy passing of config through to deeper layers without needing to know about it at intermediate layers.
What it doesn't support is any sort of readability as a code base scales. It's often hard to know how the function you're calling will behave because the param can tweak some flag and totally change the behavior.
Apropos PHP: I hate it with such as much passion as the next guy, but I am quite impressed with what Facebook managed to do with Hack! (Including adding lots of types.)
Don't forget, much of that effort has now trickled down into the PHP language itself. We've long had scalar types for function parameters and return values, but with 7.4 and 8.0 we've added union types[1], nullable types[2], and typed properties[3]. While not a part of the language yet, generics can be annotated with the linting tools PHPStan and Psalm[4], and native support for these annotations is coming to the next release of PhpStorm[5].
Using PhpStorm, I've worked with 100 kloc PHP codebases where almost every type was inspectable. And the language support and tooling are only getting better all the time.
Coming from Haskell and Scheme, I'm now waiting for them to unify the syntactic handling of variables.
At the moment, you have to put a $ in front of most variables, and no adornment for when you want to call something as a function. Reminds me of Common Lisp with its two name spaces for functions and other variables.
Yeah, the $ sigil for variables is a historical curiosity, but I don’t think it’s going anywhere. It would be an unacceptable BC break for codebases that have implicitly relied on this “two namespaces” behavior
I had almost the same experience, first learning Basic and Fortran, then coming to Pascal and C. The difference is, I just was a student. I didn't write anything big enough to really get the need for types. For a couple hundred lines, that only had to live for maybe a week, who needs types? So when C and Pascal demanded them, I was annoyed.
A few decades later, I really value them. My code now lives for decades, not for weeks. Lines are measured in the hundreds of thousands. Other people work with me. Types eliminate a set of errors. They're worth the effort.
I think time is where the tradeoff lies. How long are you writing for? If your code is only for this week, types maybe are a net loss. If you're writing for a month, maybe it's a toss-up. But if you're writing for a year, types are a net win.
I used JavaScript for years and then used C# for years. During all those years, I also use languages with advanced type systems like TypeScript, Scala, Idris, etc.
I still hate typing in Java / C#. I feel intellectually insulted from time to time. There are too many times I can naturally express something in natural languages or just JavaScript, but cannot easily express those in Java or C#.
TypeScript is great, it lets me express many things I want, but it's more or less bloating and inconsistent. I always have to worry if the signatures are hard to read for the team members.
Interestingly, TypeScript reveals a lot of use cases to the mass - there's more to type systems. It's not possible to write a typesafe 'printf' in most of the statically typed languages. Maybe a full-sized dependent typed language like Idris will come in sight in the following decades.
I been jumping between Java, ruby, Javascript for some years now.
Ruby flexibility is amazing. I love the language and syntax frameworks built around it. Amazing.
Except for investigating and refactoring. Everything in Java, even in worse written systems, was fairly easy to understand and find using intellij and friends. Meanwhile in ruby if someone gets messy it might be impossible to figure that code out.
I like the concept of typescript best. Types when you want them. No types when you just prototyping. It was actually my favorite aspect of Adobe flex when I had a short stint coding in it 12 years ago.
I started with C and some Pascal. Then Java and Perl. Then JavaScript, then Ruby, then Python, then Elixir.
The great thing of Java was garbage collection. No more malloc/free hell and programs still worked.
The great thing about Perl and the other scripting languages was no type declaration (plus garbage collection.) And programs still worked.
After 30 years I kind of agree with the author of the OP: the time lost annotating programs with types of not offset by the benefits. In almost all cases type discovery is something the computer should do, not me. Same as for memory management: automatic, not manual.
My second job had a lot of VERY old C code. They were viciously strict about enforcing Hungarian notation for variable names. This is a workaround for your concerns, but I prefer types any day.
I also started off with PHP, about 20 years ago. Soon after, I started working with C# and had the same feelings you had about Java - code was so much more readable, maintainable and refactorable, and so much less buggy. I've never looked back from static typing, and am put off from using languages I'd otherwise be interested in (like Elixir).
A lot of people are making comments about how they can code "faster" without types.
But for the majority of the code we write, inital speed isn't that important. Understanding the code and maintaining it are orders of magnitude more important for any non-trivial code.
Types are not only a way for the compiler to understand your code and impose constraints. They're also your API to other programmers. When they see a sum type, they can understand its possible states. When they see a product type they can understand its possible values.
Understanding other people's code is at least half the job of a programmer, whether it's understanding a library or understanding code you have to maintain, or understanding your own code that you wrote 6 months ago.
It’s honestly embarrassing to hear all the worthless arguments against types.
The cognitive load of a dynamically typed (or unityped, or “untyped” or whatever) language is massive, yet the common argument is that types «increase» the cognitive load??? How does offloading a large majority of the trivial reasoning of a program over to a type system, ”INCREASE” the cognitive load???
It’s just so endlessly easier to program with types
I think there are a few reasons why people develop an impression that types are overhead:
* People who are learning to code are writing lots of code but not reading very much code. I think types do the most work when trying to understand existing code.
* Lots of people's first experience with typed languages was something like C++ or Java back when they had much worse error messages.
* The kind of mistakes you make when first learning to code make the type checker feel like a pedantic nitpicker instead of a protective ally.
* Programming instruction tends not to teach technique very much. If you invent techniques that leverage the strengths of types, then great for you. If you don't, then you might program for years until you are exposed to the benefits types can provide.
Also, many beginner programmers work on small code bases in every sense of the word.
These days I will often have to glue together some tiny part of two or three enormous APIs, some of which are "auto generated" from some other system. Think LINQ-to-SQL or WCF.
It's amazing when you can take a 100 MB chunk of code, and simply "navigate" to the thing that you want using tab-complete, in the sense that "somefactory.sometype.someproperty.subproperty.foo" is almost self-evident when you press tab and cycle through the options at each step.
And then when you finally get "foo", if it's the exact unique type you were looking for, then you can be certain that you did the right thing! There's practically no need to reach for the debugger and start inspecting live objects. Just tab, tab, tab, tab... yup, that's it, move on.
If you're working with a "blank slate" PHP app (or whatever), where you've personally written most of the lines of code involved, typing can feel unnecessary.
If you're glueing together Enterprise Bean Factory Proxies all day, then strong typing is practically mandatory.
To be honest, I disliked PHP’s dynamic typing just as much as I disliked Java’s static typing, and that’s after spending over 10 years professionally developing primarily in those two languages (and some python and javascript).
The last couple of years I’ve developed stuff in haskell and elm, but I’m currently getting back into python and javascript. And I’m sniffing around, trying to figure out how to utilize typescript and python type annotations in an ergonomic way, that doesn’t feel like too much overhead.
I’ll easily admit that it’s not as easy to reach the same kind of benefit in those languages.
The magic sauce really is (ideally as global as possible) type inference combined with programming primarily via expressions instead of primarily via statements, with appropriate language support of course.
> If you're working with a "blank slate" PHP app (or whatever), where you've personally written most of the lines of code involved, typing can feel unnecessary.
> If you're glueing together Enterprise Bean Factory Proxies all day, then strong typing is practically mandatory.
Might that not be a problem, though? Shouldn't more software systems be small, elegant and well-architected rather than a spaghetti nightmare navigable only through an IDE?
I like types, I think they are great, I like editor tool, I think it is great. I even like a lot of the luxuries modern systems afford me. But … maybe we could stand a little simplification?
Have you seen the enormous scope of something like Azure Resource Manager? Amazon Web Services releases several new products or major features per day. They all have APIs.
Office and Office 365 is also a behemoth that covers entire suites of business products, front-end and back-end.
If you start to seriously talk about integrating these things with a bunch of third-party components, you're talking tens of gigabytes of binaries.
> But … maybe we could stand a little simplification?
Always. Unfortunately, that runs up against the limitations of our squishy meat brains. Especially when they're numbered in their tens of thousands. Simplification, refactoring, and code reuse requires coordination.
I too am horrified that a mere database engine no longer fits on a standard DVD disc... when compiled into a binary.
But I can download the ISO image in a matter of minutes, and use the system with a few button clicks in an IDE to produce functional software.
I know that the raw numbers should qualify as a nightmare, but at the end of the day, things get done anyway and it doesn't seem to matter that much.
I guess we're just horrified because we know how this particular sausage is made...
I agree with this 1000%. Projects (note not programs or individual source files!) are simply easier to grok (maintain, enhance, debug, refactor) with statically (and strongly) typed languages than dynamic IMO.
Five years ago when switching jobs I’d pursue “full stack” positions because I’d done a fair amount of front end dev in the past. No more, me personally, I’m backend all the way. Dynamic typing (vanilla js) is just harder; more time consuming more cognitive load, IMO.
It's not perfect, but TypeScript goes a long way toward making frontend feel as safe as backend. You still have the potential for weird bugs when interacting with external JavaScript, but your internal code is pretty safe. Also, TypeScript's type system is impressively flexible, and I often find myself missing features (like unions) when working with other languages.
Typescript allows you to have literal type discriminators in their unions, which combined with control flow based type narrowing of unions effectively gives you sum types. That is what he is talking about, because most mainstream langauges don't have sum types and you need to go to strongly typed functional langauges (haskell, scala, etc...) or modern strongly typed langauges such as Rust to get access to such features.
Yes, this. Sum types in most languages are approximated by something like kotlin's sealed classes. Sometimes you don't want to declare a whole new class hierarchy, you just want to say "this function can either return this or this".
I love love love Python for data science, in part because it's dynamically typed. I can bang things out quickly without worrying about the engineering bits, and, since I'm working in an interactive coding environment, it's generally easy enough to just inspect the values of my variables to figure out what they are.
I hate hate hate Python for ML engineering, in part because it's dynamically typed. The same features that make it so easy to hack out a quick data analysis make it absolutely awful to build for durability. For example, since stuff in production runs hands-off, you need to feel pretty confident about the return types of every function in order to feel confident you won't throw a type error at run time. Actually pinning this down can get quite complicated, though, when you're working with a library like scikit-learn that relies heavily on duck typing. Sometimes you end up having to go on a journey down a rabbit hole in order to clearly identify and document all the types your code might accept or return.
(Disclaimer: Hate aside, it's still my preferred ML engineering language. You've got to take the bad with the good, and the language gets you access to an ecosystem that is so very good.)
This is absolutely it. Untyped languages are great for glue-scripts, for exploration (of an API, a dataset, whatever), for quick-and-dirty things. As soon as your logic grows beyond "what can be appropriately expressed in <5 files" and/or "this is going to have a second developer", types become helpful.
I see it completely the other way around coming from the python, dynamically-typed side. For me, statically-typed languages have a benefit on the smaller-side of the scale, but absolutely bomb when the code-base grows. At that point everything is a FooFactory or an IInterface with no help from the IDE anyways because of IOC/DI/attribute reflection magic. And when it's that big, everyone argues over folder, package and inheritance hierarchies and the "right way" to refactor & reuse code, with the inevitable slide into yet another level of inheritance or interfaces. All the while peppered with Singletons, overloads and new abstract virtual base methods with complicated method override rules.
Obviously I exaggerate a bit, but we've all seen various incarnations of a lot of those issues.
The second you’ve “engineered” yourself into losing good IDE support half the benefit or using a strongly typed language goes out the window in my opinion. Though maybe some bias because I make an IDE! :)
Happily with TS it’s possible to have DI and IInstantiationService’s and all that and still maintain good IDE support —- in no small part because the IDE is built with all those, in TS... if it was unusable we’d fix it.
IMO dataframes are the reason why dynamic typing fits data science so well. It's certainly possible to represent a single dataframe as a static type; but representing all the slicing, column removal, joins, etc. is actually pretty hard without dependent tricks. So bypassing types for data frames is preferable. On your ML engineering point, the other side of it is that once your dataframe's schema is finalizes it really should be statically typed so that assumptions can safely be made about what is/isn't inside of it
> representing all the slicing, column removal, joins, etc. is actually pretty hard without dependent tricks
Disagree in the strongest possible terms, tbh.
It's the lack of static typing that gets you 3/4 of the way down your experimental pipeline only for your code to fail because column "trianing_batch" can't be found. Huge productivity loss, even with rapid iteration.
We must work very differently. I couldn't fathom that happening to me, if only because I compulsively peek at samples of the data frame every step of the way, in order to make sure the data look reasonable all the way through.
As helpful a static type system can be, most of them do cause increased cognitive load UNTIL the FIRST RUNNABLE VERSION. Having your code running for the first time is rewarding for devs, especially those who are handling many simple codebases rather than few complex ones.
There are great type system that provide enough ramp for for first runnable version. Rust, for example, has type inference and an unusually helpful compiler.
But most languages, especially the old ones, don't have that. Because of that, coders need full-blown IDEs like Intellij/VS to write comfortably in those languages. Without full-blown IDEs or editors packed with plugins, it is quite a chore to navigate types. There's no hyperclicking, type annotation, docs preview. While dynamic-typed languages usually is runnable since the first character without having to wait for the compilation, so even notepad is acceptable to write with (though that would be really painful).
Having been in both sides, I love types in certain languages (and disdain them in others e.g. PHP). I would still pay the price of compilation and use TypeScript for scripts that lives long rather than using JavaScript. But I think both sides need to understand where the cognitive load arguments came from.
I just didn't when I was new to programming. I learned Python because that's what I saw pitched to me all the time. There was a local Python meetup, MIT's CS courses taught Python and Udacity eventually taught Python. I loved the language and learned quickly how to do a bunch of basic stuff
But I wanted to make Android apps and, ugh, Java confused the crap out of me. It wasn't so much the language, but rather "URI? Where in the world do I get one of those?! Oh, you instantiate a URI with the string. In Python this is just a String..."
Or another time, once I had mastered types, I convinced my Javascript team that Typescript was Worth It. And the ensuing chaos when nobody understood how to use Types and nothing would ever compile for anyone.
All SUPER noob-y mistakes. All because I (or my team) didn't understand how to think in Types. Furthermore, people who can Think in Types often don't know how to articulate that thought process to others.
I'll also throw out there that scripting languages w/o types tend to have a lot less tooling around the language. People are used to being able to type whatever they want and things kinda just work.
Typed languages are very different. The tooling is far more robust and more able to point out errors, but also tends to be more complex than just a simple text editor.
This is changing with VS Code and LSP being a thing, but it still influences those communities in fundamental ways.
And you also see many dynamic languages working around lack of types by describing the types anyway in the documentation. Like jsdoc in javascript before typescript became popular.
You are already doing the hard work of describing your types anyway, but because your compiler doesn't know the types, it can't help you out.
There’s a continuum for sure. Type systems have their own kind of cognitive load, because you have to learn them, how to model your invariants in them, how to understand error messages, avoid common pitfalls, etc. Type systems are usually quite “general” and abstract, and that always comes with cognitive load. Quite often reading the code of a parametrically-polymorphic Python function is way easier than understanding the generalized polymorphism constructs of an advanced static type system.
There are times when the cognitive load of a particular static type system is less than that of a dynamic one, for certain classes of programs and audiences, and times when it’s not. This is fine and we should encourage development of both kinds of type systems and a shared understanding of how to pick the right ones for the job (which is a discussion nearly always missing in these debates).
For the most part this cognitive load already exists in order to write correct programs. Types don’t just appear when a type checker is present. Type systems only add on the additional burden of needing to understand the formalization of that intuition. It’s not an insignificant burden, but it’s a much smaller gap than learning to intuit about programming correctly in the first place.
It depends on what you mean by "correct". If it means "it does what I need it to do and I can move on with my life", then no, the cognitive load doesn't already exist, and formalized type theory in many cases adds an incredible amount of cognitive load.
I know that in many cases "correct" means a lot more than that, such as in proper software engineering contexts when building a program/system that needs to live and evolve for a long time among many people. I write that kind of software all the time, and I always use static type systems for it.
But I also write a bunch of ad-hoc, one-time-use programs, and I'm very glad that I don't need to reason about abstract type theory in order to parse a CSV file and add up some columns to send to a colleague.
I don’t care if my throw away CSV munger is formally safe/sound or maintainable. I care if it gives me correct results, once, as quickly and effortlessly as possible. Which is why very few people fire up GHCi/rustic/javac for that, but instead use awk/shell/Python or something similar.
OTOH, sometimes the CSV munger one writes today is still in use five years later, when the input CSV has a UTF-8 character for the first time, and suddenly it crashes and no-one knows where the bug could even be.
UTF-8 support is completely orthogonal to static typing. There are dynamically typed languages that handle it great, and statically typed languages which have garbage support for it.
In general I know what you mean though. “What if this CSV parser turns out to be really important?” If you think there’s a high probability of that, do it in a statically typed language then. 99.9% of data munging I’ve done have been throwaway programs to answer a question to inform some decision or help refine a mental model.
I don't buy this "choose the right tool for the job" thing. I'd always advise you to choose the tool that your company has the most knowledge in. If there's a problem that cannot be solved in that programming language, then you can look for a better tool. But you shouldn't pick a language that nobody else understands, just to save a few lines or some time when doing the initial implementation - just to realize that you now have to constantly keep up the knowledge to be able to support it.
I consider that one of the most important criteria when picking the right tool for the job (if you're writing something to be maintained by other people in a company).
> It’s honestly embarrassing to hear all the worthless arguments against types.
I think this forum is written in a language that doesn't have types. My favourite though was lambda-the-ultimate.org, at some point the number one resource on the internet when it came to discussing about programming languages (maybe it still is, I haven't followed it for a while), which was written in an untyped language, PHP (Drupal, to be more exact).
> Understanding other people's code is at least half the job of a programmer
This was one of the pain-points when I was working more with node.js: the function signature told you nothing - like whether the function would even return or not would sometimes be a mystery.
In very, very short scripts you can get away without types (like in a notebook for example), but once a project starts to get even medium size the tiny amount of time you put into writing a type name explicitly here and there is more than made up for by the degree it helps with the structure and correctness of your program.
This kills me about python. So many times I cannot figure out what exactly a functions expects and what it returns, sometimes even from reading the documentation! Matplotlib is especially bad.
Good grief, matplotlib and the whole scientific computing stack are abysmal. Every function takes dozens of arguments, but any given call only has to pass some subset, and depending on the subset and their runtime types, the function just tries to guess what the caller wanted to do. I get that this stuff was written by amateurs, but in a saner world this stuff would have been addressed by now. Anyway, I moved on from Python to Go and I’m super happy (!!performance!!, !!package management!!, editor tooling, top notch document generation, quality ecosystem, !!static binaries!!, and types—even if Go’s type system isn’t as robust as Rust’s, it still goes a long way).
This is the most ridiculous thing about dynamic languages, I don't understand how people can be productive in such environment. Meanwhile I just press few keys and IDE shows my all about I need to know about that function. Oh, and it also runs 100x faster.
And mypy is still very immature. You can’t denote a recursive type (e.g., a JSON type) or specify a callback that takes keyword arguments. Even getting it to load type annotations from third party packages is hard in many cases. Worse, it seems to be improving at a snail’s pace if at all.
A "JSON" type in Python is just an untyped dictionary. Is that not what you're looking for?
Also, python's type-hinting supports forward-references which are what you would use for recursive or "self-referencing" types.
Personally, I like the slow pace they're taking with the typing. It's touching the core usage of the language in a fundamental way and I don't think that can be rushed. We're seeing lots of community growth around the type-hinting, even using them at run-time, which is amazing to watch and marvel at.
> the function signature told you nothing - like whether the function would even return or not would sometimes be a mystery
Yes! This drives me nuts about JavaScript! Untyped parameters are one thing, but having to read the entire function just to know if it returns anything is ridiculous.
To be fair, JS’s dynamic types aren’t solely to blame for this. There are dynamically typed languages that don’t require you to read an entire function body to know if it returns something. But, those are languages without statements, and all functions return something.
I agree with this for really large codebases, but I think you can get a surprising amount done before your program becomes "non-trivial" using a good dynamic language. For example, the website you are writing this comment on has been perfectly maintainable in lisp without any static typing for the past 15 years.
It also hasn't changed much in 15 years. Now that could be (and probably is) a deliberate decision. But there are more than enough cases of projects that haven't changed because they can't...they've painted themselves into a corner, and every time they try to change something, something else breaks.
I've felt this way with Python and Ruby and Node projects (it is a major complaint in the RoR community), but have never felt that way with Scala, Java, C#, F#, Rust, or OCaml. Most of the time, when I need to make a change, I just change it, iteratively eliminate any type errors, and once the type errors are gone, the tests magically pass too.
I get the feeling that lisp doesn’t produce as many runtime type errors as Python or JS and I’m not really sure why that would be. Maybe there’s something to a functional style of programming that improves quality even apart from type checking?
Arc "feels" a lot more Scheme-like than Common Lisp-like, but pg is a CL guy afaik; my observations on the two:
Scheme avoids type errors by writing programs which could be given static types with a sufficiently powerful type-checker. If a Scheme programmer needs to define two aggregates, both of which have a property "name," they're likely to define two different functions, foo-name and bar-name, to get at them. This, though it's noisy, makes type errors more obvious.
CLOS helps avoid type errors too, since it encourages thinking not about how code interacts with a single type, but instead how it interacts with a whole _protocol_ of methods. I think multimethods in general are a powerful design tool that help avoid type errors in a dynamic setting, but I haven't had a chance to try them out in a language other than CL.
Many CL implementations also have static type-checking. For example, when I try to define a function with a type error in SBCL:
* (defun f (x) (+ 1 x (car x)))
; in: DEFUN F
; (CAR X)
;
; caught WARNING:
; Derived type of X is
; (VALUES NUMBER &OPTIONAL),
; conflicting with its asserted type
; LIST.
; See also:
; The SBCL Manual, Node "Handling of Types"
;
; compilation unit finished
; caught 1 WARNING condition
CL's philosophy here differs a bit from most typed languages: SBCL will emit a warning (not an error!) for code it can statically show is impossible to run without getting a type error, while e.g. GHC gives an error for any code it can't statically show doesn't get a type error. However, in my experience, many type errors are obvious enough that SBCL warns about them (e.g. passing a vector where a list was expected, a string where a symbol was expected, nil where a non-nil value was expected, etc.), so this helps quite a bit, especially when combined with the interactive editing one gets through SLIME/slimv.
One can also attach type annotations to functions [0], and SBCL (and probably other implementations) will add a runtime CHECK-TYPE [1] unless you tell it to optimize for speed enough.
This is partly right. FP approaches do tend to reduce the volume of mistakes, and reduce the kinds of mistakes you can make.
But that’s not because type errors are less likely. For dynamic functional languages they’re only less likely because the implicit contracts tend to be more general and the data structures tend to support a high degree of polymorphism.
The reason FP approaches tend to reduce mistakes is mostly that managing state is hard, and pure functions are easier to reason about.
I have had similar experiences with Python. What endlessly frustrated me when looking up the documentation for Python’s standard library (I was using the classes for working with emails/imap) was that what kind of thing to pass in as arguments wasn’t specified clearly. It made using the API so much harder because I had to trial/error to figure out how to construct the proper object that the API would accept.
The reason people argue for being able to write and run code quickly is it helps with prototyping and iterating on a concept. For a lot of people, overly verbose typed languages kill that creative cycle. Type systems are great for data validation and self documenting code when you need those things but they essentially double the work when you’re first getting something off the ground.
For many enterprises, getting code out the door is a key win. So in an idealist conception, maintainability, correctness and good design are everything. In the practical reality of today, these often have to take a back seat to speed of implementation.
What I'd like to see is a language that allows typeless programming to start but which is designed to allow the imposition of types at a later point.
Also, TypeScript is designed to work the same way, as it can coexist with untyped JavaScript code as you introduce it to the code base (it still has to run through a compiler, of course).
Types are cool but when I worked on eclipse apis (plugins) god forbid they were utterly useless. Design matter, having 32 intermediate classes to do just anything, 12 of which are totally unrelated and suddenly types don't help you much.
Also it was full of Option types disguised as potentially empty arrays.
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.
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.
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.
> 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.
> 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.
> 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.
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.
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.
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.)
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.
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.
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.
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!
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.
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).
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.
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...
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.
> 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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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...
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.
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.
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
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.
> Type inference: because having to write out every type, however obvious, is an incredible waste of time. Person me = new Person(); is ridiculous. let me = new Person(); may seem like a small improvement, but spread over the body of an entire program and generalized to all sorts of contexts means that type annotations become a tool you employ because they’re useful — for communicating to others, or for constraining the program in particular ways — rather than merely because the compiler yells at you about something it should know perfectly well.
Type inference was a major revelation to me as well in Rust. I was reluctant to learn the language because of my experience in Java with its high ceremony everywhere, mostly due to lack of type inference.
The first thing I noticed with Rust was type inference. It gives the entire language a distinctly high-level, almost scripting language feel - modulo ownership.
I love Kotlin. I also love Typescript. I'm yet to try Rust, but I'm positive that I'll love it as well. They are all born to solve the same problem I think, only from a different perspective. I use Kotlin every day and the more I learn the happier I am. I'd never go back to Java if I can help it.
Personally, I prefer the type names to be at the start of the line, so I don't need to scan to the end of the line (or guess based on a function name).
But I agree, it's a waste to type it all out. So I use an intelligent IDE that reduces the repetitive typing:
So typing `Person.var` gives me `Person person = new Person();` or typing `Person person = ` will suggest `new Person()`
Sure, it doesn't look as appealing when creating an new object, however I get a better information when you've written something like:
I think you are making a good case for a somewhat different point: we have long reached the point where the tools we use for writing and reading programs can give us all sorts of semantic information on demand, and language syntax design should take advantage of the fact that everything the programmer needs to know need not be forced into just one flat page-of-text view of the code.
Alan Kay made essentially this suggestion when the requirements for the language that became Ada were being debated, but it was not taken up at that time.
But that doesn't mean hide everything from view just because it can be accessed via a keyboard short-cut or mouse movement - my eyes are faster and easier to move. Also, code isn't read exclusively in an IDE, nor the same IDE that I use.
A lot of tools can put the type inference result for all variables when you are reading code (pycharm does it during runtime but IntelliJ + rust did it dynamically as I typed if I recall correctly)
This has always been the case. Emacs could do all this decades ago. Genera/Lisp, Smalltalk, etc. to various degrees did this. And they all failed to make their case. Time after time. Hiding context has always been bad for understanding code. The same reason dynamic scoping went out of style the moment lexical scoping was invented. Or that GOTO is considered harmful. Or that we all prefer simple, small functions rather than multi-page monsters that do ten different things at once.
Showing all the context has never been feasible, and abstraction is about how to deal effectively with that constraint. Types are abstractions, and consequently explicitly declaring a variable's type does not provide all the context.
The original C++ 'throw' declaration is an example of an ill-conceived attempt to provide and use more context, and an example where tools provide a better solution than piling on the syntax.
Sure, type inference isn't always better, but it's usually clearer — I almost always already know the type of the thing, so explicit types are just noise — and languages with inference still let you write
let me: Person = something.getOwner();
so you still have the freedom to write the type explicitly.
All the major Rust IDEs (VS Code, IntelliJ, and probably Vim + others) show the variable type inline when you don't provide it explicitly: https://i.imgur.com/96Wfukl.png
The white text on a gray background is provided by my IDE.
My personal experience with C# and Typescript has been that explicit typing like that rarely provides much value. Explicit typing tells me one thing and one thing only: GetOwner() returns a Person. But when I have questions, they almost always fall into the realm of "details about the GetOwner() method" or "details about the Person type". I honestly am struggling to think of any examples when seeing a variables type answered all of my questions.
Honestly though, this is a tooling problem. There's no reason at all that developers should need to spend time encoding information into the source code that's already known to the compiler, but devs also need to use tools that can feed the compiler's knowledge back to them. Jetbrains Rider (and their Resharper extension for Visual Studio) have a set of options called inlay hints [1] that do this exact thing. Personally I tend to keep most of the type hints turned off because they do add a lot of clutter, but the feature is absolutely invaluable for parameter name hints.
In the case of Java you have the option of using "Person me" or "let me". Simply stick to the convention of explicitly stating the type at least once when declaring a variable. I have also noticed that Python allows developers to note the type (at least when defining functions). It's not necessary and I don't think the language uses it, but at least it offers a standard mechanism to note it in the code.
Was recently on a project that just migrated to java 11 (mid august of this year) from java 8.
Someone has started to try to introduce 'var' and is getting push back from others. "it doesn't match current style" and "could be confusing" were some 'concerns'.
When you're trying to do
InternalFormatParserCustomForClientABCDEF customerFormatParser = new InternalFormatParserCustomForClientABCDEF(param1, param2);
you really start to hit readability limits, and formatting things like going over nominal line lengths and such.
var customerFormatParser = new InternalFormatParserCustomForClientABCDEF(param1, param2);
is certainly easier on the eyes, and the compiler can still known and infer what type 'customerFormatParser' is.
Yeah, the same debates happened in C# back when the 'var' keyword was introduced. People just fear change, but once they got hands-on experience they realized the value.
I see a lot of comments on HN the last few years that just assume the dynamic vs static debate was settled at some time. It never ended. Static typing merely became trendy because dynamic typing was the trend for many years (Ruby, PHP, Python, Perl, JavaScript). Like all fashion, things that are trendy will once again become untrendy, and the cycle repeats.
With that said, it's important to note that type inference is not new. I was doing type inference with Scheme nearly 20 years ago. I believe the reason it never took off in a serious way is because it combines all the downsides of dynamic typing with the downsides of static typing. Types are meant to document code. Without the annotations, you can't look at code and know what is going on. Which, ironically, is the complaint against dynamic typing. In addition to that, you get the pain in the ass of having the compiler always complaining. And because it's inferring types, the error messages are towers of baffling nonsense. It's the worse of both worlds.
One thing that never comes up in these discussions is the idea that creating good types is a skill itself. Much like naming variables, if you don't design your types correctly, your entire code base suffers. It's much easier in a dynamically typed code base to "nudge" the data in a certain direction than in a static type system where all types are locked down at initial design time. In addition, certain type systems are much harder to master than others. I've worked with dozens of TypeScript developers and not a single one really knew what they were doing. They were appeasing the compiler and little more than that. There are also plenty of footguns in TypeScript that even TypeScript experts continually forget. I could continue on the weakening of the value of TS (via "any" and "ts-ignore", etc.) and how these completely muddy any sort of metrics one may have on deciding whether types are "worth it" or not (or the fact that such metrics do not, in fact, exist). But that's enough ranting for one day.
> Without the annotations, you can't look at code and know what is going on.
In Rust, type inference is only inside the function, which I think gets you the best of both worlds.
> In addition to that, you get the pain in the ass of having the compiler always complaining.
This has stopped so many dumb errors of mine. My types aren't complex enough to guarantee that my program is correct, but they're complex enough to at least know that my program makes sense.
I take this one step further: Having a language with rich-ish types and good error messages like rust means I can very often rely on the compiler to tell me how to fix my dumb mistakes. In other words I know where I can be just as if not more sloppy than in an interpreted language and actually get away with it for little effort. I spend a little time getting the parameter and return types as I want them, quickly write an implementation without thinking too much about references and lifetimes then let the compiler work out the details pretty much automatically.
My experience learning Perl and Python after C/C++ and C# was okay I really get the obsession with unit tests now.
One thing notable in C you usually don't have to type the type name twice. But C++/Java/C# you do. That's when I think about it was a really bad grammar mistake. Java and C# shouldn't have propagated it.
Mentioned above I agree with, designing good types is non trivial. I think that's part of the problem that designing good API's is non trivial. I can see programmers getting the hate on when dealing with code bases with crappy types and crappy API's.
> Types are meant to document code. Without the annotations, you can't look at code and know what is going on
With the rise of VSCode IntelliSense/JetBrains code inspection, do you believe this is still true today? The programmer now has easy ahead-of-time access to inferred types that used to become available only at compile time or runtime
Should it be expected that all programmers use these text editors and have access to these tools?
As someone that likes to keep his editor simple (to an extent--I'm using VIM after all), I always get frustrated when people try to introduce policies or procedures that work for them and their preferred setup, and who look at me as an obstacle because I prefer a different setup.
I'm of the opinion that code should be written independent of the tools used to understand and modify that code. If there's anything about the code that needs to be communicated, it should be communicated via the code itself, whether through naming patterns, comments, types, or any other methodology that can be encapsulated in a text file.
Other than letting each developer have their own preferred processes and coding environment, it also makes it easier to SSH into a remote box and know what's going on. A quick google shows that VS Code does allow for SSHing and browsing the remote files via VSCode. That's nice, but I don't know how well it works, and how much I like the idea of allowing another program to run commands on the remote box. I like that I can SSH into a box and use the tools natively available there to read and modify the code, and that the code is prepared in a way that makes it as easy as possible.
> Should it be expected that all programmers use these text editors and have access to these tools?
If they don't have access, then the tools ought to be standardized to the point where they can be integrated into any editor. The Language Server Protocol[1] seems like a step toward this.
> I'm of the opinion that code should be written independent of the tools used to understand and modify that code.
This a widespread, "common sense" opinion that I've come to disagree with strongly. No one would argue that, e.g., illustrators, 3D modelers, music producers, etc. should be so tool-agnostic—and yet their situation is quite similar. One could produce a complex piece of music in Audacity instead of using Logic or Ableton, but musicians don't have the same mentality of picking the cheapest, most austere, or lowest-common-denominator tool. Instead, they invest in tools that enhance their productivity. And that's precisely what's at stake here. Pairing (a) a language that allows implicit "smart" features like type inference with (b) an equally smart editor to make what is implicit in the code explicit to the developer as needed, is more productive than forcing the developer to make everything explicit themselves.
Re: using VIM over ssh, your choice of scenario is revealing. Why would you limit your everyday development work based on the lowest common denominator tool you're forced to use in an emergency? Also, it's not necessary to run code inspection on the remote box. JetBrains IDEs, for example, will copy a folder from ssh or a similar environment, index and inspect them locally, and then sync them back as needed.
Well, to turn around your question: should languages enforce verbosity to satisfy a vocal minority using ancient tools? I'm not going to tell you what tools to use, but if your tools can't understand let/var/auto, then that's not my problem.
Upvoting so that people who need this may see it and benefit from it.
However, it's not that I'm not aware of features available to my editor of choice, it's that I specifically don't want an editor with those features. I don't want that functionality as part of my workflow. I prefer to reduce the noise and distraction so that I can keep concentrating on what's currently important to me.
Bringing this back to what the root parent was talking about, a significant part of code maintainability comes down to how we design our classes, services, etc. It's not so much about static or dynamic typing--both can experience their fair share of problems--it's about approaching our code in a way that makes it easiest for future readers and maintainers to pick up where we left off. That's a difficult task, but one that makes a huge difference. Saying that specific editors can alleviate some of those problems misses the point: that the underlying code itself is not well designed. What I meant to add is using these editor tools not only fails to fix the underlying problem, but that it also forces developers into tools they may not want to use.
I think you point to an important tradeoff. I also think it's clear the industry is leaning toward the decision that it is not worth giving up the advantages of static typing for the 0.1% of users who really don't want type info in their editor.
Your distinction between the code itself and editor-based tools I think is a false one. The types are part of the code, and while one can use them tolerably well on the command line alone, they are most helpful with things like type hovers. The line is blurry between language features and code structure on the one hand and the editor tooling that gets the most out of it on the other.
i tend to agree with you. maybe more than just agree.
if any of these "IDE's", or "tools", or whatever they are called actually provided a universal improvement in software quality and development time, then i would change my view.
experienced developer, the tools don't make a difference. VSCode, VisualStudio, Eclipse, tried many of them. This is from my experience, it may or may not be universal.
It's weird to make this argument in a thread about how type inference is only problematic if you aren't using a modern tool that understands the code at a level higher than raw text. Clearly the tool is making a difference.
> VSCode, VisualStudio, Eclipse, tried many of them. This is from my experience, it may or may not be universal.
Exactly. If specific tools allow specific people to work better, great. I completely support them. But it's unfair to say that what works for some will work for all.
My goal when adapting practices is to adapt the practices that allow each developer on the team to work in the way that's best for them, and to avoid rules that limit developers in their choices.
Always willing to update when a clear case is made, though. Recently I stopped my rule of "80 characters per line max" because I don't think 80 character wide screens are common enough to warrant my consideration. Now I limit line length based on what makes that line of code most easily digestible--whether 30 characters, 80, 140.
These features are now available in all common text editors.
Also keep in mind that you're missing out on many other arguably essential tools such as debuggers, smarter shortcuts, and other static analysis.
Therefore, yes, I think it should be expected of a programmer to pick the right tools for the job, in the same way that it can be expected of a designer to be able to work with Adobe files.
If classic VIM doesn't offer these features, then it isn't sufficient as a code editor anymore.
> These features are now available in all common text editors.
It's not just a matter of whether they're available, it's a matter of whether it's a fair expectation.
I've been developing for a decade and never found that I'm "missing out on many other arguably essential tools". Typically I'm as productive or more productive than my peers.
I get that you think usage of these features is a fair expectation. Can you provide your argument for why you think that's a fair expectation?
Working with code as if it is raw text is strictly inferior to working with code as raw text + AST. If that’s how you want to work, that’s fine, but it’s probably not good to choose your team's technologies because you want to work at that lower lever of abstraction.
> Typically I’m as productive or more productive than my peers.
This seems like a case of assuming the conclusion, tho. Whether a text editor-based workflow is as productive as an IDE-based workflow when avoiding feature that advantage the IDE doesn’t impact on whether the IDE-favoring features are valuable enough to adopt and assume everyone has access to.
I think yes. I do F# for a lot of time, and looking at past code I can't just figure some stuff. Not only their type system look alike dynamic, it make you build some stuff that is IMPOSSIBLE to get without a serious look at the types: And the types are invisible and behind abstracts constructs.
With python, for example, I can't at first see what a function need but at least see the body give huge clues, because python rely less of abstract type stuff (it have a issue with monkey patching and delayed build of objects BUT, "if look like a duck.." is most of the time enough to get things..
F# lets you specify the types of function arguments and return values. If it will make your code easier to read and reason about, why not just do that?
I use Ocaml a lot, which is very similar to F# as you know... and I document the types of all toplevel values in my code. Not because the compiler needs it, but because it helps me navigate the code more easily.
Yeah, but it is not very idiomatic and if I need to put types everywhere, but is the point of it at the end? Is like people that type python: Is screaming is the wrong tool for the job :)
I don't think dynamic typing will ever swing back. They came out of an era when types were expensive and had moderate benefit. Now they are cheap (in modern languages) and have significant benefits. There will always be things like shell scripts and stuff, but I don't expect to ever see a big language without a good typing story ever again.
The problem to solve now is to lower the costs more and raise the benefits, not to try to eliminate them.
Python/JS/PHP/Ruby are still plenty popular... but the weight they are putting on "dynamically typed" is decreasing over time. Basically, all the dynamic languages are creeping towards the statically-typed side with various partial annotations and optional features. But there are no statically-typed languages I know of rushing to add more dynamically-typed support to their languages. If you draw all those language trends out, they don't meet in the middle; they meet solidly in the "statically-typed" area.
I phrased that carefully; obviously they aren't going to stop being "dynamically typed" under the hood. But, slowly but surely, those language's contribution to "dynamically typed" is decreasing, and I expect, will continue to decrease.
In another 20 years, I expect "dynamically typed" will be looked at as a complete mistake by the oversimplification process of history, as the number of people who were around when they were getting popular and understand why they were so attractive decreases. I do, myself; I experienced being liberated from some really, really crotchety languages by the freedom of Python 2.0, and I understand why the people of the time analyzed the programming landscape, and decided that the problem was "static typing" rather than "bad static typing", because there weren't any examples of good static typing. But now there are, and they aren't going to go away, and I expect that barring legacy languages, the choices in the future are going to be simple static typing like Go, complicated static typing like Rust, really complicated dependent typing like $LANGUAGE_YET_TO_BE_BUILT, and tiny languages like shell designed to never write programs in them large enough for typing to even matter.
I’m not so sure about that, the reason is that the total number of programmers in the world are just increasing and more importantly more and more non-programmers program code today than ever before.
It hard to convince non-programmers to see the beauty of a type system when they only want to print some html.
So what I see is a bright future for languages that can do both, types or no types.
PHP is quite weak in the U.S, I can only assume that's why you got the impression it's dying. In Europe it couldn't be more popular. I'm not here to "defend" PHP, I use Ruby, but living in the Netherlands you'd be amazed how many companies use it. Ruby is a very small niche here in comparison.
> I see a lot of comments on HN the last few years that just assume the dynamic vs static debate was settled at some time. It never ended. Static typing merely became trendy because dynamic typing was the trend for many years (Ruby, PHP, Python, Perl, JavaScript). Like all fashion, things that are trendy will once again become untrendy, and the cycle repeats.
It’s because the ergonomics of the previous generation of (mainstream) languages was cumbersome. No sum types, no type inference, nullability all over, terrible error messages, abhorrence of expressions, etc. some of those things are only indirectly related to static typing, but many people mistakenly attribute them to static typing nonetheless. The new crop of mainstream statically typed languages wed these quality of life improvements with the rugged practicality of the previous generation of mainstream languages (none of these features are new, but no serious team is going to switch from C++ to scheme for the type inference alone).
> With that said, it's important to note that type inference is not new. I was doing type inference with Scheme nearly 20 years ago. I believe the reason it never took off in a serious way is because it combines all the downsides of dynamic typing with the downsides of static typing. Types are meant to document code. Without the annotations, you can't look at code and know what is going on. Which, ironically, is the complaint against dynamic typing. In addition to that, you get the pain in the ass of having the compiler always complaining. And because it's inferring types, the error messages are towers of baffling nonsense. It's the worse of both worlds.
Type inference is very much a “sweet spot” thing. Like many things, Rust nails this: you have to annotate struct fields and function arguments, but within a function body you get inference. Changes outside of the function don’t result in type errors inside of the function. Locality is key.
> One thing that never comes up in these discussions is the idea that creating good types is a skill itself.
This is an interesting point, and I agree; however, I think the issue is less that it’s hard to do and more that dynamic typists don’t see it as a worthwhile activity at all—“why should I try to create good types? I just need to get this happy path working so I can get on with life!” Of course there are abundant good reasons (we should care about quality and maintainability and not just superficially churning through feature tickets at the expense of all else), but I think this is a sort of fundamental disagreement between dynamic and static typists.
>I see a lot of comments on HN the last few years that just assume the dynamic vs static debate was settled at some time. It never ended. Static typing merely became trendy because dynamic typing was the trend for many years.
Not always. If you examine history not everything is a cycle, depending on what you look at human efficiency improves as well.
Society goes through natural selection. The cultures, methods and behaviors that help us survive live on while methodologies that aren't as good tend to get eliminated.
The cycles in the process occur in areas not under selection pressure. It's called genetic drift and mutations in this area can occur willy nilly in random steps or even cycles if it doesn't have an effect on survival. The pressure in this case is survival of a business.
There's not enough cultural data on dynamics types vs. static types, but I feel in general the dynamic type thing was a mutation. Dynamic types were natures trial at a baby born with one kidney instead of two because one kidney is more energy efficient to maintain. Now it's being naturally selected out.
We'll never know for sure unless you live long enough to see what happens to programming in the far future. For something to be truly called cyclical it must be adopted by huge amount of businesses and eliminated and recreated multiple times.
Both classic static and dynamic typing are trending over time to being replaced with the option of static typing with gaps; languages with static typing are more frequently implementing dynamic escape hatches and languages with dynamic typing are getting optional static type checking tools, and both static-first languages and optional typecheckers for dynamic-first languages often have fairly robust type inference, so the firm bright lines that used to exist between static and dynamic languages of the experience of using them is quite a bit blurred.
Yes, but overall static typing is trending again in the sense that more and more static typing is being used... just a little less strict because of the trapdoors as you say.
> reason it never took off in a serious way is because it combines all the downsides of dynamic typing with the downsides of static typing. Types are meant to document code. Without the annotations, you can't look at code and know what is going on.
Type annotations aren't for me, they're for the compiler and the IDE.
If I wan't to know whats going on inside a variable, I cmd+hover over it.
I actually wish you'd written more about this, this is important and doesn't get a proper discussion.
If you could come up with some code examples of the problems you hinted at it would make a beautiful blog post.
> I believe the reason it never took off in a serious way is because it combines all the downsides of dynamic typing with the downsides of static typing.
Are you talking about scheme? It didn't take off for the same reason all other functional languages didn't take off. Why functional languages aren't as popular, who knows.
> Types are meant to document code.
Not true. Comments/documentation are meant to document code. Types and type systems exist to constrain the program space to produce sound programs. Type systems limit the number of valid programs. Types and type systems do not exist to document code.
I don't have to look at my static code to know that it is doing the wrong thing. The compiler tells me. This is impossible with dynamic languages. It is obvious that you never used types otherwise you wouldn't have come up with this nonsense.
I started off in PHP. It was wild. Anything could be anything. Refactoring was a nightmare. Our codebase was littered with mystery variables like $hold, $hold1, and $holda, which were re-used all over the place. (Granted, this two other problems entirely orthogonal to types.)
Then I got a job at a Java place. My god, it was beautiful. I could suddenly know what arguments functions were expecting, even if I hadn't seen them before. I could be instantly aware if I was trying to use the wrong variable somewhere, or pass in invalid data.
It was as if someone lifted me out of a dank cellar where I had previously subsisted on whatever mushrooms the rats didn't deign to eat, into a brightly lit dining room full of steak and pastries.