I've written a small Clojure project, maybe 3k loc. It was great fun. But when I go back to add small features I find it quite tricky. Bugs often sneak in.
I've written a small Clojure project, maybe 3k loc. It was great fun. But when I go back to add small features I find it quite tricky. Bugs often sneak in.
If you want to write maintainable code and catch bugs early, that pretty much rules out dynamically typed languages.
OTOH in strongly typed languages with rich type systems the actual code might be uglier and more complicated because you're constructing a proof of something. The property might be week (tests needed!) or strong (tests? what tests). Depends how far the rabbit hole you want to go.
At least that's my view on this.
It's rare for the programmer to actually produce proofs that a piece of code meets the spec given by its type[1]. Those proofs are generally produced by the type checker, possibly with occasional hints from the programmer.
1: Even in Coq, detailed specs for some code are typically given (and proven) separate from the code itself rather than in that code's own type.
Statically typed languages apply typechecking as a "test" to the entire program at load time.
People have written and maintained code in typeless assembler, including for mission-critical systems. It's just more work and requires a different kind of rigor. The larger and more complex your system gets, the more useful typechecking becomes.
"Maintainable" is not a boolean, it's a cost function.
In a dynamically typed system, every expression and line of code is a liability, and requires a large weight of tests to have any confidence that it might work.
Static typing eliminates entire classes of bugs. The cost of the up-front inconvenience to the programmer is tiny compared to the ongoing maintenance costs of possibly-incorrect, we-won't-know-for-sure-until-that-code-path-executes-in-production dynamically-typed code.
This is dependent on many variables but a good rule of thumb is that for any project bigger than a doddle, it's a safe bet to just fucking use a statically typed language.
I don't want to start a discussion here on typed vs untyped so please consider everything I say to be qualified "It is only my personal opinion and experience".
I'm writing Haskell in my day job, Clojure for fun side project, OCaml because it's a nice language that's a bit underused and C++ because it's useful to know and not so evil as most people say (and it's progressing fast!). I should throw Rust into the mix because it might have a bright future.
WARNING: personal opinions and anecdotal evidence ahead!
Maybe there's a level of lisp enlightenment that I haven't reached yet but I can't just get by with writing lisp without writing tests. On the other hand I can get by with writing Haskell and OCaml without writing tests.
This is particularly true when I'm coming back to a project that I haven't touched for a few weeks. I change something and something breaks. In Haskell and OCaml I change something and compiler complains.
Maybe we'll see HaLispML one day.
BUT those guys are much smarter than us... so creating a language that combines the qualities of dynamic and static languages is probably a hard problem.
* Racket has a sophisticated contract system that allows you to enforce "type-like" properties at runtime very easily (e.g., you can say "This function should behave as an ((int -> int) -> int) function" and it will do all the necessary runtime checks to make sure that contract is honored as your program executes)
* It also has an optional modern type system, Typed Racket, that you can opt into on a per-module basis. Typed Racket modules can interact with untyped modules safely via contracts. The Typed Racket type system was designed specifically so that it's easy to migrate untyped, idiomatic Racket code to the type system, so you can write untyped Racket code idiomatically, and then go back and port your untyped module to Typed Racket with a minimum of fuss and get the benefit of the type system.
That said ...
> On the other hand I can get by with writing Haskell and OCaml without writing tests
Tests and static type-safety serve different purposes. If you end up writing tests for properties that should have been inferred by a static compiler, then you're using the language in a wrong way or you've picked the wrong language for the problem at hand.
When working with a dynamic language, I don't need to write tests just to see that my code works. It's because I work with a REPL and in terms of happy paths, that's just as effective as having a static type system.
And surely having a compiler is very cool when refactoring, however we tend to miss the fact that (a) the kind of refactorings we are doing are very superficial and for architectural / design refactorings the compiler doesn't save you and (b) in a dynamic language there is less need for refactoring, because you don't end up modelling the whole world through types.
> I don't want to start a discussion here on typed vs untyped
I'm also thinking you're making a confusion. Dynamic languages can be strongly typed. A language Clojure is very much typed. The difference is in the moment those types are used, at compile time or at runtime.
This is important, because a dynamic language like Clojure can do optional typing when you want it. Of course, something like core.typed will never be as expressive and potent as Haskell's type-system, however this leads to gradual evolution - at first you don't have a well defined shape for the data you're working with, so you can enjoy the relaxed rules and protocols of Clojure and afterwards you can start introducing type definitions with core.typed or with prismatic/schema.
As I said, I'm a developer that leans on the static side of the argument, however this debate will never be settled simply because which tool is the best depends on the problems you're trying to solve, therefore people will never agree on anything, because people are always thinking from their "personal experience".
Well, I never want to have to maintain one of your Haskell or OCaml projects. If you think you can get by with any language without tests you are wrong.
That being said, yes it is easier to deal with lack of tests in a staticly typed/compiled language than in a dynamic language like clojure/ruby/python/perl/etc.
The main point I'd like to make to you is you aren't complaining about Clojure per say, you are complaining about dynamic languages in general. It just so happens clojure is the one you are picking on.
Either way, you might like Shen (http://www.shenlanguage.org/) though it is very much academic and has few tools around it, it is very much a HaLisp, though i'm not sure about the ML part.
At this point in my learning it's more about pleasing a SAT solver than getting anything useful done... but I'm sure that will change with time.
Common Lisp does have a type system... it's just dynamic so you can hit things at run-time. This is great in development because it allows you to under-specify things that aren't terribly important (like types) at that time. However when you begin to find your code is ready to be locked in place you can annotate your function to hint to the implementation what the types should be. In particularly well-tested and heavily-typed code you can even turn off dynamic type-checking entirely for your production builds and get the performance boost from that.
Regardless of your approach and requirements, tests have a completely different use beyond ensuring type consistency. They set expectations, inform API design, and catch mistakes in refactoring; they act as a specification for the module under test. I've had plenty of OCaml code compile that still failed tests. Even in the presence of strong static typing you need unit, integration, and regression tests.
Just food for thought.
I'm doing a lot more Scala these days, which feels a lot easier to work with in the long run.
Golang, in comparison, is very transparent, WSYIWYG.