Hacker News new | ask | show | jobs
by arwhatever 1704 days ago
Is it possible to elaborate in a comment? Honestly I probably wouldn’t take the time to read a lengthy article, but if there’s some elevator pitch then I’m all ears.
1 comments

What makes CL/Clojure really work is that your editor (emacs usually, but there’s other options now) connects to the live program and has access to the entire runtime environment. So, you can do a lot of the things other languages need static types for via introspection (e.g. autocomplete: CL just asks the running program what functions are available that matches the current pattern and returns a list).

Secondly, since I’ve learned statically typed languages, I already have a mental model for how they make you structure your code, except dynamically typed languages make patterns easy that would require something like dependent types to check (see how complicated Typescript is, because it has to be able to model JS idioms). My experience is that a lot of the value of static types isn’t in the checking but in the modeling aspect: if you follow the general patterns you’d use in Haskell (represent algorithms like “apply a function to each member of the list” as functions), you reduce the amount of thought it takes to see the program is correct by splitting it up. For example, if I have this pattern in my imperative codebase:

    let result = []
    for (let idx = 0; idx <= input.length; idx++) {
      result.push(input[idx]+1);
    }
    return result
I have at least three things mixed up together: accessing each member of a list (and there's an easy to miss off-by-one error in this implementation), transforming that member and building up a result. If I translate this to a functional style, it's easier to see that the implementation is correct:

    const inc = v => v+1
    . . .
    return list.map(inc)
Looking at this code, I can break down correctness into three questions: is list.map implemented correctly? is inc (the transformation) implemented correctly? And, assuming both are correct, are these two functions combined in the correct way? Types definitely can help here but my experience is that 90% of the benefit isn't the _checking_, it's the code structure you end up with as a result.[1]

Now, if this is true, why do I prefer dynamically typed languages? Well, it comes down to two things: I find the "live programming" model of CL/Clojure more productive and roughly equal to types when it comes to checking correctness (and I don't think it's just me, I've seen various papers, etc. that claim Haskell and Clojure have roughly equal defect rates); and, I find the patterns I like in CL/Clojure/Javascript require much more sophisticated type checkers to actually validate, and such type-checkers have a huge up-front learning cost and still add a lot of boilerplate that exists mainly to convince the type-checker that you know what you're doing.

Finally, in a language with macros, you can roll your own static guarantees: one project I worked on was doing a bunch of calculations inside a database. We hit an edge case where the DB's idea of a week didn't match our requirements. As a result, I wrote a code generator that generated Clojure functions and DB queries simultaneously. In this situation, if you assume the code generator is correct, you have a compile-time guarantee that the Clojure versions of the queries are equivalent to the calculations being done inside the DB.

[1]: This page surveys a bunch of studies on the question of dynamic v. static types and finds the evidence in favor of static types to be surprisingly small https://danluu.com/empirical-pl/

> This page surveys a bunch of studies on the question of dynamic v. static types and finds the evidence in favor of static types to be surprisingly small

Most of the studies seem to be rather poor though, so difficult to draw any solid conclusions from them. Almost all seem to drown in noise, or have flawed setups.

From personal experience, with a static type language I can jump into an unknown codebase and make non-trivial modifications much, much faster than if it's a dynamic type language codebase.

I've wasted soooo many hours doing print(dir(x)) in Python it's far beyond funny.

On the flip side, over the years I've helped countless people with their C/C++/Delphi code in minutes, frequently using libraries and API's I've never seen before.

Yeah, the evidence here is mostly anecdotal but, while we’re trading anecdotes, I think you have to distinguish Smalltalk/Clojure/Common Lisp from other dynamic languages. Most dynamic language essentially work like statically-typed languages without typechecking: you put code in a file and then run it all at once (or run unit tests) and see what happens. The languages I mention actually bring your development environment to runtime (twisted manhole and pry are the closest things I can think of here) so, you don’t have to run the whole thing, you can just run the parts you care about and see what they do.

That being said, my experience isn’t the same: I’ve been able to make helpful changes to dynamically-typed codebases in roughly the same amount of time as to static codebases. I’ve never really identified what it is about how I approach code that makes a difference here, but I think it is because I think about changes in terms of operational equivalence (e.g. l.map(a).map(b) === l.map(compose(b, a)) ) rather than in terms of data types.

This is actual JavaScript code, from one of my projects:

    function processAudioData(data, callback) {
       // dump audio data to WAV file  
    }
It's implementing a callback from a library.

Even reading the source code of the library I had problems figuring this one out. Had it been say C# code I'm pretty certain I would have had it done in seconds.

How do you solve this in seconds? I'm genuinely curious as this is something I often struggle with when having to use say Python or JavaScript.

I could see a statically typed language that would give you a live reflection system and macros. I think it's more if you have to chose, you'd rather have that than static types.

But I think it is possible to have all 3, it just doesn't exist in any popular language that I am aware of.

The problem is that the “live programming” aspect violates a fundamental assumption of a lot of static type systems: the “closed world” assumption that all the relevant types are known at compile-time. If you can dynamically extend/redefine the types on the fly, your type-system guarantees start getting weaker anyways. Instead, you need a system of contracts or something like Racket has.

Also, if you have macros, you can always just embed a Haskell into your language for the parts where you want that sort of guarantee: https://coalton-lang.github.io/

This is a really useful overview from which I gained some new insights. I appreciate you taking the time.