13 ms·
I found one of the perceived weaknesses of Clojure (in this article), it being dynamically typed, is a tradeoff rather than a pure negative. But it applies that
by dgb23 5y ago
I found one of the perceived weaknesses of Clojure (in this article), it being dynamically typed, is a tradeoff rather than a pure negative. But it applies that tradeoff differently than dynamic languages I know otherwise and that difference is qualitative: It enables a truly interactive way of development that keeps your mind in the code, while it is running. This is why people get addicted to Lisp, Smalltalk and similar languages.
> To understand a program you must become both the machine and the program.
- Epigrams in Programming, Alan Perlis
Two of the big advantages of (gradually-) typed languages are communication (documentation) and robustness. These can be gained back with clojure spec and other fantastic libraries like schema and malli. What you get here goes way beyond what a strict, static type systems gets you, such as arbitrary predicate validation, freely composable schemas, automated instrumentation and property testing. You simply do not have that in a static world. These are old ideas and I think one of the most notable ones would be Eiffel with it's Design by Contract method, where you communicate pre-/post-conditions and invariants clearly. It speaks to the power of Clojure (and Lisp in general) that those are just libraries, not external tools or compiler extensions.
- vnorilo 5y agoAgreed. I feel Lisps and SmallTalk are dynamic done right. I think the other language features that you use also influence the value from dynamic or static types. For OOP style, static types are a huge asset for refactoring and laying our architecture. On the other hand, immutable data and stateless functions (as idiomatic in clojure) make them less necessary, and also work great together with interactive development.
- geokon 5y agoI've admittedly not played with spec, but can't you solve documenting interfaces by defining `defrecord`s ? You rarely really care about the actual types involved. You just want to know which fields you either need to provide or will recieve
- roenxi 5y agoSpec will give you stronger feedback than a docstring or function signature. It can tell you (in code terms, with a testable predicate) if a call to an interface wouldn't make sense. Eg, spec can warn you when an argument doesn't make sense relative to the value of a second argument. Eg, with something like (modify-inventory {:shoes 2} :shoes -3) spec could pick up that you are about to subtract 3 from 2 and have negative shoes (impossible!) well before the function is called - so you can test elsewhere in the code using spec without having to call modify-inventory or implement specialist checking methods. And a library author can pass that information up the chain without clear English documentation and using only standard parts of the language. You can't do that with defrecord, but it is effectively a form of documentation about how the arguments interact.
- modernerd 5y agoDoes the spec logic typically live inside the modify-inventory function, or elsewhere? If elsewhere, what triggers it before the function is called?
- teataster 5y agoThere is very little spec logic. It looks a lot like type declarations in typed languages. It's usually outside the scope of functions, since you are likely going to want to reuse those declarations. For example, you can use spec to generate test cases for something like quick-check. You can add pre and post conditions to clojure function's metadata that test wether the spec complies with the function's input/output.
- valenterry 5y ago> Eg, spec can warn you when an argument doesn't make sense relative to the value of a second argument. That's something that dependently-typed typesystems easily do as well if not better because inside of the function or data definition, the information is still available and used for code-completion, other compiletime checks etc.
- pjmlp 5y agoNot only that, Smalltalk and Lisps are languages designed with developer experience as part of the language. You just don't get an interpreter/compiler and have to sort everything else by yourself, no, there is a full stack experience and development environment.
- k__ 5y agoI think, the big issue with dynamic typing in popular languages like PHP and JavaScript are the automatic conversions.
- elwell 5y agoWhich is less of a concern considering Clojure's focus on immutability.
- k__ 5y agoThat's what I meant. As far as I know, some dynamic languages like Python don't have that issue.
- dgb23 5y agoYou mean implicit type conversions? That's a thing you can get somewhat used to. But it throws off beginners and can introduce super weird bugs, because they hide bugs in weird ways, even if you are more experienced. Yes, I find strong typing strictly better than weak typing. An even better example of this would be Excel, the horror stories are almost incredible. So even if your environment is dynamic, you want clarity when you made a mistake. Handling errors gracefully and hiding them are very different things. The optimal in a dynamic world is to facilitate reasoning while not restricting expression.
- dangerbird2 5y agoIt's always worth reminding folks that weak typing and implicit conversions can plague statically typed languages. C's implicit pointer array-to-pointer and pointer-type conversions are a major source of bugs for beginner and experienced programmers alike.
- robertlagrant 5y agoThis is a consequence of weak typing rather than dynamic typing. I appreciate that these are not precise terms, but being able to change something's type (dynamic) is different to the language just doing strange things when you combine types (weak).
- dharmaturtle 5y ago> What you get here goes way beyond what a strict, static type systems gets you, such as arbitrary predicate validation, Is this refinement types, which most static languages provide? https://en.wikipedia.org/wiki/Refinement_type https://en.wikipedia.org/wiki/Refinement_type > freely composable schemas, My understanding is that you can compose types (and objects) https://en.wikipedia.org/wiki/Object_composition https://en.wikipedia.org/wiki/Object_composition I'm assuming that types are isomorphic with schemas for the purposes of this discussion. > automated instrumentation I know that C# and F# support automated instrumentation/middleware. > and property testing. You simply do not have that in a static world. QuickCheck has entered the chat: https://en.wikipedia.org/wiki/QuickCheck https://en.wikipedia.org/wiki/QuickCheck
- dgb23 5y agoRight! I made the dumb, typical error to write: "You simply do not have that in a static world." When I should have written: "This type of expressiveness is not available in mainstream statically typed languages". With "freely composable" I mean that you can program with these schemas as they are just data structures and you only specify the things you want to specify. Both advantage and the disadvantage is that this is dynamic.
- dharmaturtle 5y agoAh, well if you're going to shit on Go/Java/C#/C++ I won't stop you :)
- CraigJPerry 5y ago>> Is this refinement types Well it does include that kind of behaviour but it's quite a bit more than just that. E.g. you could express something like "the parameter must be a date within the next 5 business days" - there's no static restriction. I'm not necesarily saying you should but just to give an illustrative example that there's less restrictions on your freedom to express what you need than in a static system. >> types are isomorphic with schemas I don't think that's a good way to think of this, you're imagining a rigid 1:1 tie of data and spec yet i could swap out your spec for my spec so that would be 1:n but those specs may make sense to compose in other data use cases so really it's m:n rather than 1:1
- c-cube 5y agoExcept, of course, that specs are only tested correct, not proven correct like types would be. Types (in a reasonable static type system, not, say, C) are never wrong. In addition, specs do not compose, do they ? If you call a function g in a function f, there is no automatic check that their specs align.
- cle 5y ago> Except, of course, that specs are only tested correct, not proven correct like types would be. Yes this is the fundamental tradeoff. Specs et al are undoubtedly more flexible and expressive than static type systems, at the expense of some configurable error tolerance. I don't think one approach is generally better than the other, it's a question of tradeoffs between constraint complexity and confidence bounds.
- dgb23 5y agoYes, I think that is one of the big weaknesses of it. You can write specs that make no sense and it will just let you. So far there is also no way to automatically check whether you are strengthening a guarantee or weaken your assumptions relative to a previous spec. In a perfect world we would have this in my opinion.
- undershirt 5y agoYeah, and I think this is obvious, but it certainly depends on the origin of the data being checked. We can prove the structure of “allowed” data ahead of time if we want guarantees on what’s possible inside our program. We also want a facility to check data encountered by the running program (i.e. from the user or another program.) which of course we can’t know ahead of time. It is a design decision to be able to build a clojure system interactively while it is running, so a runtime type checker is a way for the developer to give up the safety of type constraints for this purpose—by using the same facility we already need in the real world, a way to check the structure of data we can’t anticipate.
- travisjungroth 5y ago> Types (in a reasonable static type system, not, say, C) are never wrong. Oh man. This is the fundamental disagreement. Sure, you can have a type system that is never wrong in its own little world. But, that's not the problem. A lot of us are making a living mapping real world problems into software solutions. If that mapping is messed up (and it always is to some degree) then the formal correctness of the type system doesn't matter at all. It's like you got the wrong answer really, really right.
- scotty79 5y ago> ... such as arbitrary predicate validation, freely composable schemas, automated instrumentation and property testing ... Why static typing makes those things impossible?
- dgb23 5y agoThey don't make these impossible, they typically just don't let you express these within the type system and they typically don't let you not specify your types. I should have made clear that I'm emphasizing the advantages of being dynamic to describe and check the shape of your data to the degree of your choosing. Static typing is very powerful and useful, but writing dynamic code interactively is not just "woopdiedoo" is kind of the point I wanted to make without being overzealous/ignorant.
- thinkharderdev 5y agoThat largely depends on the type system. Languages like Haskell and Scala which have much more powerful type systems than C/Java/Go/etc absolutely do allow you to do those sorts of things. It is a bit harder to wrap your head around to be sure and there are some rough edges, but once you get the hang of it you can get the benefits of static typing with the flexibility of dynamic typing. See https://github.com/milessabin/shapeless https://github.com/milessabin/shapeless or a project that I've been working on a lot lately https://github.com/zio/zio-schema https://github.com/zio/zio-schema.
- scotty79 5y agoI think TypeScript is best of both worlds. Typesystem strong enough to express dynamic language and completely optional wherever you want.
- travv0 5y agoThe very first property testing library was written in Haskell, as far as I know.
- hota_mazi 5y agoIn 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!
- blacktriangle 5y agoI feel like there's a missing axis in the static/dynamic debate: the language's information model. In an OOP language, types are hugely important, because the types let you know the object's ad-hoc API. OOP types are incredibly complicated. In lisps, and Clojure in particular, your information model is scalars, lists, and maps. These are fully generic structures whose API is the standard Clojure lib. This means that its both far easier to keep the flow of data through your program in your head. This gives you a 2x2 matrix to sort languages into, static vs dynamic, and OOP vs value based. * OOP x static works thanks to awesome IDE tooling enabled by static typing * value x static works due to powerful type systems * value x dynamic works due to powerful generic APIs * OOP x dynamic is a dumpster fire of trying to figure out what object you're dealing with at any given time (looking right at you Python and Ruby)
- pjmlp 5y agoSmalltalk is "OOP x dynamic". CLOS is "OOP x dynamic". Common Lisp has arrays, structures and stack allocation. IDE were invented from Smalltalk and Lisp development experience.
- blacktriangle 5y agoCLOS is wierd since it sits in a multiparadigm language, so CL really spans boxes. JS is the same as well depending on how you use it. Smalltalk is a great example since it really did manage to do OOP x dynamic in a much better way and its worth wondering why Smalltalk pulled it off where Ruby feels like a nightmare. I suspect it has to do with focusing on the message passing aspect. So no, it's not a perfect model, but I think its more informative than looking at languages on a one dimensional static / dynamic axis.
- brundolf 5y agoMaybe I've just never given it a chance, but I've never understood the appeal of being able to modify code in-memory while it's running. I like a REPL for testing things out, or for doing quick one-off computations, but that's it. I would never want to, say, redefine a function in memory "while the code is running". Not just because of ergonomics, but because if I decide to keep that change, I now have to track down the code I typed in and manually copy it back over into my source files (assuming I can still find it at all). And if I make a series of changes over a session, the environment potentially gets more and more diverged from what's in sourced if I forget to copy any changes over. So I'd often want to re-load from scratch anyway, at least before I commit. Am I missing something? Am I misunderstanding what people mean when they talk about coding from a REPL?
- shaunxcode 5y agoYes, what you are missing is that usually you are using an editor like emacs where you can modify a specific form and send it to the repl. That way there is no chasing back through repl history for a form to paste back into a file.
- sooheon 5y agoInstead of typing things into a repl and copying back into source, imagine stepping through your source in a debugger.
- NightMKoder 5y agoAs others have mentioned - you’re probably talking about something like a python or node repl. Lisp repl development is not like python or node - you _work_ in the repl. The closer comparison might be between bash+node as a repl - up+enter and the like to rerun tests has an equivalent in clojure+IntelliJ. There’s no copy pasting but there are different key bindings. One of the best parts about lisp style repl development is that you end up doing TDD automatically. You just redefine a function until it does what you want from sample data you pass in - without changing files or remembering how your test framework works. You can save the output of some http call in a top level variable and iterate on the code to process it into something useful. The code you evaluate lives in the file that will eventually house it anyway so it’s pretty common to just eval the entire file instead of just one function. Since you don’t ever shut the repl down, developing huge apps is also quite pleasant. You only reload the code that you’re changing - not the rest of the infra so things like “memoize” can work in local development. That’s why it’s a bit closer to your bash shell in other languages. If you’ve never tried it, I highly recommend trying the Clojure reloaded workflow [1] to build a web app with a db connection. You can really get into a flow building stuff instead of waiting for your migrations to run on every test boot. [1] https://cognitect.com/blog/2013/06/04/clojure-workflow-reloaded https://cognitect.com/blog/2013/06/04/clojure-workflow-reloa...
- lakecresva 5y ago>You simply do not have that in a static world. I keep seeing lisp people bandy about all of this design by contract/arbitrary predicate validation stuff. Can you give an example of an instance in which static types + runtime checks don't completely subsume this? My intuition is that almost all of these methods people are talking about would have to be enforced at run-time, in which case I don't see how it's providing anything fundamentally more than writing an assertion or a conditional.
- valenterry 5y ago> These can be gained back with clojure spec and other fantastic libraries like schema and malli. What you get here goes way beyond what a strict, static type systems gets you That reads like what someone would think when they used the typesystem of Java 6 and now think that this is what "statically typed programming" means. No, you can _not_ get back what types give you by any kind of spec - if anything you can get some of the benefits of types, but you also pay a price. The thing is - dynamically typed languages don't really seem to evolve anymore. They add a bit of syntatic sugar here and there and sometimes add some cool feature, but mostly only features that already existed for a long time in other languages. At least that is what I have seen over the past couple of years, I would be happy to be proven wrong. Looking at statical typesystems however, there is much more progress, simply because they are much more complex and not as optimized. From row-types over implicits and context-expressions towards fully fledged value-dependent typesystems, which have amazing features that start to slowly trickle down into mainstream languages like Typescript or Scala. While both dynamically and statically typed languages have their pros and cons and it will stay like that forever, I expect that statically typed languages will proceed to become the bigger and bigger piece of the cake, simply because they have more potential for optimizations going forward.