10 ms·
In 2021, I find it hard to justify using a dynamically typed language for any project that exceeds a few hundreds of lines. It's not a trade off, it's a net los
by hota_mazi 5y ago
In 2021, I find it hard to justify using a dynamically typed language for any project that exceeds a few hundreds of lines. It's not a trade off, it's a net loss.
The current crop of statically typed languages (from the oldest ones, e.g. C#, to the more recent ones, e.g. Kotlin and Rust) is basically doing everything that dynamically typed languages used to have a monopoly on, but on top of that, they offer performance, automatic refactorings (pretty much impossible to achieve on dynamically typed languages without human supervision), fantastic IDE's and debuggability, stellar package management (still a nightmare in dynamic land), etc...
- sova 5y agoI must respectfully disagree with the points you've brought up.
- throwaway_fjmr 5y agoCan 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.
- daxfohl 5y agoYeah, I had a fairly large (about a year of solo dev work) app that I maintained both Clojure and F# ports of, doing a compare and contrast of the various language strengths. One day I refactored the F# to be async, a change that affected like half the codebase, but was completed pretty mechanically via changing the core lines, then following the red squigglies until everything compiled again, and it basically worked the first time. I then looked at doing the same to the Clojure code, poked at it a couple times, and that was pretty much the end of the Clojure port.
- dharmaturtle 5y agoHey, so my my career path has been C# (many years) -> F# (couple years) -> Clojure (3 months). I understand multithreading primarily through the lens of async/await, and have been having trouble fully grokking the Clojure's multithreading. One of the commandments of async/await is don't block: https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html https://blog.stephencleary.com/2012/07/dont-block-on-async-c... Which is why the async monad tends to infect everything. Clojure, as far as I can tell so far, doesn't support anything similar to computation expressions. So I'm guessing your "poked at it a couple times" was something like calling `pmap` and/or blocking a future? All my multithreaded Clojure code quickly blocks the thread... and I can't tell if this is idiomatic or if there's a better way.
- daxfohl 5y agoNot even. It was opening it, looking, realizing it would take a couple weeks, and going back to F#. I did this a couple times before fully giving up. IIRC/IIUC, Clojure's async support is closer to Go's (I've never used go), in the form of explicit channels. Though you can wrap that in a monad pretty easily, which I did for fun one day (https://gist.github.com/daxfohl/5ca4da331901596ae376 https://gist.github.com/daxfohl/5ca4da331901596ae376). But neither option was easy to port AFAICT before giving up. Note it's possible that porting async functionality to Clojure may have been easier that I thought at the time. Maybe adding some channels and having them do their thing could have "just worked". I was used to async requiring everything above it to be async too. But maybe channels don't require that, and you can just plop them in the low level code and it all magically works. A very brief venture into Go since then has made me wonder about that.
- gnaritas 5y agoNot remotely true.
- joelbluminator 5y agoIt's your opinion though, there's nothing scientific about what you're saying. Take mocking for example, in Ruby/Rails it's a breeze. In Java you need to invent a dependency injection framework (Spring) to do it.
- throwaway_fjmr 5y agoI think you are mistaken. Mocking and DI frameworks are two unrelated concepts. There is nothing in Java that forces you to use a DI framework, e.g., Spring if you want to use mocks during testing.
- joelbluminator 5y agoLet's say I have a class called User and in it a method that says the current time. So User#say_current_time which simply accesses the Date class (it takes no arguments). Can you show me how you would mock the current time of that method in Java? It's one line of Ruby/Javascript code to do that.
- hota_mazi 5y agoUser mock = mock(User.java) when(mock.say_current_time()).thenReturn(someDate)
- joelbluminator 5y agoOK. first I could be ignorant about Java since I haven't touched it in more than a decade. Which library is doing that? And also what is mock(User.java) returning - is it an actual User instance or a stub? I want a real User instance (nothing mocked in it) with just the one method mocked. And again if this is possible I will admit ignorance and tip my hat at the Java guys.
- hota_mazi 5y agoIt's Mockito [1], which has been a standard for a while. There are other libraries and they use different strategies to provide this kind of functionalities (dynamic proxies, bytecode weaving, annotation processing, etc...). [1] https://site.mockito.org/ https://site.mockito.org/
- outworlder 5y ago> In 2021, I find it hard to justify using a dynamically typed language for any project that exceeds a few hundreds of lines. It's not a trade off, it's a net loss. Only if you are skimping on tests. There's a tradeoff here - "dynamically typed" languages generally are way easier to write tests for. The expectation is that you will have plenty of them. Given that most language's type systems are horrible (Java and C# included) I don't really think it's automatically a net gain. Haskell IS definitely a net gain, despite the friction. I'd argue that Rust is very positive too. Performance is not dependent on the type system, it's more about language specification (some specs paint compilers into a corner) and compiler maturity. Heck, Javascript will smoke many statically typed languages and can approach even some C implementations(depending on the problem), due to the sheer amount of resources that got spent into JS VMs. Some implementations will allow you to specify type hints which accomplish much of the same. Which is something you can do on Clojure by the way. Automatic 'refactorings' is also something that's very language dependent. I'd argue that any Lisp-like language is way easier for machines to process than most "statically typed" languages. IDEs and debugability... have you ever used Common Lisp? I'll take a condition system over some IDE UI any day. Not to mention, there's less 'refactoring' needed. Package management is completely unrelated to type systems. Rust's robust package management has more to do with it being a modern implementation than with its type system. They have learned from other's mistakes. Sure, in a _corporate_ setting, where you have little control over a project that spans hundreds of people, I think the trade-off is skewed towards the most strict implementation you can possibly think of. Not only type systems, but everything else, down to code standards (one of the reasons why I think Golang got popular). In 2021, I would expect people to keep the distinction between languages and their implementations.
- blacktriangle 5y agoHere's what I've noticed with my tests and dynamic languages. I'll get type errors that static typing would have caught. However those errors occur in places I was missing testing of actual functionality. Had I had the functionality tests, then the type error would have been picked up by my tests. And had I just had static typing, the type system would not have been enough to prove the code actually works, so I would have needed tests anyways. Point being, I don't really buy that a static type system saves me any time writing and maintaining tests, because type systems are totally unable to express algorithms. And with a working test suite (which you will need regardless of static vs dynamic) large refactors become just as mechanical in dynamic languages as they are in static languages.
- deleted 5y ago[deleted]
- deleted 5y ago[deleted]
- bcrosby95 5y agoI find immutability way more important. I don't pick Clojure for its dynamic typing, I pick it for other reasons. I've tried Haskell but it really doesn't seem to mesh with the way I tend to develop a program. But I would love to have more static languages with the pervasive immutability of Clojure.
- JackMorgan 5y agoI really like F# for this, it's like Haskell-lite
- jhgb 5y ago> automatic refactorings (pretty much impossible to achieve on dynamically typed languages without human supervision) ...are we talking about the thing pioneered by Smalltalk's Refactoring Browser?
- chakkepolja 5y agoMy question is how does that work in a dynamically typed language? In static typed language we can know scope & type of a variable and we can't change much in runtime.
- didibus 5y agoIn Clojure you know the number of arguments to a function and the name of functions and variables, and the code is all very well structured as an AST (being a Lisp). So you can do a lot of refactorings with that such as: Rename function, rename variable, rename namespace, extract constant, extract function, extract local variable, extract global variable, convert to thread-first, convert to thread-last, auto-import, clean imports, find all use, inline function, move function/variable to a different namespace, and some more. The only thing is you can't change the "type" of something and statically know what broke.
- hota_mazi 5y agoAll these refactorings can only be done automatically and safely if you have type annotations (i.e. core). Without them, all these refactorings can break your code (as in, not even compiling, let alone run).
- deleted 5y ago[deleted]
- didibus 5y agoI believe you're mistaken, but please explain otherwise? None of those seem to require type information from my reasoning (and are also all available in Emacs for Clojure) For example, moving a function from one namespace to another, you know where this function is being used from the require declarations, and you know where you've been told to move it too and where it currently resides. So you can simply change the old require pointing to its old namespace to point to the new namespace and cut/paste the function from the old to the new. Nothing requires knowing the type or the arguments or the return value of the function. See a gif of it in action: https://raw.githubusercontent.com/clojure-emacs/clj-refactor.el/master/examples/move-form.gif https://raw.githubusercontent.com/clojure-emacs/clj-refactor...
- didibus 5y agoWe can debate this forever, but all I can say is that at my work we have equal part Java and Clojure, and we have some Kotlin and sole Scala as well. Out of all of them, Clojure does not cause us anymore issues, it doesn't take us any longer to add features, it doesn't perform any worse, and it doesn't have any more defects than the others. My conclusion is that it's a matter of personal preference honestly. Those are all really good languages. Personally I have more fun and enjoy using Clojure more. I would say I tend to find I'm more productive in it, but I believe that's more a result of me finding using it more enjoyable then anything else.
- Sophistifunk 5y agoNot only this, but the programming style where you pass around dictionaries / maps for everything yet have expectations about what keys they contain works just as easily in JS, and with TypeScript or Flow you get a lot more help from the compiler than you do using spec (as I understand it).
- roenxi 5y agoAlthough you are right, the Clojure community probably by and large agrees with you. That is why everyone is excited about spec - it looks a lot like a type system for Clojure.