My team has been using Clojure for close to a decade now. We have apps that have been in production for many years, and are still under active development.
- refatoring
I don't really find it to be problematic with dynamic typing. I tend to rely on specification tests and spec to test functionality at the API level. If the API behaves according to the specification, then the code can be said to be working as intended.
The project heavily relies on homoiconicity as the business logic for each form is encoded using EDN and stored in the database.
- static types
I've primarily worked with statically typed languages prior to Clojure. I've worked with Java for close to a decade, then used Haskell for about a year, and some Scala.
My experience is that dynamic typing is problematic in imperative/OO languages. One problem is that the data is mutable, and you pass things around by reference. Even if you knew the shape of the data originally, there's no way to tell whether it's been changed elsewhere via side effects.
What I find to be of highest importance is the ability to reason about parts of the application in isolation, and types don't provide much help in that regard. When you have shared mutable state, it becomes impossible to track it in your head as application size grows. Knowing the types of the data does not reduce the complexity of understanding how different parts of the application affect its overall state.
My experience is that immutability plays a far bigger role than types in addressing this problem. Immutability as the default makes it natural to structure applications using independent components. This indirectly helps with the problem of tracking types in large applications as well. You don't need to track types across your entire application, and you're able to do local reasoning within the scope of each component. Meanwhile, you make bigger components by composing smaller ones together, and you only need to know the types at the level of composition which is the public API for the components.
The other problem is that OO encourages proliferation of classes in your code. Each class is its own DSL with an ad hoc API, and knowing the behavior of one class doesn't tell you anything about that of the next. Keeping track of that quickly gets out of hand. With Clojure, you're working with a common set of data structures and using standard library functions to manipulate them.
REPL driven development also plays a big role in the workflow. Any code I write, I evaluate in the REPL straight from the editor. The REPL has the full application state, so I have access to things like database connections, queues, etc. I can even connect to the REPL in production. So, say I'm writing a function to get some data from the database, I'll write the code, and run it to see exactly the shape of the data that I have. Then I might write a function to transform it, and so on. At each step I know exactly what my data is and what my code is doing.
> What I find to be of highest importance is the ability to reason about parts of the application in isolation, and types don't provide much help in that regard. When you have shared mutable state, it becomes impossible to track it in your head as application size grows. Knowing the types of the data does not reduce the complexity of understanding how different parts of the application affect its overall state.
Ironically, I find having types allows me to reason more about an app in isolation. Pick any function in isolation with a typed language and I know the shape/contents of the data I'm dealing with. I know what these things "are". With something like Clojure and no Spec, I have no idea what I'm dealing with until I find the source of where the original data came from. Having worked on an application that used a Clojure library that read sql from a file and translated the queries into maps, I had to look at the sql file every time to make sure I had the data I thought I did. In my experience, worrying about what could be changing the data is overrated. In a typed language, I know exactly what's changing the data since I can statically guarantee what other parts of the codebase are mutating it. It can definitely become a problem with concurrent code, but that happens so rarely in the codebases I've worked on that isn't really a concern and there are ways to mitigate it when it is.
I don't find knowing the shape of the data to be problematic in the applications I work on. I find the REPL tends to play a big part for me here. Any time I develop a feature, I run the code to see what it's doing. So, say I'm adding a new feature that needs some data from the database. I'll run the query to get the data and see it, then I'll write a function to transform it in the desired way, run it, see the output, take the next step, and so on. When I'm updating or changing the feature, I do the same thing. I'll get the application in the desired state, find the spot where I want to make the change, run the current code, and work from there.
Meanwhile, without immutability the types do little to help with tracking side effects which create implicit coupling. To know whether it's safe to change data, you have to know how it's used in every location it's referenced. This completely precludes the ability to do local reasoning about your code. This is a completely separate problem from concurrency.
- refatoring
I don't really find it to be problematic with dynamic typing. I tend to rely on specification tests and spec to test functionality at the API level. If the API behaves according to the specification, then the code can be said to be working as intended.
- homoiconicity
Absolutely, my team gave a talk about one of our projects recently https://www.youtube.com/watch?v=aXBe6hoi-Mw
The project heavily relies on homoiconicity as the business logic for each form is encoded using EDN and stored in the database.
- static types
I've primarily worked with statically typed languages prior to Clojure. I've worked with Java for close to a decade, then used Haskell for about a year, and some Scala.
My experience is that dynamic typing is problematic in imperative/OO languages. One problem is that the data is mutable, and you pass things around by reference. Even if you knew the shape of the data originally, there's no way to tell whether it's been changed elsewhere via side effects.
What I find to be of highest importance is the ability to reason about parts of the application in isolation, and types don't provide much help in that regard. When you have shared mutable state, it becomes impossible to track it in your head as application size grows. Knowing the types of the data does not reduce the complexity of understanding how different parts of the application affect its overall state.
My experience is that immutability plays a far bigger role than types in addressing this problem. Immutability as the default makes it natural to structure applications using independent components. This indirectly helps with the problem of tracking types in large applications as well. You don't need to track types across your entire application, and you're able to do local reasoning within the scope of each component. Meanwhile, you make bigger components by composing smaller ones together, and you only need to know the types at the level of composition which is the public API for the components.
The other problem is that OO encourages proliferation of classes in your code. Each class is its own DSL with an ad hoc API, and knowing the behavior of one class doesn't tell you anything about that of the next. Keeping track of that quickly gets out of hand. With Clojure, you're working with a common set of data structures and using standard library functions to manipulate them.
REPL driven development also plays a big role in the workflow. Any code I write, I evaluate in the REPL straight from the editor. The REPL has the full application state, so I have access to things like database connections, queues, etc. I can even connect to the REPL in production. So, say I'm writing a function to get some data from the database, I'll write the code, and run it to see exactly the shape of the data that I have. Then I might write a function to transform it, and so on. At each step I know exactly what my data is and what my code is doing.