Hacker News new | past | comments | ask | show | jobs | submit login

Nice article. It does, however, trigger a thought:

> By making values mutable, this magical-changing-at-a-distance is always a possibility.

I agree, very much so. However, I could also simply promise that none of my code, nowhere, will modify that DateTime. On top of that, I'll ask all my colleagues to not mistreat it either.

Now, that's what many of us are doing now, and the entire point if the OP's first section is that that's just asking for trouble. But how is that fundamentally different from promising to never call toRubles() on a DateTime object? That's what you do with dynamic typing. Yo, here's an object. Please only call methods that it has, and I won't tell you which those are so you'll have to guess the types it may have and then browse to the API docs first (unless it has a method_missing, which you'll have too look at the API docs for as well - but what if it's duck typing and only one of the ducks quacks method_missing?)

Sure, I'm exaggerating. I like dynamic typing. I just have the idea that immutability is for nouns what static typing is for verbs. Just like Java has it half-assed, I feel a bit that Clojure has it half-assed the other way around.

In terms of Steve Yegge's conservative vs liberal discussion[0], it feels like Clojure went all liberal on one end, just to get super-conservative on the other.

[0] https://plus.google.com/110981030061712822816/posts/KaSKeg4v...




One difference is that in dynamic typing, if the object doesn't have the method, the runtime will tell you it can't be done.

If you mutate DateTime when the rest of the code expects it not to change, the runtime will let you, and you'll only notice through a logic error somewhere, that probably won't be obvious until way too late and the error has propagated through your system and screwed a chunk of your data.

On a conservative/liberal scale, decisions seem less conservative if the stakes are higher.


Yeah, replying to myself. I'm not sure if that's considered weird but ok.

According to this logic, most mainstream languages don't make sense. Notably, Java, C#, C, C++, Clojure are all screwed up. Not sure about other lisps. Ruby, PHP, JavaScript, Python, F#, Scala and Haskell have it right though.

(yeah sure, you can do mutability in Scala and immutability in Java, but the languages and their community lean toward the other, which is what matters here, I suppose)

Given that I've been much of a C# fanboy recently, my nose bleeds.


> it feels like Clojure went all liberal on one end, just to get super-conservative on the other.

In the brief time that I used Clojure, I also thought that the contrast between immutability and dynamic typing was very strange. Ultimately, I think it makes more sense than having the entire language be highly dynamic (or highly static).

I think Clojure's choice is not inconsistency, but that there is a kind of budget for craziness in a language. In Clojure, you can go crazy with dynamic types on the foundation of immutability, transactions, etc. In Haskell, you can go crazy with really awesome static typing tricks that would be utterly unfathomable if the language wasn't very rigid in every other way -- immutable, referentially transparent, side-effect-tracking. These two pack all their craziness into one corner of the language.

On the other hand, other languages -- Python, Javascript, Ruby -- seem to spread the craziness (I am not sure I picked the best word for this!) around, instead of having one super dynamic feature and everything else very inflexible. These languages tend to lack the really big flashy features: macros, typeclasses, etc.

It seems that you have a lot of flexibility as a language designer on how you spend your craziness budget, as long as you don't go too high (and become incomprehensible) or too low (Java?).


Maybe constraints rather than craziness? Constraints tailor a language, make it fitter for one purpose or another. But you have a limited constraint budget - overspend and everything is difficult.

As an old boss of mine (hi Paul Earwicker) used to say - "flexibility is just design decisions you haven't taken yet."


Craziness is not zero sum game, but it's hard to convince people that complain about "expression problem", "modular abstractions", "better DI", all the phrases to capture part of the problem, to read Real world haskell or Scala in Depth, both demanding books.

jerf did a good writeup, maybe a little overboard on clispscript, but besides that

http://www.jerf.org/iri/post/2908


Clojure's dynamic typing is somewhat different from other languages, though, as it really just has two non-atom types: seqs for collections and maps for data objects. Virtually all collections and objects can be manipulated as one of these two types. So "calling a method" on an inappropriate type is basically passing an inappropriate map to a function. You might want to disallow it, but Clojure allows it for good reason: this makes it possible to have many data access/manipulation functions that work on all objects. So the question is, do you want to forbid passing an argument that doesn't make sense to a function or open the door to lots of useful functions that would work on all objects. Clojure simply chose the latter.


> this makes it possible to have many data access/manipulation functions that work on all objects.

This is not actually true. Add proper type inference and structural typing, and it's perfectly possible to have all those generic data manipulation functions while also having strong static typing.


But using structural typing (on the JVM, where Clojure lives) is slow, because it requires using reflection, which severely limits its use currently.


I'm talking about structural static typing. At runtime, all the types would be fully determined, so no need for reflection. (Or vcalls for that matter).


How would you write a general function that prints out the values of all fields of any object (without reflection)?



I'm not quite sure, but I don't think this has anything to do with structural typing.


I think of it as each language making functionality trade-offs, in their design, for specific goals. Each language tweaking the recipe to make a language that best fits the programming model they advocate.


Huh?

If I do this:

    (defn to-rubles [x] (* x 1.2))
    (to-rubles (java.util.Date.))
I get an error:

    ClassCastException java.util.Date cannot be cast to java.lang.Number
How is this unreasonable?


The compiler didn't tell you it was broken. You had to run the program first.

Which means it's possible the error wasn't caught till it was in production.

Which means it's possible that a customer discovered a bug that a compiler could have discovered if you had used a language with a compiler capable of compile time static typing.


Which means you really, really, really need a good automated unit test suite. When you switch to a dynamically-typed language, you trade compile-time type guarantees for the ability to ask primitives and objects what type they are at runtime. So, you have to incorporate those questions into your unit tests. Especially when you are doing casting--which of course you should do only when there isn't a better option.

As an aside, static typing without type inference (a la Java) is just a pain. The compiler is just smart enough to enforce typing, but not smart enough to figure out what type anything is unless you explicitly tell it. This puts an extra burden on the programmer.

Fortunately Scala has type inference, so that's an option if you want to use a statically typed functional language on the JVM.


> When you switch to a dynamically-typed language, you trade compile-time type guarantees for the ability to ask primitives and objects what type they are at runtime.

This doesn't strike me as true. In a statically-typed language like C# or Java I can ask objects (and in C#, primitives) what they are all day long. Could you expand what you mean a bit?

(I also don't find Java particularly onerous regarding its lack of type inference--I mean, Java is onerous, just not because of that--and I find that I don't really use type inference much in C++ or C# either. But that is more of a question of taste.)


"In a statically-typed language like C# or Java I can ask objects (and in C#, primitives) what they are all day long."

Which shows that Java and C# are a hybrid of static and dynamic features. Using reflection and introspection to invoke behavior at run time is dynamic-language behavior. More strongly typed languages such as Haskell won't allow you to do this, as far as I know.


>More strongly typed languages such as Haskell won't allow you to do this, as far as I know.

Haskell has Data.Typeable. It'll let you reify some types for run-time reflection.


I mean that in situations where you have a possible type error, you can substitute compiler typechecking with your own typechecking by using something like (cond (= (type foo) bar)) or (cond (= (class foo) bar)). And you can decide how you want to handle it, instead of necessarily throwing a type error. Plus, polymorphic functions allow you to handle different types with the same function instead of having to overload it. And the test suite lets you run tests like (is (instance? Bar foo)) to help you catch type errors before you push to production.

So, what I mean is, although giving up the type-checking compiler opens up the possibility of introducing runtime type errors in production, it's far from a given that they are going to happen, because dynamic languages give you plenty of other tools to prevent them.

And Java's type system is onerous because it gives you all of the rigidity of static typing but none of the power of type inference. You really notice the difference when you switch to a language like ML or Scala that allows pattern-matching and polymorphic functions, unlike Java where you have to use overloading.


>Which means it's possible the error wasn't caught till it was in production.

Statically typed languages have plenty of errors that aren't caught until production. If you're really serious about compile-time guarantees, you'll want to use something like Haskell, Agda or Coq (in ascending order of extreme guarantees).

Of course, even formal verification won't protect you from an incorrect specification of your program.




Consider applying for YC's Summer 2025 batch! Applications are open till May 13

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: