It’s true that with static typing, you are usually forced to propagate your changes to “your whole codebase” to get it compiling before you can run any of it. That stinks. However, it turns out you can lift this restriction in static languages, and this is what Unison does:
I haven't given Unison a try so I won't dismiss it outright but to me having to explicitly propagate your change to your entire codebase before you can run anything doesn't suck at all, it's actually the killer feature of statically typed languages.
I've written big applications in python, every time I made a significant architectural change (which might not even be huge code-wise, just far-reaching) I feel super uncomfortable. I know that I have to follow up with an intense testing session to make sure that I go through all code paths and everything still works correctly. Even then sometimes the testing is not thorough enough and you end up with a regression in production.
As a result I tend to avoid these types of changes as much as possible, even I believe that the application would benefit from them.
Meanwhile in Rust it's a breeze. Yesterday I decided to make a small but far-reaching change in an emulator I'm writing: instead of signaling interrupts by returning booleans, I'd return an "IrqState" enum with a Triggered and an Idle variants. It's more explicit that way, and I can mark the enum "must_use" so that the compiler generates a warning if some code forgets to check for an interrupt.
It's like 4 lines of code but it means making small changes all over the codebase since interrupts can be triggered in many situations. In Python that'd be a pain, in Rust I can just make the change in one place and then follow the breadcrumbs of the compiler's error messages. Once the code builds without warnings I'm fairly certain that my code is sound and I can move on after only very superficial testing.
I’d read through the link and see what you think, but TL;DR is that you can still propagate a change everywhere in Unison (and Unison tracks this for you), but even if you’ve partially propagated, you can run all the code that you’ve upgraded so far. So if a change has 500 transitive dependents and you upgrade 7 of them, you can run those 7 (and this might help you figure out that you want to do the change a bit differently) Whereas you’d typically need to upgrade “the whole codebase” before being able to run anything, including code unrelated to the change being made.
Surely if the code was unrelated then you wouldn’t have to change it? It might be tangentially related to the change at hand but the fact it breaks means it must be given consideration and not just as a “geez I have to update all these cases” but also in terms of how the change impacts them.
I’m sympathetic to the idea that you want to solve the immediate problem first rather than have to move the entire codebase through several iterations. I’d be interested how well Unison handles that on a complex codebase in reality though.
The idea is that you wouldn't need large refactorings in more dynamic languages in the first place since you are operating on a different abstraction level.
In the JS based project i recently joined (previously i was writing C# ), what i notice is that in a team of 6 people 4 are doing refactorings every sprint for the past 8 months.
This is talking about the case where you're just testing something out – prototyping it for a few branches before implementing it for the whole thing. In this case, _you_ are your own user, so it doesn't matter if you see the type errors.
If I’m making a potentially wide-reaching change, I’d want to test it against everything else so I know for sure that it’s actually viable in the context of every instance of its use.
Also, some statically-typed languages (like Haskell) allow you to defer type errors to runtime if you really want.
Thanks for pointing me to Unison, I have yet to dig into the details, but its core idea is something I've been thinking about lately and it's fantastic to see that it exists!
To be fair most of the time that type change propagation is just manual work guided by the compiler. Some smart IDE would even be able to make the changes for you.
https://www.unisonweb.org/docs/refactoring
In Unison you can implement a change and propagate it just far enough to run one little experiment. There’s no need to upgrade all your code at once.