4 ms·
This doesn't feel consistent with my experience. Significantly restructuring a large and complex piece of infrastructure is never going to be easy, but I don't
by nilkn 10y ago
This doesn't feel consistent with my experience. Significantly restructuring a large and complex piece of infrastructure is never going to be easy, but I don't think a strong static type system makes it any harder. If anything, it makes it significantly easier, just like a strong battery of unit tests would.
A sufficiently expressive type system will guide you and hold your hand over the course of the refactoring. It will determine for you the precise cascading effect of any code changes you make. And if you forgot to write a unit test to handle some edge case, the chances are pretty decent that the type system will detect it for you anyway.
In terms of writing a new piece of software whose design you haven't fleshed out yet, my approach in a language like Haskell is in fact very experimental, but quite different from something like Python. Rather than writing lots of little tests and trying to get bare-bones functionality implemented passing those tests, I often completely ignore tests and let the type system guide me. I try to flesh out the types that my program will need at a high level and just go from there. I call it "type-driven development", and it's extremely useful for early-stage exploratory programming, because it lets you play around with the structure of the program without worrying about any details. This is only possible in a language those type system is powerful enough to actually encode nontrivial aspects of a program's high-level structure.
I make sure at every point that I have a codebase that passes the type checker. It may not be interesting to actually run it at an early stage, but in many ways that actually makes exploratory problem solving even easier.
In many cases, this approach will reveal architectural issues early on that you may not have realized existed until you'd already written thousands of lines of code in something like Python. The real point is that either way you will have to face those issues if you want to end up with a working piece of software. But it's a lot easier to face them early on, when little code depends on them, rather than later on.
- nostrademons 10y agoI think there's a distinction in exploratory programming between code with known inputs & outputs but unknown data structures & algorithms, vs. code with known algorithms but unknown inputs and outputs. The former encompasses situations like contract development, signal processing, control & embedded systems, compilers, maintenance programming, etc. I've found that strictly typed languages like Haskell, Ada, or Rust can be very helpful here, even for exploratory programming. In this situation, you know (and have little control over) the data that goes into your program and the requirements that it must meet, and if it doesn't do so, it's because you made a mistake somewhere. But many types of exploratory programming involve unknown inputs or unknown outputs. Think of data mining or scientific computing, where the issue is often that you have unclean data and your task is to figure out how it's messed up and how to fix it as quickly as possible. Or UI software (both web and mobile) and tech entrepreneurship, where the big unknown is often how the user will react to the software and where they will find value in it. I've found that dynamic languages are great for this, because the whole point is you don't know what the data will look like, let alone how it will be structured. It was interesting to me that the two alternatives you mentioned were "write lots of little tests" or "let the type system guide what needs to be fixed". For a lot of the exploratory programming I've been doing lately, the most efficient approach is just "let it break". Go make your changes, and if some part of the system doesn't work anymore, just take note of that. Because the act of letting things break gives you important information. If you don't care, then obviously that feature wasn't very important to the system to begin with, and you should rip it out entirely. If you do care, you should fix it, and then write some tests to make sure it doesn't break again. This way, you can build a rough rank ordering of how important features are, so that when you're faced with engineering trade-offs (and you always are), you have a solid, experience-based intuition about what you really need to support and what you can let slide.
- nilkn 10y agoTo some extent, "let it break" is not all that different from "let the type system guide what needs to be fixed." The difference is in what's breaking. The type-driven approach is basically "let the type coherence of the program break." In other words, compile the program, knowing fully well that it won't successfully compile. You see exactly where the types break down, and you can then figure out what needs to be changed. With all that said, I like your distinction at the beginning of your comment a lot. A lot of my recent work has focused very much on a problem domain where the input data is very well understood, but the transformations applied to that data are complex and not all that well understood. I've found that Haskell has worked extremely well for me in this case, but I think you make a compelling point that it may not work quite so well in cases where the input data is the unknown, but the transformations are fairly well understood already. Perhaps this is why languages like Python have dominated the machine learning space, and strictly-typed languages just never really took off there.
- nostrademons 10y agoThe advantage of "let it break" is that you can continue to run the portion of the program that remains coherent. It's letting the user (who is often just yourself, or a small group of beta testers) into the system; if the user continues to find value in the program even when it's broken, the part that's broken must not have been that important. It's not unusual for programmers to overspecify the requirements when the requirements are unknown; you have to make a number of (often arbitrary) decisions just to get a program working end-to-end, and there's no guarantee that those decisions align with how a user will get the most value from the system.
- coolsunglasses 10y ago>The advantage of "let it break" is that you can continue to run the portion of the program that remains coherent. This works fine in Haskell, thanks in part to laziness. There needn't be a tradeoff here.
- 10y ago