One thing that deep, static type systems often lend themselves to is complex types, and those complex types lead to complicated designs; and if I have the option, I'd rather spend my time designing a simple architecture than exhaustively testing and validating a complicated one.
Since using clojure (roughly the last 3 years in data-pipeline style architecture) I try to follow a principle of writing functions that do just one thing, simply, and do it well, keeping the interface as basic as possible. Almost all of the time when writing clojure, I'm writing functions that act on a couple of built-in types and return one built-in type, usually the same one as before.
Additionally, I try to isolate side effects.
Following these principes, I find that even when I'm dealing with a stinker of a namespace or running into a runtime error, it's pretty cheap to try it out in the REPL and get to refactoring.
That said, I kind of want my cake and to eat it too. I do miss static types -- just not as much as I missed them when I was writing Javascript or Ruby. If I write a new clojure library now, yes, I would try to instrument it (at least the public api) with clojure-spec. If I could re-design clojure, I'd leave out null and maybe build a more intuitive interface to clojure-spec to use it as a kind of progressive validation system via annotations and type hints.
I don't know how to call this. But it's something I find true too. And to a point, it feels like a lot of businesses leverage that as a justification point for their cost.
>Wirth’s philosophy of programming languages is that a complex language is not required to solve a complex problem. On the contrary, he believes that l languages containing complex features whose purposes are to solve complex problems actually hinder the problem solving effort. The idea is that it is difficult enough for a programmer to solve a complex problem without having to also cope with the complexity of the language. Having a simple programming language as a tool reduces the total complexity of the problem to be solved.
[0] https://www.cslab.pepperdine.edu/warford/ComputingFundamenta...
I think the idea of bottom-up programming also resonates with me, though I tend to find it hard to use as a general approach to programming. In my mind, it's similar to the development of DSLs, which I do fairly infrequently, and with care. There is an undeniable elegance to it ... find a set of primitive operations that can be composed into larger ones. My development style usually goes in either direction: Either super generalized, for which I'll take the bottom-up DSL approach, or the more typical case where it's more approachable for me to be explicit with types/code.
Usually I start at the top and write out high level code as if everything I needed magically existed. That helps me think about what I want and how I think I'll use it and that helps and clues me in to what low level stuff I need to build. Then I start to think about how to build those things and that usually goes bottom up.
I do this iteratively. When I get stuck I go back to high level top down, figure out what I'm missing, go build that, and repeat.
The REPL is indeed a major component of doing the Clojure workflow where you literally run each function as you're writing it. This approach also encourages refactoring as you go since you can make changes and see the results immediately.
The best answer I could think of was, "don't manage huge projects." Instead, divide projects as early as possible in development into supporting libraries, each capable of being independently tested and maintained.
In my experience, applications developed with this attitude coalesce themselves into a small ecosystem of mostly generic library code with a domain-specific apex "application" entrypoint to bind them.
In an application factored like this, type checking isn't something you want as much anymore, since the constituent libraries interface with one another at a level above. They present and consume "public" (intra-application) APIs, the behavior of which are defined not just by types but by at least documentation and tests. The thing that static types let you do -- have huge amounts of code, woven together with structural dependencies -- is no longer a thing you need or want. It would seem bad, actually, since what you'd have is an inscrutable mass only a machine can understand.
I haven't worked enough with large non-Clojure codebases to say whether or not this approach is superior, but I can say that it scales. Another nice thing about it in a business setting is that supporting libraries lend themselves to open-sourcing, and a relationship with an open source community is a good thing for a company's development efforts/hiring/contracting in my experience.
To answer your specific questions:
* I don't think I write more unit tests (I should probably write more tests in every language I use)
* I don't have any experience with clojure.spec
* All of the features of Lisp work together to make it awesome, including macros. In particular, in the context of an "ecosystem of libraries" approach, macros are a fantastic tool for exporting a DSL from a supporting library. They're also use for DSLs in the apex code, where application-specific behavior can be defined in terms of a DSL.
* I don't really wish I had static types - I love Lisp the way it is. But I have found various languages for structural checking useful with certain kinds of sub-program, like receiving data from the outside world, and for writing compilers. If you write a compiler as a series of transformations, it's nice if the shape of the code between transformations is well-defined somehow.
You're definitely smart enough to use Lisp. I think the key to success is not necessarily smarts, but attentiveness to modularity or cognizance of the difference between "library" and "application" types of code.
* There's a Paul Graham essay related to this idea: http://www.paulgraham.com/progbot.html
* I think this approach is applicable not just to Lisp, but maybe to any dynamic language with a sufficiently rich set of generic collections and with the concepts of a "module" or "package".