News for hackers. Stuff that matters.
Re:Learning from Ada by nilkn on Sunday August 21, 2016 @05:21PM (#12332792)
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.

Re:Learning from Ada by nostrademons on Sunday August 21, 2016 @06:21PM (#12332979)
I 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.

Re:Learning from Ada by nilkn on Sunday August 21, 2016 @06:33PM (#12333025)
To 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.

Re:Learning from Ada by nostrademons on Sunday August 21, 2016 @06:45PM (#12333072)
The 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.
Re:Learning from Ada by nilkn on Sunday August 21, 2016 @10:13PM (#12333712)
And the disadvantage of that is that you possibly don't even know what's broken! It will come down entirely to how vigilant you've been with testing. Your argument is that if it's broken but you don't notice, then it must not be all that important. But I think there are tons of exceptions to that, and that's putting a huge amount of faith in yourself and beta testers to reliably test all important aspects of the software. Besides, if you do more or less figure out what's broken and what's not important, you still have to engage in a flimsy surgery of the codebase to excise the broken pieces.

I'm not arguing universally against what you're proposing by any means, but it doesn't seem like a clearly superior approach to me. The bottom line is that "let it break" takes many forms. There's your version, there's my type coherence version, there's Erlang's version with supervisor trees, etc. I think it's hard to argue that any of these is universally superior to the others.

Re:Learning from Ada by nostrademons on Sunday August 21, 2016 @10:54PM (#12333803)
It's superior for a certain problem domain, one in which the desired feature set is unknown, the desired feature set is simple, and the consequences of failure are limited. Consumer web & mobile startups, basically, as well as some limited small-business B2B software where the alternative is an error-prone manual process. It's probably not appropriate for enterprise software, and is certainly not appropriate for embedded, regulated, or safety-critical software.

This is far from the whole software industry, but it's a segment that tends to be of disproportionate interest to HN readers, given the startup-focused origins of this site.

Re:Learning from Ada by coolsunglasses on Sunday August 21, 2016 @07:45PM (#12333282)
>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.

Re:Learning from Ada by nostrademons on Sunday August 21, 2016 @07:54PM (#12333314)
Even if it fails to typecheck? Has the Haskell compiler gotten smart enough that it can stub out portions of the code with type errors and fail at runtime?
Re:Learning from Ada by pinkythepig on Sunday August 21, 2016 @10:44PM (#12333774)
Actually yes, you can pass a compiler flag, -fdefer-type-errors, which makes type errors turn from compilation errors to simply warnings. That said, any code that does not type check will also not run and instead will throw an exception at runtime if called (laziness is key here). But if Part B was giving type errors and you only cared about testing Part A, then you are good to go.
Re:Learning from Ada by coolsunglasses on Sunday August 21, 2016 @11:14PM (#12333879)
Yep. It's easier to implement and makes more sense in a lazy language than what you're describing though. I don't use it in my work but I do use a related technique to isolate what I am focused on fixing.
Re:Learning from Ada by coolsunglasses on Monday August 22, 2016 @11:39AM (#12337080)
To elaborate a little on this:

In a strict language, a type error would mean the bricked off expression that doesn't type-check would spread like an infection to any expression that incorporated it.

In Haskell, I can dance around sub-expressions that don't type-check at runtime and evaluate things optionally.

The technique doesn't work in practice otherwise.

Re:Learning from Ada by tome on Monday August 22, 2016 @01:54PM (#12338066)
> The technique doesn't work in practice otherwise.

I don't see why. Functions in a strict language are evaluated lazily. Any type error within a function wouldn't spread beyond that function.

Re:Learning from Ada by coolsunglasses on Monday August 22, 2016 @04:21PM (#12339135)
Values still wouldn't work. You'd have to change their types to `() -> whatever` and that means a lot of churn.

Even strict-by-default languages that let you easily make values explicitly lazy would mean type level churn, if not term level.

The value part is important because it means you can't really talk about partially implemented hypothetical results where you know how to handle one branch of a function, but not another. This is a super common divide-and-conquer technique in Haskell.

Could possibly half-ass it if you have some kind of panic functionality, but it'll fail unconditionally in a strict language when the result of the function is forced. In Haskell, if you don't use the bottom, it doesn't blow up.

Re:Learning from Ada by sordina on Sunday August 21, 2016 @10:52PM (#12333798)
Yes. You can defer type errors. I turn this on in development.