9 ms·
Can you elaborate why? To be honest, I don't have experience with large-scale Clojure codebases, but I have my fair share working on fairly hefty Python and Per
by throwaway_fjmr 5y ago
Can you elaborate why? To be honest, I don't have experience with large-scale Clojure codebases, but I have my fair share working on fairly hefty Python and Perl projects, and I tend to think that the parent commenter is mostly right. What makes you think they are incorrect?
- uDontKnowMe 5y agoNot who you are responding to, but the common idea that static types are all win and no cost has become very popular these days, but isn't true, it's just that the benefits of static typing are immediately apparent and obvious, but their costs are more diffuse and less obvious. I thought this was a pretty good write up on the subject that gets at a few of the benefits https://lispcast.com/clojure-and-types/ https://lispcast.com/clojure-and-types/ Just to name some of the costs of static types briefly: * they are very blunt -- they will forbid many perfectly valid programs just on the basis that you haven't fit your program into the type system's view of how to encode invariants. So in a static typing language you are always to greater or lesser extent modifying your code away from how you could have naturally expressed the functionality towards helping the compiler understand it. * Sometimes this is not such a big change from how you'd otherwise write, but other times the challenge of writing some code could be virtually completely in the problem of how to express your invariants within the type system, and it becomes an obsession/game. I've seen this run rampant in the Scala world where the complexity of code reaches the level of satire. * Everything you encode via static types is something that you would actually have to change your code to allow it to change. Maybe this seems obvious, but it has big implications against how coupled and fragile your code is. Consider in Scala you're parsing a document into a static type like. case class Record( id: Long, name: String, createTs: Instant, tags: Tags, } case class Tags( maker: Option[String], category: Option[Category], source: Option[Source], ) //... In this example, what happens if there are new fields on Records or Tags? Our program can't "pass through" this data from one end to an other without knowing about it and updating the code to reflect these changes. What if there's a new Tag added? That's a refactor+redeploy. What if the Category tag adds a new field? refactor+redeply. In a language as open and flexible as Clojure, this information can pass through your application without issue. Clojure programs are able to be less fragile and coupled because of this. * Using dynamic maps to represent data allows you to program generically and allows for better code reuse, again in a less coupled way than you would be able to easily achieve in static types. Consider for instance how you would do something like `(select-keys record [:id :create-ts])` in Scala. You'd have to hand-code that implementation for every kind of object you want to use it on. What about something like updating all updatable fields of an object? Again you'll have to hardcode that for all objects in scala like case class UpdatableRecordFields(name: Option[String], tags: Option[Tags]) def update(r: Record, updatableFields: UpdatableRecordFields) = { var result = r updatableFields.name.foreach(r = r.copy(name = _)) updatableFields.tags.foreach(r = r.copy(tags = _)) result } all this is specific code and not reusable! In clojure, you can solve this for once and for all! (defn update [{:keys [prev-obj new-obj updatable-fields}] (merge obj (select-keys new-fields updatable-fields))) (update {:prev-obj {:id 1 :name "ross" :createTs (now) :tags {:category "Toys"}} :new-obj {:name "rachel"} :updatable-fields [:name :tags]}) => {:id 1 :name "rachel" :createTs (now) :tags {:category "Toys"}} I think Rich Hickey made this point really well in this funny rant https://youtu.be/aSEQfqNYNAc https://youtu.be/aSEQfqNYNAc. Anyways I could go on but have to get back to work, cheers!
- codingkoi 5y agoYour third point about having to encode everything isn’t quite true. Your example is just brittle in that it doesn’t allow additional values to show up causing it to break when they do. That’s not a feature of static type systems but how you wrote the code. This blog post[1] has a good explanation about it, if you can forgive the occasional snarkyness that the author employs. In a dynamic system you’re still encoding the type of the data, just less explicitly than you would in a static system and without all the aid the compiler would give you to make sure you do it right. [1]: https://lexi-lambda.github.io/blog/2020/01/19/no-dynamic-type-systems-are-not-inherently-more-open/ https://lexi-lambda.github.io/blog/2020/01/19/no-dynamic-typ...
- uDontKnowMe 5y agoI've seen this article and I applaud it for addressing the issue thoroughly but I still am not convinced that static typing as we know it is as flexible and generic as dynamic typing. Let's go at this from an other angle, with a thought experiment. I hope you won't find it sarcastic or patronizing, just trying to draw an analogy here. So, in statically typed languages, it is not idiomatic to pass around heterogeneous dynamic maps, at least in application code, like it is in Ruby/Clojure/etc. But one analogy we can draw which could drive some intuition for static typing enthusiasts is to forget about objects and consider lists. It is perfectly familiar to Scala/Java/C# programmers to pass around Lists, even though they're highly dynamic. So now think about what programming would be like if we didn't have dynamic lists, and instead whenever you wanted to build a collection, you had to go through the same rigamarole that you have to when defining a new User/Record/Tags object. So instead of being able to use fully general `List` objects, when you want to create a list, that will be its own custom type. So instead of val list = List(1,2,3,4) you'll have to do: case class List4(_0: Int, _1: Int, _2: Int, _3: Int) val list = List4(1,2,3,4) This represents what we're trying to do much more accurately and type-safely than with dynamic Lists, but what is the cost? We can't append to the list, we can't `.map(...)` the list, we can't take the sum of the list. Well, actually we can! case class List5(_0: Int, _1: Int, _2: Int, _3: Int, _4) def append(list4: List4, elem: Int): List5 = List5(list4._0, list4._1, list4._2, list4._3, elem) def map(list4: List4, f: Int => Int): List4 = List4(f(list4._0), f(list4._1), f(list4._2), f(list4._3)) def sum(list4: List4): Int = list4._0 + list4._1 + list4._2 + list4._3 So what's the problem? I've shown that the statically defined list is can handle the cases that I initially thought were missing. In fact, for any such operation you are missing from the dynamic list implementation, I can come up with a static version which will be much more type safe and more explicit on what it expects and what it returns. I think it's obvious what is missing, it's that all this code is way too specific, you can't reuse any code from List4 in List5, and just a whole host of other problems. Well, this is pretty much exactly the same kinds of problems that you run into with static typing when you're applying it to domain objects like User/Record/Car. It's just that we're very used to these limitations, so it never really occurs to us what kind of cost we're paying for the guarantees we're getting. That's not to say dynamic typing is right and static typing is wrong, but I do think that there really are significant costs to static typing and people don't think about it.