Switched to Scala and SQL, and code is almost as compact but typed relations are far easier to reason about; if a relation changes then you change the core type and follow the compile errors. Once it compiles, it usually works. We've not missed Clojure in the least.
Scala (and even Java) simply feels more structured, with relations between types, the resulting app more "designed", which seems necessary at the enterprise level. In my 2 years, Clojure constantly felt like just hacking.
Would love to see some examples of Clojure being used well at an enterprise level so I could understand better how to do so, but CircleCI's recent flopping between core.typed and prismatic just makes me think it's a square peg in a round hole.
From this I derive both the database schema itself as well as implementation details (e.g., check what columns are in a table or see if the current DB schema agrees with the current SQL schema in resources/sql)
For program internal representation I have my own "classes" (hash maps with a few pre/post assertions on key points) and I just use a single db.clj file that needs to be adapted when the SQL representation changes.
I also worked a lot with Java and Hibernate and it just feels verbose to change a simple datatype definition in both the application and the db. In a way, with clojure I get DB / App consistency that I wouldn't get with Java and/or types. I haven't had the chance to use scala, however.
I also worked with MongoDB (when I started with clojure) and I agree, there it is very easy to make mistakes but maybe I would do things now differently if I used it again.
It's really no different than the OO argument. If one wants OO functionality in lisp it can be implemented with the introduction of a few functions. But by using an OO only language you have just forced that abstraction to the surface leaving you with only one way to write your code even when it's not suitable for the problem space.
So the same could be said for Types. Without Type Inference you're locking yourself in to model/framework that has a list of disadvantages too.
I think the real problem is when people don't recognize they need to account for those problem areas, and they don't take the time to write the abstraction layer when needed and naturally they end up with bad results and feel like it's just hacking, because, well, they are.
Racket supports your argument much better. And Racket is not Lisp: https://news.ycombinator.com/item?id=10763584
The problem outlined was in dealing with data model changes. So the problem as I see is in managing the alignment of parameter types used in code with database field types. For that I've written a schema-template abstraction that manages that relationship. This template engine is about 100 lines of code and for every point the code intersect to/from the data it goes through the engine, looks up the a template and uses that for access/processing (a hook if you will). This allows automation for finding all the code entry spots.
Bottom line is I don't hit these issues because I identified the problem and wrote an abstraction layer to handle it and for the rest of my code I can happily rely on type inference.
What do you use to interface with SQL on the Scala side? Squeryl?
Another thing that helped was to use underscore_naming for any case-class in Scala that maps directly to a query, so it's easier to identify them.
I'm thinking it's a total potential for exploit on web apps backed by mongodb and any dynamic language.
Isn't that what tests are for?
(0) Imprecise, because of the holes in Java's static safety. You can establish a convention that statically unsafe features are verboten, but it will remain just that - a convention.
(1) Verbose, because Java can't express even the simplest ideas without using at least 5 classes: Finite case analysis? Visitors. Higher-order functions? Strategies. Persistent data structures? Memento. Things don't get better if we move to a higher architectural level: Embedded DSLs? You need a separate class for each production rule in your abstract syntax. Type-safe database queries? Java has no structurally typed records, so you need to make a custom POJO class for every query that returns different columns.
These things are incredibly unpleasant to deal with, and while it might be true that "unpleasant work builds character", I don't think it should come as a surprise that some people say "enough is enough", and seek alternatives that minimize or at least reduce their pain (like dynamically checked conformance to a schema), even at the cost of sacrificing some type safety (dynamic checks can't enforce higher-order invariants).
However, I don't think Scala supports your argument very well. Its type system is:
(0) Redundant: When should I use an abstract class or a trait? When should I provide extended functionality using a subclass or an implicit? What are the relative benefits of poor man's ML-style type abstraction (with path-dependent types) vs. poor man's type classes (with implicits)?
(1) Inconsistent: Why are closed sums [(generalized) algebraic data types] implemented in terms of an open-ended inheritance mechanism? This makes `match` exhaustiveness checks unnecessarily more complicated, and hiliarity ensues when the implementors get it wrong.
(2) Unhelpful: Scala's type inference is local and unidirectional, from arguments to results, which limits the usefulness of using types for exploratory programming. GHC Haskell, which has all sorts of crazy type system extensions (some even more ambitious than Scala's), does a much better job of globally inferring types.
So, in spite of my general preference for statically typed languages, I find Clojure easier to live with than Scala, simply because Scala has so many warts.
Almost all of that is due to the intentional choice of running on the JVM with high interop compatibility. Choose ML-family or Haskell, and you don't have Java baggage.
But I'd take types with class-vs-trait design choices, less-than-ideal match exhaustiveness checks, and type inference incompleteness any day over a language lacking all static typing features.
Here are some counterexamples:
(0) Clojure is compatible with Java, but has less warts than Scala. It is not statically typed, but it is internally more unified and consistent. With some exceptions, you only get to see ugly compatibility compromises when you need to interact with Java code.
(1) Rust is compatible with C - in fact, it interoperates with other languages that have C FFIs by simply pretending to be C. Nevertheless, outside of specifically designated `unsafe` blocks, which idiomatic Rust only uses sparingly, most code is written in a "modern C++ meets ML" style.
> But I'd take types with class-vs-trait design choices, less-than-ideal match exhaustiveness checks, and type inference incompleteness any day over a language lacking all static typing features.
Static typing is a useful feature, but I think it's narrow-minded to use it as the only criterion for judging a language's merits. For instance, Clojure's macros are objectively nicer than Scala's, even if both pale in comparison to Racket's. Writing multimethods in Clojure is straightforward once you know the syntax, whereas emulating multiparameter type classes in Scala can be confusing unless you know the specifics of how implicit parameters are resolved. (This is, ironically, a direct consequence of Scala's richer type system.)
You can't do that in Clojure.
Real world examples: Play and Akka are two large projects that include Java development in their primary use cases.
Two-way interop is the cause for a huge number of less than ideal decisions: different Array equality than Seq, @specialized, universal equality, hash codes, and toString, abstract class vs trait, generic type erasure, etc.
You can call the compromise misguided (and you wouldn't be the first to do so) but that's why.
Types are a "proof" to the compiler that your code will run in a certain way. No missed "edge cases", no fuzziness, just pure comprehensive logic.
Of course, the success of this varies by language and a lot of people have worked to find better and more powerful approaches.
But you should regard any type system as a proof system for runtime behavior.
(I'm not saying that running tests with arbitrary examples is bad; I'm just saying that HUGE classes of potential bugs reliably go away with good static typing.)