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.
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.
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.
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.
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.
This works fine in Haskell, thanks in part to laziness. There needn't be a tradeoff here.
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.
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.
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.
How? By building the structure/hard stuff first and letting the compiler verify it while leaving out the uninteresting parts.
A very simple example. Let's say you have some data model with people, addresses, whatever. You want to see what you need to do to get a list of addresses for some group of people.
You start by defining the structure of your data model (this can depend on DB schema, business requirements, whatever):
data Person = Person { name :: String, city :: String }
data Address = Address { city :: String, zipcode :: String, street :: String }
Now, go straight to the hard part. No need to fuck around with DB connections, mock data, whatever: getAddresses :: [Person] -> [Address]
getAddresses = undefined
Leave out the implementation for now. So probably we are gonna need a way to get an Address for a Person. Ok: addressForPerson :: Person -> Address
addressForPerson = undefined
Again, leave out the implementation. Now go back to `getAddresses` and see if we can implement it now: getAddresses = map addressForPerson
Cool, the compiler doesn't complain. So far we've verified that if we have a function like `addressForPerson` we can safely assume that our design is going to work.Now we can go back to `addressForPerson` and implement that. But turns out, we only have a city stored in Person, so the best we can do is get back a list of potential addresses for each person. So we need to adjust that type signature:
addressForPerson :: Person -> [Address]
addressForPerson = undefined
Leave out implementation, it's trivial so no need to spend time on it now. But now the compiler complains that `getAddresses` doesn't make sense anymore! We are saying that, having a list of people, we can get a list of addresses, but that's not true anymore. We found out that we can only get a list of potential addresses, so we have to adjust the type signature. getAddresses :: [Person] -> [[Address]]
Now every use of `getAddresses` will have to be adjusted to the new requirements. Etc etc.Granted, this is a very simple example, but I hope it illustrates how static languages facilitate extremely fast prototyping. Sure, a Lisp REPL is very very nice, but in my personal experience it's no match for a type system. We didn't even have to mock data to start experimenting!
(And yes, Haskell, OCaml, PureScript, Elm etc all have a REPL too).
The small amount of scientific study around what type checking and static typing gives you is that you avoid around 3% of bugs but even those 3% are easier to fix than the effort in writing them correctly in the first place. While I agree that the subject needs more study but what we know now: the real win would be to have a dev process that lowers criticality of failure (like TDD, CI and other Agile processes) and dispense with static typing for most projects.
Maybe I need to spend some time in Haskell and come over to your way of seeing it. But the studies show that constructing the type system for your problem is very difficult and remains error prone. People routinely make mistakes with types systems of all kinds. I've found I spend more time making the compiler happy that it does making me happy.
But still, my point stands - writing tests during early experimentation is a lot harder than just altering some type and letting the compiler infere what needs to be done or what you broke.
I've spent some time writing Clojure and still use dynamic languages a lot for smaller tasks, so I know both sides. This doesn't mean much, but I'd really encourage you to try some ML language and spend some time with a nice type system. Haskell is probably too big a time investment, so maybe try Elm [0] - it's very beginner friendly and a lot of the people using it come from JavaScript.
With property based testing, your system will come up with mock data for you.
Wow, then you are extremely lucky because bugs like that would occasionally consume a whole day of senior developer time at my previous Python job.
It seems like a symptom of other problems more than anything, and using a staticly typed language will just make those problems manifest in a different way.
If you are writing all of your code from scratch, then it doesn't happen too often. You usually have enough of the current context in your head to avoid most type level mistakes.
The real strengths come from two situations.
The first is refactoring. There is just no contest here. Having every single place where your change affects the test of the program identified automatically is extremely useful.
The second is returning to old code and either trying to understand it or make small changes to it.
http://games.greggman.com/game/dynamic-typing-static-typing/
For an example of a project which I worked on for months, given N molecular graphs, find the maximum common subgraph which is in at least M of them. I did all that in Python. I don't think static types would have made a difference.
Matrix algorithms are an even more clear example. They work with a single numeric type, in a 1D vector or 2D array. It's hard to confuse the types. On the other hand, it's easy to get the algorithms wrong, and choosing the right algorithm can require a lot of design space exploration.
Just think of the number of clustering algorithms available which you might want to try out through prototyping before spending the time to scale things up. How does static typing really help there?
reduceGraph :: Graph a -> Graph Reduced
Now you can't pass unreduced graphs to functions where you expect some sort of an already reduced graph, and you could encode as much invariants as you wanted in your types, but yea, in cases where there are homogenous types with little variance (machine learning, statistics, your use case) static typing won't probably bring that much (unless you go nuts with dependant typing I guess).Even with no knowledge of the problem space, I can think of a whole bunch of static types off the top of my head.
You've already given your input type: N molecular graphs, and a number M.
You've also given your output type: the maximum common subgraph which is in at least M of the given N molecular graphs.
Straight away I can spot a problem with this, since there are no programs of this type. In particular, there is no valid output when M > N. Hence I would refine the input type to ensure that M <= N.
Since it appears in your input type, it would be useful to have a type for "molecular graph". Likewise, based on your output type, it would probably be useful to have types for "subgraph of g", "common subgraph of gs", "maximum common subgraph of gs" and "subset of gs".
Naively, I'd give your program a type something like this:
(m : Nat, gs : Set MolecularGraph, m <= size gs) -> (hs : SubSet gs, size hs >= m, MaximumCommonSubgraph hs)
Programming with type constraints like these would be very different to using Python, where you'd have no guarantee that a value is even a graph, let alone a maximum common subgraph of the input set.Whether it would be more or less appropriate for your situation is, of course, an entirely separate issue ;)
[0] In some languages, like older versions of pascal, arrays of different lengths were different types, which was a horror for writing generic code.
Beyond strong typing, do you get the sense that static typing significantly improve prototyping ability for matrix analysis code?
Why shouldn't I be able to do this on a meta-level? There does not appear to be any restriction on, for example, Lisp code regarding the use of problem solvers and provers. The same should go for other metalanguages (perhaps even better, without forcing me into specific semantics).
(Likewise, generative solutions might reduce this problem simply by performing a sequence of correctness-preserving transformations from a higher-level specification. That would shift the burden to checking the specification, which would require a custom checker anyway. Haskell's capability to check or automatically derive stuff seemed limited the last time I looked at it.)
Also, using a type checker to wholly check your Lisp code basically makes it a static language, the difference then is pretty much philosophical I'd say.
I'd rather avoid someone forcing me into using a hardcoded type system, among other reasons because I'm contemplating a (perhaps novel?) search-based program/expression-manipulating system that wouldn't play well with any fixed typing system. For one thing, the types of expression results (being return values of operators applied to input expressions or programs) can easily be emergent (is the resulting expression linear? Is it an odd function? Is it analytically integrable? Etc.) and not easily arrived at in something ML-like. If I had to use something like Haskell, I'd still be rolling out my own type system/prover on top of the existing language implementation anyway. However, types in general (as something you can compute with in the meta-program) would be absolutely mandatory for a system like this (for example, some algorithms or operations can have more desirable refinements that work only on subsets of values - consider determinants of triangular matrices, with triangular matrices being an emergent type - and I absolutely want to find out if such a thing is available for the specific problem being transformed), just not types that any specific existing language offers, to my knowledge.
So it's not that I dismiss the notion of proving things about programs - that is a vital thing to have - but the ability to impose restrictions on a program from the outside seems much more flexible to me than going for some fixed type discipline that could force me to straddle inconvenient fixed walls in the structure of a complicated program (such as this transformation system that would search the tree of possible transformations for a result with some desirable properties, with the transformations concisely expressed in some very-high-level DSL source code).
If instead of having separate Person and Address types you just use strings for everything (even "lists" can be strings - just keep the data serialized into HTML, or XML) then you can your program is likely to "do something" if you just compose a bunch of functions together. Sure, your code might have a bunch of logic bugs (like HTML injection / XSS) but at least it does something!
Static typing is great once you leave this "stringly typed" world and start separating your values into strongly-typed types. When you get a type error from passing a value to the function then its nice if you can be warned about this at compilation time.
It might not be just about that. Sometimes you don't know what you're doing, and you have to find out first. What use is it if you painstakingly design and craft a perfectly engineered program, only to find out that you actually needed something else? Being able to experiment is precious. Being forced to handle everything before the thing even compiles is not all that useful at times. There's a reason why interactive environments like Lisp and Smalltalk don't force you to define all functions that could plausibly get called (and when they do get called, you can even add them on the fly and continue).