Even code I wrote six months ago.
Not having to dig through 6 functions deep to try to figure out whether "person" is a string, or an object, and if it's an object what attributes it has on it etc. is huge. And not to mention that some clever people decide - hey, if you pass a string I'll look up the person object - so you can pass an object or a string - which makes all sorts of convoluted code paths when someone else was looking at "person" and only saw one type so now their function doesn't work on both types etc.
I hate having to waste time figuring out the type of every variable and hold it in my head every single time I read a piece of code.
Doing radical refactoring often involves just making those changes and then fixing all the IDE or compiler errors until it runs again.
Now, I know, it's not true. It's entirely possible to build weird things in python that are provably impossible to typecheck statically. But modern language servers and their type inference capabilities in rust, terraform, or even straight up python are very impressive.
Static typing type checks are compile time, dynamic typing doesn't. I don't see how these two could be indistinguishable, in one you can't run the program with type errors, in the other you can.
def compose(start, *args):
def helper(x):
for func in reversed(args):
x = func(x)
return start(x)
return helper
There is no mainstream typed language which can write a fully general type for the vararg compose function. TypeScript is probably the one that comes closest, but last I checked it still was unable to write a sufficiently powerful array type. You can write a type for a version of compose with a fixed number of arguments, but not for one working over an arbitrary number of arguments.I feel that too much focus is on static languages like C/C++ where types become a chore and judging it on that rather than looking at the plenty of languages with type inference brought by ML-style languages.
I use the Python REPL quite often, and have non trivial experience with Lua’s. But the best experience I’ve ever got was with OCaml: I type the expression, or function definition, without giving type annotations, and I get the type of the result in the response.
You wouldn’t believe the number of bugs I caught just by looking at the type. Before I even start testing the function. And that’s when I don’t have an outright type error, which a dynamic language wouldn’t have caught — not before I start testing anyway.
I know this isn't the approach of choice for most folks but hey - I'm working with ADHD so I've got to make some allowances for some neurodiversity.
When I use a dynamic language I get no errors in dev, I need to run/invoke the program to see if it works. It may appear to work fine as I haven't executed a specific code path hence dynamic languages have extremely high test coverage. With dynamic languages I am delaying my feedback loop, I may get some visual output quicker but that doesn't mean my program is correct.
With a strongly typed language and utilizing types you use the compiler to guide you. The compiler says hey, this isn't correct, fix it, you go fix the error and recompile and repeat.
I've used Elm before and it's the only time I had a complex Javascript UI just compile and work first time. It's like a wow, did that just happen.
With Typescript it's not quite to the level of Elm but find my experience working with React etc far more productive. Typescript says hey, that's wrong, I expect ... you gave ..., you work through the errors and when it runs generally there's less silly mistakes than when I just use Javascript.
I'm learning Rust, the compiler error messages have greatly helped. When you compile it says hey, you tried to do ..., maybe you want ... instead. Not to sure what the suggestion is I try it and 9 times out of 10 it works, compiles, program runs.
With types you generally get better IDE auto complete support etc.
Now i'm using Python for my day job. My experience has been painful, discovering what arguments functions take, passing in wrong values, needing to run slow test suites, finding errors at runtime. Yes you can use type hints and I do but I find them far less reliable.
I guess I'm not a very good programmer so learnt to lean on a compiler to do the hard work for me, and if you have good type support you can lean on types more to get the compiler to help you more.
In Haskell I can write complex logic by writing out the types and ADT's. I've written whole programs with tests to verify the logic without writing a program. I find this incredible efficient for prototyping ideas, just write the types, the functions signitures etc etc. Once that is done you implement the functions, hit compile then boom, your shocked it just worked first time running.
Last week, i could've done either a dataframe, a list of list, a list of tuple, a dict of tuples, a dict of lists (this was a bad idea that did not survive more than 2s in my head) or a list of dict. I started coding with a dataframe in mind (i guess i wanted to show off my numpy/pandas skills to my devops colleagues), but adding type hints to my prototypes shut down the idea pretty quick: lot of complexity for nothing.
Yes, I'm a total scatterbrain. Types let me remind myself later that I did in fact forget what I'm doing and what I did. It lets past-me protect future-me.
The IDE was my logical buddy, and every idea's possibility was rapidly shown with it. And I need to massage things a bit, I go faster because I know what's missing.
The only time I liked eclipse/java :)
After university, the opposite became true. No difficult to diagnose undefined behavior because of ambiguity in typing.
if you write dynamic OO languages with a static mentality in mind, i.e. you try to enforce some sort of global type expectation before the program runs, then obviously static languages are better, because you're trying to write static code.
Benefiting from dynamic languages means ditching that mindset altogether.
If a codebase doesn't have static types, it damn well better be set up to be highly grep-able. Including dependencies and frameworks.
This is why Rails pisses me off so much. No static types to help you out, and you can't grep (can barely google, even!) methods and properties that aren't defined anywhere until runtime. Is this from core? Is it from some 3rd party gem? Well fuck me, this file doesn't even tell me which gems it's relying on, so it could be literally anything in the entire goddamn dependency tree.
This is so important.
It is also the reason why I like global variables. They are accused of making a spaghetti mess but ... in my experience the opposite is true.
Fancy patterns are way worse to reverse engineer than simple flat long functions accessing globals. Easy to debug too!
As for greppable though...then you may as well be using a static language. The point of a dynamic language is to be dynamic, ie you can do those things at runtime.
With Rails you have the option of pry-rails, and you can get a list of descendants of important parent classes like ActiveRecord with this: https://apidock.com/rails/Class/descendants
With the combination of vim, rspec, pry, fzf, and ripgrep, it's possible to become quite comfortable refactoring pure Ruby and Ruby+Rails code. But it does take some time to learn how to navigate the Rails runtime code generation magic. The more magic the code, the more you might have to use a debugger to break on method definition, but Ruby's dynamicism lets you do that.
On the topic of frameworks with a lot of magic, having used both Rails and Spring Boot (with Java and Kotlin), I'll take Rails any day. It was way easier to introspect Rails codegen magic with Pry, than Spring's codegen magic with IntelliJ. With Spring Boot, even with Kotlin, we had the burden of semi-manual typing, but lost a lot of the benefits because a lot of DB interaction and API payload handling was still only runtime checked.
Also, type hints really help your IDE, even catching errors before you even run tests.
There's also a visual cue that you are doing something wrong: If a function returns 4 levels of Union[Tuple[List[int]], Optional[str]........ Then you are doing something too complex and the function should be broken up.
Tangentially related: I think it'd be cool if there was a development environment that combined a node-based dataflow editor with normal text editing, so pure plumbing could be implemented visually, but embedded within (and translated to) textual code.
Do you have hints on how to avoid being one of those 10x clever programmers while programming a prototype? I find that I am most likely to write functions like that when there's some variables that I don't want to pass 5 layers down the call stack and then, in your example, would accept either a string (in which case those variables use their default values) or the Person object, where the variables are pulled from the Person's attributes.
> I find that I am most likely to write functions like that when there's some variables that I don't want to pass 5 layers down the call stack
I agree for a prototype, there are some tradeoffs to be made. However, very often prototypes can end up becoming production. Temporary decisions often become permanent ones. Just something to keep in mind.
This is a very different mindset, but once you adopt this style, the lack of static types isn't as big an issue.
The reason you can do this in a dynamic language is that you can very easily adapt one structure to another, so its okay if not all your functions work directly on the same shared structures.
It also has the advantage that this style really favors making modular independent granular components that can be reused easily, because they aren't coupled to an application's shared domain structures, but to their own set of structures, creating a natural sub-domain.
There are other aspects to make this style work well, like keeping call-stacks shallow, and having a well defined domain model at the edge of your app with good querying capabilities for it.
Concretely it means say you need to add some feature X to the code, you might think, ok this existing function is one place where I could add the behavior, but for my new feature I need to have :age of "person", but I don't know if the "person" argument of this existing function would contain :age or not. Dammit, I wish I had static types to tell me.
Well, in this scenario, instead, what you do is that you don't add the behavior to that function. Instead, in my style you would have:
A -> B
A -> C
instead of: A -> B -> C
That means if after B is the right place for your logic, you don't do: A -> B -> B' -> C
And hope that the "person" passed to B had the :age key which is needed by B'.Instead you would do:
A -> B
A -> B'
A -> C
And when you implement B', you don't even care about "person", you can just say you need person-age, or that you need a Person object with key :age (which you don't care if it is the Person object shared in other places or not).Finally, you modify A, where A was the function that creates the Person object in the first place, it has direct access to your actual database/payload and so finding whatever data you need is trivial in it.
This would never fly in a code review in any of the companies I've worked for.
function find_user(person) {
if user is string {
query_by_name(person)
} else {
query_by_name(person.name)
}
}
and yeah, we all know it's kinda messy, but also that logic has to live somewhere and we need this feature asap so it passes code review. I wrote a test for it, ship it.In a static language, you either can't do it, have to really go out of your way to do it, or at least do function overloading (which is a bit cleaner)
But all it takes is a method that expects an integer Id to receive a string representation of said id because of some obscure path in code that notwithstanding your 100% line coverage the team is so proud of, was never exercised on tests because nobody can have 100% branch coverageSuppose I call fire(bob). Programmers from other languages might reason that since fire is a function which takes a Person, bob must be a Person. Not in C++. In C++ the compiler is allowed to go, oh, bob is a string and I can see that there's a constructor for Person which takes a string as its only argument, therefore, I can just make a Person from this string bob and use that Person then throw it away.
To "fix" the inevitable cascade of misery caused by this "feature" C++ then introduces more syntax, an "explicit" keyword which means "Only use this when I actually ask you to" rather than as a sane person might, requiring an implicit keyword to flag any places you actually want this behaviour to just silently happen.
This way, hapless, lazy or short-sighted programmers cause the maximum amount of harm, very on-brand for C++. See also const.
What I really wish existed was a built in way to cast and validate, or normalize and validate. I never care if something is a string. I care that if I wrap it in str(), or use it in a fstring, the result matches a regex. Or if I run a handful of functions one of them returns what I need.
The only benefit I can see of type hints on their own is it makes it easy to change a callable's signature, but I think that's best avoided to begin with.
The problem the DWIM approach to APIs is that when you go out of your way to "do something reasonable" with absolutely any kind of argument type, leaving the caller's intent implicit, you will sometimes run into combinations that "work" in unexpected—and often unwanted—ways.
For example, say you have a function which returns either a Person object or, in very rare cases, an error string. Moreover, you fail to check for the error string, and pass the result into another function which expects a Person object but will also take a name and look up the corresponding Person object in a table. Now if the first function fails you're left trying to look up an error string as a name, with no obvious signs (such as a type mismatch error) to show that anything is amiss.
It's important to make the intent explicit, and not just let the function guess. One option compatible with both statically- and dynamically-typed languages is to provide two functions, one requiring a Person object and another taking a name string. This is still perfectly ergonomic for the user and mitigates most of the potential for confusion.
You can't say "we simply don't allow bugs!" because it's a lie. Why rely on a another person manually checking for silly mistakes when the computer can do it for you?
For the same reason, I’m not a fan of type-inferring variable declarations.
In an IDE you can get the type annotation from the IDE over every inferred var type, but I don't like requiring an IDE to see that information and like it showing up in 'less' as well.
I think the best experience is having a language server annotate the inferred types (like how rust-analyzer does it.) But even then, it can become hard to read code on GitHub or somewhere where tools are not available. Granted that's becoming less and less of a problem, and even GitHub allows using some VS Code extensions now.
Javascript is by far the worst offender here with its ignoring extra arguments. Javascript functions that totally change effective type signatures based on number of args are the devil's work.
I'd argue that if the types that a function accepts are not easily defineable than you're doing dynamic typing wrong.
More, the nice easy things to build with major restrictions pretty much gets thrown out the window for complicated things that have constraints that most efforts don't have. This isn't just a software thing. Building a little shed outside? Would be silly to use the same rigor that goes into a high rise. Which would be crazy to use the same materials engineering that goes into a little shed.
Take game systems, as an example; far far more effort will be spent in the art and general asset management than is true for many business software setups. Which is why many of the business best practices haven't necessarily moved over to games.
Similarly, look at the general practices around building and maintaining bridges in physical world. We call all bridges by the same name, but reality basically dictates that what works in some locations cannot and will not work in others.
Now, you are right that we can grow large software out of smaller in ways that the physical can't do. But, it is a common fallacy to stall out a project by trying to be at google's scale from the start. Ironic, in many ways, as not even google was built to be at their scale from the start.
And I should have leaned in on how much is still left to implementation in terms of "shed." From weather, to what is being stored. It isn't like there is a universal shed design that will make everyone happy.
Nor is this saying that some things aren't truly valuable. Just recognize that some places they don't help as much as you would like. This isn't saying they are bad or worthless. Just acknowledging that they are oversold.
What we are observing here is „the market fixing it“.
The process is messy and redundant, but effective.
I don't think it's a matter of reinventing the wheel, in this case, more a matter of bolting something like a wheel on a system which didn't start with wheels.
I see static types as one of the most powerful communication tools around, as far as code goes. I can't relate at all to people complaining that they waste time. They must work very differently from how I do, is all I can figure. It's that, or they don't realize how much time they're losing to communication-related tasks, or refactoring, or writing (and maintaining!) extra or more verbose tests, or having even one more bug per year make it to production, or whatever, that'd be saved by static types, so aren't correctly accounting for the time savings. One of the two.
(1) Low-level, static types
(2) Low-level, dynamic types
(3) High-level, static types
(4) High-level, dynamic types
For whatever reason, historically #1 and #4 have been most popular. C, C++, Pascal, Ada, and Java are #1. Python, JavaScript, Perl, and BASIC are #4.
There haven't been a lot of #2 or #3 languages. Some #3 languages (TypeScript and Python with types) have come along, but relatively recently.
A person who experiences only #1 and #4 might notice that they can whip up programs faster in a #4 language than in a #1 language, then falsely attribute that difference to the static types. Whereas the real reason is working at a different level of abstraction.
But it's something you just have to get used to, and now that I understand it much better I feel more productive and have more confidence in my code. And the communication aspect is definitely a great help too. No handwritten documentation can be this consistent, completely independent of who touched the code (though to be fair, it's still difficult to get the naming right).
I can't imagine the people calling it a waste of time got over the hump in the beginning. To me it's obviously a timesaver. It does a tedious, difficult (for humans) task and does it quickly & with perfect accuracy. Beforehand worrying about all the type signatures and interfaces felt like 4D Sudoku across various modules, now I can concentrate on the interesting parts.
To me, ideally, types are supposed to be a benefit not only in safety, but in understanding the intent of a piece of code more quickly. For an api or library interface, review the types to see what its intentions are.
But there's something about the typescript type system, with all the picks and keyof and typeof... sometimes it just feels like it's way too easy to go overboard, to the point that it occludes meaning. I understanding struggling with types if you're struggling with figuring out exactly what your boundary does and does not allow, but when you're struggling with types just because you're struggling with the kabillion different ways that some other typescript programmer chose to use the utility types... there are times when I feel like even Scala is easier.
Depends of course a lot on the codebase but all typescript codebases that I've seen so far and considered "well-maintained" didn't really use keyof and typeof all that much. The only way I can imagine how one ends up with lots of those keywords is when you start with a dynamic language approach, and then tell the compiler afterwards what that type might be, instead of defining the type beforehand - might that be the issue?
This is huge for me. As someone who takes on already completed projects, it's a huge help with debugging and understand what's going on without requiring you to know the whole system forward and backwards. Sure, you still need to build a mental map of the general code flow, but you can look at a single function and clearly see the obvious inputs and outputs. Combine that with a a stack trace and you can debug that method as a single unit and then start to look at where it's called and what its downstream effects are. You don't need to start from the very beginning of the call and then follow it through, keeping mental track of what is available and in what form when and where.
I'll add for folks thinking about this transition that we took a pretty different strategy for converting Zulip to be type-checked: https://blog.zulip.com/2016/10/13/static-types-in-python-oh-...
The post is from 2016 and thus a bit stale in terms of the names of mypy options and the like, but the incremental approach we took involved only using mypy's native exclude tooling, and might be useful for some projects thinking about doing this transition.
One particular convention that I think many other projects may find useful is how we do `type: ignore` in comments in the Zulip codebase, which is to have a second comment on the line explaining why we needed a `type: ignore`, like so:
* # type: ignore[type-var] # https://github.com/python/typeshed/issues/4234
* # type: ignore[attr-defined] # private member missing from stubs
* # type: ignore[assignment] # Apparent mypy bug with Optional[int] setter.
* # type: ignore[misc] # This is an undocumented internal API
We've find this to be a lot more readable than using the commit message to record why we needed a `type: ignore`, and in particular it makes the work of removing these with time feel a lot more manageable to have the information organized this way.
(And we can have a linter enforce that `type: ignore` always comes with such a comment).
So rather than write them all by hand, just get your tools to do it.
That legibility to the computer is what makes them much better than documenting the same thing some other way. Are they out of date? Were they wrong to begin with? The computer will tell me, no action needed on my part. I need to look up something in the context of what I'm reading right now—oh, look, the computer just told me exactly what I needed.
I think the people debating it never tried it seriously.
I think it ruined a lot of people to static typing and exceptions because Java is/was terrible for both of those things.
Almost every Python user now has to "deal" with type annotations. It's tempting to gradually add type annotations, it's nice documentation.
But it also rubs me the wrong way to have annotations that are never checked(!). In many codebases, you might just have "casual" style type annotations in Python, and nothing ever asserts that they hold. That's nagging on me, a bit.
Also, tooling like https://pydantic-docs.helpmanual.io/ can do runtime checking for important parts of your app or you can use this https://github.com/agronholm/typeguard to enforce all types at runtime (although I haven't measured the performance impact, probably something to do in a separate environment than production?).
(edit: corrected "Linda's" to "Pandas" heh, mobile kbd)
"The benefits of explicit typing are obvious and clear but they downsides are subtle and hard to communicate"
I still think typing in general is a net win but I'm not sure whether static typing is. You find yourself writing code that just wouldn't be neccesary in a dynamic language - and I don't just mean the direct code you write to declare and cast types. There are more subtle costs.
I need to spend time with a good type inference in a language with modern typing and dynamic features to sort out how I feel about this.
I believe it really has to do with the size and complexity of modern projects. With a half-decent IDE you could sort of used non-type-checked Python in 2012, but times have changed, and now we are talking about statically checking Python and Ruby. And Javascript, of course, now has it in form of TypeScript.
But if I had to pick either a language without any type hint/inference or a verbosely strictly typed language - I would must rather use the strictly typed language.