Chris Smith - who wrote the original piece years ago - added a current prologue to the text. I think part of it is an excellent summary so I quote this part here:
> If pressed for an answer, though, yes I still do believe in the central conclusions of the article. Namely:
> That “static typing” and “dynamic typing” are two concepts that are fundamentally unrelated to each other, and just happen to share a word.
> That “static types” are, at their core a tool for writing and maintaining computer-checked proofs about code
> That “dynamic types” are, at their core, there to make unit testing less tedious, and are a tool for finding bugs.
> That the two are related in the way I outline: that one establishes lower bounds on correctness of code, while the other establishes upper bounds, and that questions about their real world use should come down to the possibility and effectiveness of addressing certain kinds of bugs by either computer-checked proof, or testing.
EDIT: When I wrote this comment the post linked to Steve Klabnik's 'reprint' of the temporary lost original article.
Currently it links to the original. The 'reprint' missed the prologue.
Doesn’t hold water, imo. If dynamic and static typing were really independent concepts, then why are languages only one or the other, never both? I explained my take on dynamic versus static typing here:
> The primary difference between static and dynamic languages, in my view, is this: in static languages, expressions have types; in dynamic languages, values have types.
Read the rest of the answer for more details in the context of Julia, which, as a dynamic language with a sophisticated type system, really crystallizes the distinction.
> "The dichotomy between static and dynamic types is somewhat misleading. Most languages, even when they claim to be dynamically typed, have some static typing features. As far as I’m aware, all languages have some dynamic typing features. However, most languages can be characterized as choosing one or the other. Why? Because of the first of the four facts listed above: many of the problems solved by these features overlap, so building in strong versions of both provides little benefit, and significant cost."
It's a bit like asking: If electric and gasoline motors are independent concepts, then why are cars only one or the other, not both? Well, some are, to varying degrees. Most aren't, because it adds a lot of complexity. I don't see any inherent contradiction here, nor do I even find the relative unpopularity of hybrid systems terribly surprising.
> in static languages, expressions have types; in dynamic languages, values have types
This kind of agrees with OP in practical terms, despite using opposite terminology. A type applied to an expression (syntax) is expressed as a proof about that syntax, and a type of a value at runtime is expressed in code that can safely crash in the face of an ill-typed operation. They both constrain program behavior, but do it with such radically different mechanisms that it's easy to argue they're fundamentally different. It depends on whether "fundamental" should be about mechanism or purpose. Let us know when you've solved that problem. :)
Julia's type inference can be seen as finding proofs of the properties of certain expressions and, if it finds them, using them to optimize the dynamic typing code, basically eliminating dead branches for type checks that can't fail. I'm tempted to think of languages with nominal subtyping as being hybrids as well: the base class is the static type, and the runtime class is the dynamic type.
All languages have some notion of static types and dynamic types. They're most easily bucketed by whether they consider more than one type.
C is statically multityped but dynamically unityped. Python is statically unityped but dynamically multityped. Haskell, since it uses lazy evaluation, is statically multityped and dynamically multityped.
The main issue with static unityping is that insufficient testing can easily lead to type errors in production (or worse). And static multitypes help static analysis tools reason about code more easily.
Introducing a new type that is intended for use in a specific context that can easily be tracked is tremendously useful when programming at scale.
But C++ has static and dynamic types, and a C++ programmer needs to understand both.
For example, in inherited methods the selection of the method implementation is based on the dynamic type of the (pointer) value, but the selection of the default arguments is based on the static type of the value.
Given algebraic type (as in Haskell - data List a = Nil | Cons a (List a)), you can have static type verification (map :: (a -> b) -> List a -> List b) and dynamic dispatching for situation in concrete part of a program:
pairwiseReduce :: (a -> a -> a) -> List a -> List a
pairwiseReduce f (Cons a1 (Const a2 as)) = Cons (f a1 a2) (pairwiseReduce f as)
pairwiseReduce f as = as
We fix the type, but concrete instantiation of a type may vary in run-time, so we pattern-match: the list can have at least two elements (first case) and we reduce them and the rest of the list or it has less than two elements and we leave it as it is.
I took my time and read about Julia's type system. It is really the worst from both worlds: it does not allow for tagged unions (like List I declared above), it does not allow for adhoc polymorphism (1+2 gives error for floating type and evaluates for integers), there are no type classes (that adhoc polymorphism thing) and, thus, no good guarantees for whatever substitution for GADTs Julia has. And it also has inheritance which makes type inference exponentially harder in simple cases.
So I urge you to take your time and read about Haskell's type system. It really allows you to have static and dynamic types and vary their presence and utility as you see fit.
ML Style constructors are not particularly idiomatic, but you can write basically the same thing in Julia:
struct Cons{T, S}; head::T; tail::S end
pairwiseReduction(f, a::Cons{T, Cons{S, Tail}}) where {T,S,Tail} = Cons(f(a.head, a.tail.head), a.tail.tail)
pairwiseReduction(f, a) = a
I think the primary problem here is one of terminology. Take Stefan's quote from the post you're responding to:
> The primary difference between static and dynamic languages, in my view, is this: in static languages, expressions have types; in dynamic languages, values have types.
There is a clash in terminology here. People coming from a static languages background would use the term "tag" for what dynamic languages people call a type. There isn't really a super good word in the dynamic languages world for what static languages people call a type, and the word type is generally re-used, since the distinction isn't as important (generally there is an embedding of the tags into the type system, so using "type" for both can be sensible). I'm gonna adopt (my best interpretation of) the static languages terminology here for this comment.
Some languages have both types and tags. The biggest difference then between static and dynamic languages are which of the two is primarily dispositive (in the sense of determining the semantics of the language). In dynamic languages, the tags are, in static languages the types are. This choice has deep seated implication for the kind of programs that can be written and the behavior of the program.
In fact, you sometimes end up in situations where static languages have multiple tag systems (but generally only one type system) and dynamic languages have multiple type systems (but generally only one tag system).
In julia, the tag system is primary, but we have a very rich type system that is embedded in the tag system by parametricity and is used for dispatch. This type system is constructed in such a way that the subtyping relationship is induced by tag system (for types T, S, (T <: S iff for all values v s.t. tagof(v) <: T, tagof(v) <: S)) [1]. You basically want to choose a decidable type system that satisfies this constraint, but is at the same time sufficiently expressive to be useful for what people want to do.
We also have another, larger type system that is used for inference judgements, but that is generally transparent to the user. It is in principle possible to type check over any of these type static system, but type checking is not part of the semantics of the language.
The problem with statements like "Julia's type system does not allow for X", is that it presumes that the type system having some feature is a pre-requisite for having the corresponding functionality. So while Julia's type system does happen have support for unions (and universally qualified unions), the significance of that is for dispatch, not for whether the language can have expressions that can take on values of different tags.
In any case, I can assure you that many people working on Julia are well familiar with Haskell's type system. Hopefully I was able to convince you that there is at least something interesting about the way Julia's type system works.
> then why are languages only one or the other, never both?
what do you mean by that ? multiple statically-typed languages have "var" or "any" types which can be used to go from the static to the dynamic typing world.
"Any" types do not imply dynamic typing. Languages like Haskell are purely statically typed yet do not require explicit, concrete types for everything. Type inference is the feature which bridges this gap.
> That “static types” are, at their core a tool for writing and maintaining computer-checked proofs about code
> That “dynamic types” are, at their core, there to make unit testing less tedious, and are a tool for finding bugs.
They are not rival. Imagine a gradual typing design, it has a statically typed system, but it won't stop you from running it. The type errors would only be suggested in IDEs and stop building on CI environment but you can run it anyway. Then you can have both checked proofs and debugging advantages. (Dialyzer for Erlang is an example but there could be more strict typing)
Another similar topic is 'compiled' and 'interpreted', instead of static and dynamic types. 'Compiled' is runtime favored because the code was compiled is ready to be executed a lot of times, while 'interpreted' was executed once on demand because you may change your code at any time, so there's no point compile it for one person. Plus source code is much easier to debug than byte code.
It would be interesting to see languages could have solid static/dynamic/compiled/interpreted at the same time. Elixir/TypeScript/F#, etc. are good examples but this idea can definitely go further.
>> That “dynamic types” are, at their core, there to make unit testing less tedious, and are a tool for finding bugs.
To the extent this is true, it's true because modern dynamic type systems are fairly strong. They enforce a contract about what operations are valid on a given type, and you can't usually get out of the contract. If you can cast in Python like you can cast in C, I don't know about it.
Part of the problem is that we've lost a whole quadrant of languages in the static-dynamic weak-strong graph: Weak dynamic typing is nonexistent these days.
We used to have it. B and Bliss were both weakly dynamically typed languages. Extremely so, in that they only had one datatype: The machine word. Since literally any operation was valid to perform on a machine word, there was no way for the type system to disallow anything, so it faded into irrelevance. Much like Bliss, in fact.
That's taking it to an extreme, shoving the "weak" part of weakly-typed so far that the language is untyped, like assembly language is usually regarded as being.
> If you can cast in Python like you can cast in C, I don't know about it.
Note that "cast" and "coerce" are different. Python supports coercion but not casting. Casting is an operation done in static type systems where you tell the compiler "I know this has type X, but pretend it has type Y". Coercion is a run-time operation which moves data from a box of one type into a box of a new type (i.e., a new object is allocated).
> Part of the problem is that we've lost a whole quadrant of languages in the static-dynamic weak-strong graph: Weak dynamic typing is nonexistent these days.
I'm not sure what you mean by "the problem" here. Weak dynamic type systems are, in my opinion, the worst of all worlds. They provide neither compile-time checks nor runtime checks that really amount to much. I don't know why you would ever prefer a weak dynamic type system in a language.
JavaScript is essentially weak and dynamically typed. The weak part comes from all the implicit coercions the language inserts to ensure your code nearly always produces some result. This is terrible because it makes identifying errors exceptionally more difficult than in a strong or static type system.
> Note that "cast" and "coerce" are different. Python supports coercion but not casting. Casting is an operation done in static type systems where you tell the compiler "I know this has type X, but pretend it has type Y". Coercion is a run-time operation which moves data from a box of one type into a box of a new type (i.e., a new object is allocated).
Right. I know this, I just call coercion "autoconversion" because the data is typically given a new form as well as a new box. Having autoconversion without casting is a sign of a strong type system, because you can't turn off the type system, you are only allowed to use it. If there's no way to autoconvert from type A to type B, that's it: You can't turn a value of type A into one of type B by hook or by crook.
> I'm not sure what you mean by "the problem" here.
"Problem" in the sense of our discussion being incomplete, nothing more.
> Weak dynamic type systems are, in my opinion, the worst of all worlds. They provide neither compile-time checks nor runtime checks that really amount to much. I don't know why you would ever prefer a weak dynamic type system in a language.
I agree with all of this.
> JavaScript is essentially weak and dynamically typed.
Wrong, for the reasons I went over in other posts.
Ah I didn't realize I had replied to the same person in different places. Sorry about that haha.
> I know this, I just call coercion "autoconversion" because the data is typically given a new form as well as a new box.
Hmm interesting. I think I dislike this because coercion is not always automatic, so the "auto" part of "autoconversion" is a hang-up for me. For example, doing `int("3")` in Python is an explicit coercion, but I don't think it would qualify as an "autoconversion" (unless I'm not quite understanding your definition of that term). In the interests of clarity until I understand better, I'll continue using "coercion" and I'll specify implicit vs. explicit as needed.
> Having autoconversion without casting is a sign of a strong type system, because you can't turn off the type system, you are only allowed to use it.
I agree for explicit coercion, but not with implicit.
If your coercion is implicit, then the user has no control over what's going on. The type system is essentially transforming values' types behind the curtain in an effort to prevent errors. This means that errors which the user might believe should be raised may not be. The type system is "weak" because it's not holding types rigidly with respect to the user's perspective. The user's perspective is key, because if we look at things from the language's perspective then almost any language could be viewed as strongly typed given the right theoretical framework.
> "Problem" in the sense of our discussion being incomplete, nothing more.
Is JavaScript not considered to be at least partially on the weak side of the spectrum? Things like using the `+` operator on arrays and objects and getting back numbers and integers seems to be to be weak typing.
> Is JavaScript not considered to be at least partially on the weak side of the spectrum? Things like using the `+` operator on arrays and objects and getting back numbers and integers seems to be to be weak typing.
I might be in the minority on this, but I draw a distinction between stuff which gives odd results but is defined and controlled, and stuff which is unspecified and only 'defined' to the extent the machine does... something when you do it.
Everything Javascript does is in the first camp. Autoconversion is specified, it's part of the language design, and the implementation handles it. What C does when you use casts to turn a string of chars to a double and add the result to some other value is undefined, and whatever the hardware happens to do is what happens.
So doing a lot of autoconversion to the point you can feed any value to any function and no errors happen greatly reduces the value of dynamic typing in making testing easier. However, the implementation is still keeping track of types, because it's doing the autoconversions for you.
The Javascript equivalent to Bliss is using stringly typing to the extent your only language-level datatype is the string, and every other type only exists in the programmer's head. But that's not a property of the language as much as it's a property of the specific codebase.
> I draw a distinction between stuff which gives odd results but is defined and controlled, and stuff which is unspecified and only 'defined' to the extent the machine does... something when you do it.
Whoa, this is totally orthogonal to the strong/weak typing distinction.
Traditionally, strong/weak typing refers to how easy it is to "trick" the type system into doing something it wouldn't otherwise want to do. C is weakly typed because you can cast things all over the place and do whatever you want with the actual values, regardless of what type they're "supposed" to have. This is completely unrelated to undefined behavior.
The C type system is perfectly happy with casts. The behavior of casting is well specified. And yet the fact that the type system supports casts to the extent that it does means that C's type system is weak.
But this is not the only metric to judge a type system's "strength", and it was poor word choice on my part that I phrased my statement in a way that made it seem so.
JS's type system is weak because of implicit coercion. The fact that values of type X can be coerced into values of type Y at runtime without any notification to the user is indicative of a weak type system, regardless of how well-specified this behavior is. Weak type systems do not lack specification; they lack a feeling of types being "rigid" or "strong". If your values can be converted to different (semantically incompatible) types, then your type system is weak because it isn't telling you anything.
A strong type system is one which stands up to you and says "No, I won't let you do that because I don't think that's what you meant to do." A weak type system is one which does its best to let you do whatever you want. Again, the specification of the behavior of various operations within the type system (such as casting or coercion) is not the determining factor in assessing the "strength" of the type system.
Maybe? I think that phrasing is lacking in some clarity. Does Python care? Most people would say "no", but Python is strongly typed because you simply cannot use values with the wrong attributes. (The nominal types don't matter in Python, generally, but the structural types do.)
Python says no in a (sufficiently) sensible way at runtime to certain classes of things, and tries to let you do what you want about others. I was referring to things like undefined behavior in C as instances where the language "plain doesn't care" what you want.
I don't think UB has any particular bearing on a conversation about the "strength" of a type system, actually. There are plenty of non-type-related UBs, and those UBs that are related to types do not necessarily make the type system weaker. C's type system is weak due to things like explicit casting that can circumvent any type safety, for example.
Python does no runtime checks of types. The type system employed by Python is effectively a structural (not nominal) system. So long as you provide things with the right structure (i.e., your objects provide the correct fields/methods), everything will work. But many people would like say that Python just "doesn't care" what you want because of the lack of any nominal type checking at any point.
UB just means that the language specification makes no assertion about how to handle a given case — the behavior is literally just not defined. Any implementation of the language could choose to raise an error for such cases and that would conform correctly to the standard.
Note that I specifically said "undefined behavior in C", where it pretty much means exactly that. It is not the same as "implementation defined". It's true that an implementation could in theory respect types even in the case of undefined behavior, but it is not promised by the language spec.
JS facilities such as `typeof`, `instanceof`, `Array.isArray`, and built-in support for protocol checking (e.g. `Symbol.iterator`), and many related runtime facilities — despite their warts and limitations — place the language's dynamic typing in the strong camp.
I don't think the presence of these facilities grants a type system a "strong" classification. None of them actually enforce anything on their own — they're just functions and expressions that are provided to the programmer to implement their own dynamic type-checking via introspection/reflection. They do not, on their own, constitute a strong type system.
Python is strongly typed because you cannot use a value of type X in a position where a value of type Y is expected. But Python doesn't check in advance — it just tries to go and do the thing. The trick to this is that Python's type system is actually structural instead of nominal; it doesn't care what the name of anything is, so long as it's got the right fields. (This is where we get "duck typing" from.) But you can't, for example, have Python do `"3" + 2`. A runtime error will result, because the language does not perform any implicit coercions.
JS is different. There are some native implicit coercions from certain types to other types. The presence of these makes JS's type system weaker than Python's, so in general I see it grouped as "weakly typed".
You can try the following in a browser's JS console or in a NodeJS REPL:
> for (const x of [1,2,3]) { console.log(x); }
1
2
3
undefined
> for (const x of {a:1,b:2,c:3}) { console.log(x); }
TypeError: {(intermediate value)(intermediate value (intermediate value)} is not iterable
So, sometimes JS automatically performs type conversions. But it also has the same kind of type-related runtime errors as Python. It just depends what you're doing.
Considering the time when it was written, it could mean for example Java's ridiculously long generic declarations which couldn't be inferred or aliased.
Things are much better and more ergonomic these days.
Specific example - some languages will allow you to do:
fun f(x: int): // return type inferred
var foo = something(x)
return foo
While others need (including type erasure getting in the way):
Disagree. Type erasure is actually one of th best possible decisions made by the java designers.
Languages that have higher kinded types (Haskell, Scala)are virtually impossible to implement without type erasure. This is why the .net runtime doesn’t have languages with HKT
I don't see how the absence of type erasure could possibly be a restriction. Cast everything to object if you must.
I've literally never run into a situation where I wished type erasure was there but didn't have it, but I've ran into situations where type erasure caused problems in Java and where the absence of type erasure let me do things the way I wanted in C#. typeof(T), new T[], new T(), default(T), etc.
> I don't see how the absence of type erasure could possibly be a restriction. Cast everything to object if you must.
Erasure can be nicer than casting to object everywhere, especially when dealing with interfaces. It means you don't have to define separate interfaces for the erased and specialized cases.
e.g. in Java, implementing List<T> means you also implement List. Meanwhile, in C#, you have to implement both IList and IList<T> if you want to be able to use a collection in an erased context (and even then, the interfaces don't provide exactly the same functionality). Implementing both IList<T> and IList<object> isn't much of an improvement.
> I've literally never run into a situation where I wished type erasure was there but didn't have it, but I've ran into situations where type erasure caused problems in Java and where the absence of type erasure let me do things the way I wanted in C#. typeof(T), new T[], new T(), default(T), etc.
IMO most of these aren't too bad to work around. Although, sometimes erasure can cause problems with reflection and you have to use Guava's TypeToken or something similar.
One definite problem with erasure is that it doesn't play nice with unboxed value types.
Sure, good interface design can ease things, but it doesn't really solve the problem I'm talking about.
In Java, List<Foo> and List<Bar> are the same interface. In C#, IList<Foo> and IList<Bar> are different interfaces that just happen to have similar properties/methods.
Right, but again, it means you're implementing two separate interfaces. You can implement IList without implementing IList<T>, and you can implement IList<T> without implementing IList.
> Of course it could be implemented type-erased style, but that's an unpleasant can of worms.
> If it were implemented like C++ templates, it would be a source level feature, meaning you can't have higher kinded polymorphic methods in assemblies. I think we can reach consensus that such a feature would only pollute the language.
> If it were implemented like Java generics, it would lead to all sorts of nonsense. For example you couldn't do typeof(M<>) inside the hkpm, since the type has been erased.
And then a complex-but-good implementation idea follows. Type erasure is not necessary in this case.
The complexity is exactly is issue. So while it’s possible to implement on the .net runtime without type erasure, it’s hard.
Plus the only reason type erasure is bad is if you want reflection. Which in FP languages with a solid type system isnt idiomatic and safe anyway.
Edit: value types are one other reason for not using erasure but Can be done other ways
> ... “dynamic types” are, at their core, there to make unit testing less tedious, and are a tool for finding bugs.
I don't think this is a good description of dynamic typing. I'd probably go with something more like, dynamic typing is a way to defer until runtime typing aspects of the program's implementation. I suppose dynamic typing does make unit testing easier, among other things, but it feels wrong to say it's "for" that. A car is for getting one or more people from point A to B. It also has a radio, but a car is not "for" listening to the radio.
> I suppose dynamic typing does make unit testing easier, among other things, but it feels wrong to say it's "for" that.
The summary is trying to say that that's dynamic typing's primary advantage. Any other advantage you'd care to ascribe to dynamic typing is not intrinsic to dynamic typing itself.
The whole article seems concerned with the testability and bug-finding nature of types. But the main strength of dynamic types is the ability to defer decisions until runtime.
JS isn't dynamically typed to make testing easier. JS uses dynamic types because you couldn't build the web any other way.
> But the main strength of dynamic types is the ability to defer decisions until runtime.
That's not a strength. Deferring until runtime only that which must be deferred is a strength, but that's not one exhibited by dynamically typed languages. It is one that's available to statically typed languages though, like C# and now even Haskell has a deferred typing mode.
> JS uses dynamic types because you couldn't build the web any other way.
That's patently false. The whole engine running every browser is statically typed. There are libraries for working with the full DOM in HTML documents for Java, C# and other typed languages.
Obviously the ability to defer decisions to runtime is a strength - how could you possibly dispute that?
And yes statically typed languages have this strength if they use dynamic types. The article's point is that static and dynamic types are fundamentally unrelated to each other. Many languages can and do use both.
When you make a web page you have no idea what "only must" be deferred. There's no way to enforce that the JS you write today will be statically compatible with the libraries and browsers that it touches tomorrow. That's why JS uses dynamic types.
> That's patently false. The whole engine running every browser is statically typed
My point is not about building web browsers, it is about building the web. The web has JS which must coexist with arbitrary other JS, running on many different browsers with many more versions. How could you possibly build this without dynamic typing?
> The article's point is that static and dynamic types are fundamentally unrelated to each other
Not unrelated. You can embed dynamic types into a statically typed language, but not vice versa. Formerly dynamically typed languages that pull in static types, like Racket or TypeScript, become statically typed languages with embedded dynamic types.
> There's no way to enforce that the JS you write today will be statically compatible with the libraries and browsers that it touches tomorrow
This has nothing to do with dynamic types but with standardisation. C# is a language that is considerably different from its first release, and yet v1.0 C# still compiles on the latest compiler. The converse is not true, for either JS or C#. Dynamic types don't help with this problem.
> The web has JS which must coexist with arbitrary other JS, running on many different browsers with many more versions. How could you possibly build this without dynamic typing?
How do all the arbitrary programs on your computer coexist? The programs that interact do so via protocols or standard interfaces, and the ones that don't interact are isolated. This is no different than any other software.
> Formerly dynamically typed languages that pull in static types, like Racket or TypeScript, become statically typed languages with embedded dynamic types.
These languages are not "formerly dynamically typed." These languages are fully dynamically typed. Dynamic typing does not mean the absence of static types! That's the main point of the article.
I use and love statically typed languages. Dynamic types are a "yes and", enabling new things. ObjC and TypeScript are examples of languages with static and dynamic types.
> C# is a language that is considerably different from its first release, and yet v1.0 C# still compiles on the latest compiler. The converse is not true, for either JS or C#. Dynamic types don't help with this problem.
Actually dynamic types help enormously with this problem. They enable testing for the presence of features at runtime, and conditionally invoking code that otherwise could not type check.
Consider for example preventing event propagation in JS. This varies by browser so you might write:
if (event.preventDefault) event.preventDefault();
dynamically checking for the presence of a field, and then invoking it as a function. This is where dynamic types shine.
You can write this in C# or Java using reflection, because these languages have dynamic types. You can't write this in languages without dynamic types.
> How do all the arbitrary programs on your computer coexist? The programs that interact do so via protocols or standard interfaces
Every program on my computer is a conversation between the application code, the system libraries, and plugins. The browser I am using to type this dynamically interrogates the classes that it uses to discover their features, and conditionally invoke code. Example [1] - notice how this statically typed code is making a dynamic type query, illustrating that these concepts are completely compatible.
My computer's programs rely on dynamic types. That's how they retain compatibility across different versions, and it's how the OS vendor keeps the programs working across system updates.
> These languages are not "formerly dynamically typed." These languages are fully dynamically typed. Dynamic typing does not mean the absence of static types!
No, dynamically typed means the program has only one type: the universal value.
> They enable testing for the presence of features at runtime, and conditionally invoking code that otherwise could not type check. [...] You can't write this in languages without dynamic types.
That's incorrect. Extensible idioms don't look the same as they do in dynamically typed languages, but looking the same is immaterial. The same sort of behavioral extensibility is achievable with static typing without reflection, for instance, as found in the Yi editor [1]. Once again, it's about protocols and interfaces.
> Example [1] - notice how this statically typed code is making a dynamic type query, illustrating that these concepts are completely compatible.
That is merely one way to do it
> My computer's programs rely on dynamic types. That's how they retain compatibility across different versions, and it's how the OS vendor keeps the programs working across system updates.
That's not dynamic typing, compatibility is the advantage of standardized protocols and interfaces.
> Fallacy: Dynamically typed languages provide no way to find bugs
>
>A common argument leveled at dynamically typed languages is that failures will occur for the customer, rather than the developer. The problem with this argument is that it very rarely occurs in reality, so it’s not very convincing. Programs written in dynamically typed languages don’t have far higher defect rates than programs written in languages like C++ and Java.
Having written software for paid work in both Java (static typing) and JavaScript (dynamic typing), I agree. A statically typed language does not improve the quality of the software that gets written, assuming equal skill in both languages.
The kinds of bugs that get caught by a static type system are also caught by a good suite of automated tests, where every test is written before the production code that makes it pass.
The article doesn't mention refactoring, but this is where a static type system really shines. The ability to refactor an entire code base with confidence at the click of a button is extremely useful and the thing I miss most when using dynamically typed languages. You can get some of that with modern editors, but it's just a pale shadow of what you get with static typing.
>The kinds of bugs that get caught by a static type system are also caught by a good suite of automated tests, where every test is written before the production code that makes it pass.
That's not a small thing though. It's one extra thing that you just have with static typing. Not that you don't need tests but at least you will have already some bases covered.
For me a big reason of why I like static types it's the ease of reading even if a developer was sloppy. I have read dynamically typed code where the same parameter can be a string or an array or an object depending on what's the weather like outside when a dev needs to call a function. This sort of confusion makes it exponentially harder to read and understand code IMHO.
> The kinds of bugs that get caught by a static type system are also caught by a good suite of automated
This is correct in theory, but to get the same benefit you'll need to write all the tests that the static language would guarantee. That's both relying on the programmer being always correct and that they think the time writing those tests is worth it.
Stating typing you have to explicitly opt out of, and automated tests you have to go to quite some effort to explicitly opt into, and most teams I've been on have been quite poor at this opting in.
Code is worked out until it has few enough bugs for the customer to accept it. A program bug count is a function of how it will be used, and how sloppy the developers (or their company) thinks they can get away with, it's almost no sensitive to how easy or hard it is to find and correct the bugs.
That said, Java's type system sucks, you can not extrapolate its problems into a general concept.
> The kinds of bugs that get caught by a static type system are also caught by a good suite of automated tests, where every test is written before the production code that makes it pass.
To make matters worse, one would also have to write tests that cover every possible path through the function to ensure none of them change a value into a shape the rest of the code doesn't expected. This gets very tedious if it has to be done for every function.
> The kinds of bugs that get caught by a static type system are also caught by a good suite of automated tests, where every test is written before the production code that makes it pass.
Mostly, but it depends how you're using the type system. I have a talk (slides at https://dlthomas.github.io/using-c-types-talk/slides/slides....) that ends with enlisting the C type system to statically catch a "you called this from the wrong thread" error - with no run-time overhead.
Given that the same articles come up over and over again, and attract upvotes and comments, it seems clear that the userbase gets value out of reposts.
Perhaps the HN software should turn the knob the other way, and automatically repost old links which got a lot of attention previous times.
I wonder how many of Hacker News' current users were around in pre-2012. This sort of post could be "new to them"-- they may never have thought to search for the topic, and the repost could bring new perspectives.
I apologize, I should have worded that more politely.
I assumed there was no automated dupe linking because I regularly see comment posts with links to earlier discussions. Always struck me as the kind of thing a computer should be doing.
A few years back, there was a user that used a script to find duplicate submissions and put them in a comment on every story. I can’t seem to find it now; I thought they had “giraffes” in their username? Anyway, it was mildly controversial, and they stopped.
My background is php and then javascript. My first experience with 'types' was learning that apparently javascript's implicit type conversion was not normal and actually pretty bad. That made sense to me.
Then I learned about explicit types via TypeScript, and that made sense to me too, although I could imagine how this could explicit typing could get in the way of my prototyping, small projects, or REPL-driven development.
Now, a few years later, and having learned a bunch of new languages, both explicitly and implicitly typed, I'm mostly wondering why I'd go for anything that isn't gradually typed as an ideal approach.
I get the impression that the core discussion is explicit vs implicit (correct me if I'm wrong), and I can see how in some cases enforcing explicit typing is worthwhile, but for most of the work I actually do, it seems like the best solution is something gradual and flexible. Typescript, or perhaps some (improved) version of Elixir/Erlang's Dialyzer, or Clojure's spec: flexible enough to allow for said prototyping, small projects, and REPL-driven development, but gradually rigid enough to be useful when things grow larger and more solidified.
> I'm mostly wondering why I'd go for anything that isn't gradually typed as an ideal approach.
In some ways, gradual or optional types give you the best of both worlds. You aren't obligated to submit to the whims of the type checker when you don't want to, but you can elect to get the safety of types where it matters. You can get the brevity of dynamic typing and the safety of static types.
But, in other ways, they give you the worst of both worlds. If you have any sufficient fraction of typed code in your program, then you will feel pressure to structure even the untyped parts of your program in ways that play nice with static typing. Some API design choices are more "typable" than others, and having static types at all discourages you from some interesting API choices.
In order to play nice with untyped code, most gradual and optionally typed languages are deliberately unsound. There are holes in the type system that can't be closed. That means compilers cannot rely on types for optimization purposes.
You end up with the rigidity of static types and the performance of dynamic types.
I wonder if coming from the other way would be acceptable for fans of dynamic typing.
Instead of adding types gradually to a dynamic language (like Typescript sorta does with Javascript), we could add type-checking features that enable a kind of "statically-checked duck typing". Complete inference (like Elm, Crystal or Haskell) gets you halfway there – most of the time you don't have to write method/function signatures.
Then add structural typing like Extensible Rows [1] or Elm Records [2], or something that type-checks based on the structure instead of classes, and it allows you to have the compile-time type-checking of Interfaces or Generics/Templates without the extra typing. Then you can add explicit generics/templates later if you want.
Both Crystal and Elm get very close to that for me, but not 100%. I honestly think that this is a safer bet than gradual typing that could lead to very similar results, but I don't know how feasible or acceptable to mainstream programmers this is.
If you haven't already, you should see this paper from 2017 about sound gradual typing: https://www.cs.cornell.edu/~ross/publications/nomalive/nomal... The performance and rigidity issues you mention are solvable. Although the research is pretty cutting-edge and not implemented in many programming languages yet.
It's quite interesting seeing the polarization of people's opinions on type systems. I have friends who adamantly argue that static types slow them down and prevent them from adapting quickly. On the flip side I have friends who claim they cannot write code without static types and that JavaScript is unusable.
When really, both styles have their value. Static types are very useful and do provide some nice safety. However, static types can also be quite annoying and force you to acquiesce to the type system. You have to learn to "speak" types, which can be very tricky. There's a joy that comes with the freedom of no static types.
> You have to learn to "speak" types, which can be very tricky. There's a joy that comes with the freedom of no static types.
If you struggle with "speaking types" then that's an indication that you don't understand the domain of values your code works with. Having a solid plan for exactly what shape your data can take is empowering, not restrictive. To some extent, certain restrictions provide greater freedoms in the sense that you can make more accurate assumptions about your data without needing to perform a bunch of run-time checks or write hundreds of unit tests to ensure your code works in all possible situations.
> If you struggle with "speaking types" then that's an indication that you don't understand the domain of values your code works with.
I'm not sure I agree. I've met intelligent programmers who can elucidate the domain of values which their code is utilizing, but simply cannot do so in the framework of a given type system. Or perhaps they just find it difficult or painful to conform their description to the grammar of the type system. Understanding the data domain and being able to describe it in a specific type system are different skills.
Even if one is completely in favor of static types, it's important to be sympathetic to this point of view so that we can design better, more friendly type systems going forward.
There are definitely some failings in static type systems that can cause programmers to struggle, regardless of how well they understand their domain of values.
I guess I was thinking more of people I've talked to who advocate for dynamic type systems for rapid prototyping, because they don't want to be bothered to stop and think about their types first — they kind of just go with an approximate view of their data held in their head. I think this style of programming leads to a lack of understanding of the domain of values down the road, which is more what I meant to address. But I did not sufficiently explain this in my previous comment haha.
> I guess I was thinking more of people I've talked to who advocate for dynamic type systems for rapid prototyping, because they don't want to be bothered to stop and think about their types first
Totally! I wish we taught more type directed programming in general. It's a good way of thinking and can guide your process very nicely.
I've slowly come to the conclusion that instead of assuming my friend who doesn't like static type systems is misguided, I should try to figure out what aspects of static type systems need improvement so that a person like him would use them. Even if I don't agree with him, I can still use his point of view as a different perspective.
> I've slowly come to the conclusion that instead of assuming my friend who doesn't like static type systems is misguided, I should try to figure out what aspects of static type systems need improvement so that a person like him would use them. Even if I don't agree with him, I can still use his point of view as a different perspective.
Ah yeah, that's a very pragmatic view on things! And totally reasonable. I intend to work on improving type systems, so this is definitely the mindset I strive to maintain more often than not haha.
Most often in imdustry, you have to begin any complex project without a deep understanding of your domain values, because that understanding only comes iteratively. In my experience, the biggest drawback of static typing is that it forces you to design your types when you still don't have enough knowledge, and changing it later is painful.
> In my experience, the biggest drawback of static typing is that it forces you to design your types when you still don't have enough knowledge, and changing it later is painful.
I 100% understand where you're coming from, but I view this as an advantage of static type systems. The "pain" is you having to go through your code and make sure that you change your usages of given types to match the new definitions, right? This is forcing you to make sure your code is still correct with respect to the types.
Most dynamically-typed languages let you skip this part, but this causes you to lose clarity. Is your old code still correct? Or is it just mostly correct? Are there new unexpected edge cases being introduced that you didn't bother to go check on due to faulty now-outdated assumptions?
Changing your types is a pain, I agree. But programs are just about data transformations, and types are representations of the shape of those data. Changing your types without changing every part of your program that touches those types inherently means that your program is now running on invalid assumptions. Sometimes it works fine, but I think it is better to know it's fine than to just say "Well, it seems to work in most cases."
The risk you describe is real, but good testing coverage lowers it in a way that I find way more efficient than static type checking. When I have that good coverage - which for me means the full pyramid of testing, not just unit tests - I almost never find that problem.
Refactoring when you change your understanding of the domain can be a lot of work also when you have dynamic typing and lots of tests, but if the tests are well designed, I rarely feel that as painful as it was when I felt I was battling with the type system.
PS: my main experience with static typing was with c++, it's possible that more modern type systems could have improved the situation.
I find static types useful for gaining that understanding. It's very helpful to clear up confusion in my head to start prototyping by writing down type definitions and functions' type signatures without implementations before starting to write the actual code. Of course, this only works with a language where the type system is sufficiently painless and allows for such experimentation without a ton of boilerplate (i.e. the language is not Java).
When I was using c++, the pain came when I discovered that I had to refactor my types because of some cases I didnt know about... Maybe that's easier with more modern statically typed languages.
Personally, I have a bad working memory and struggle to remember the structure of data. I constantly have to jump around and try to find the name of things / recall their hierarchy
With static typing I can just highlight a type / jump to the definition or get intellisense. I also find it useful when I have specific names for data types because I can reason about things / communicate them easier
To me taking the time to specify types or wrangle the type checker is worth the reduced mental burden of having to remember the shape of things, and refactor data structures confidently
That's basically my reasoning as well. I want the IDE to tell me things so I don't have to look up documentation all the time, especially since most of the time I'm working on code I had no part in originally. It's a helpful learning tool more than anything else.
Dynamically typed languages are perhaps not as easy to program in as thought. You need a lot of practice in naming things to produce readable code. For example, in Java it doesn't matter if the count of items is stored in a variable named 'n' or 'f'. The type would still be int. In Python, 'f' would be a confusing name, but 'n' would be acceptable.
Same problem with all dynamically-typed languages, really. It's too easy to write the wrong thing and not find out about it until you've written umpteen unit tests. This can happen due to typos, refactoring, or whatever else. Only JavaScript is worse because it's weakly typed too, so it performs implicit coercions in an effort to make your code always do something (meaning your unit tests have to be very good to really ensure your functions do what you think they do).
A static type system ensures that, at the very least, all the types line up at compile-time. You find type errors much faster than with a dynamically typed language, and this can guide a much less exhausting development cycle.
IMHO Javascript is "unusable" because you can mutate state (among other things). Rich Hickey has a few other arguments against static typing and OO such as names dominating semantics and "types are an anti-pattern for program maintenance and extensibility because they introduce coupling. They’re also parochial." I think he said that in his talk called "Effective Programs":
As much as I liked Clojure when I dabbled for a few months it didn't really fix my dislike for dynamic languages, but I imagine core.typed / spec would fix that for me if I gave it another shot
I loathed dynamic typing mostly because you had to jump around code to understand what the shape of anything was, and it distracted me from the problem I'm trying to solve
In ML influenced languages types actively help me plan a solution, and rarely get in my way
As someone working in dynamic languages for years, I've noticed a certain trend in arguing about type systems.
People will ARGUE about the benefits of compile-time checking.
However, when people point to concrete benefits, they tend to point to TOOLING communication. (e.g. IDEs supplying functions/variables that provide the necessary type) during writing.
Rarely do they notice these two are not the same thing.
I'm not in a place to say which is objectively correct (I'm both biased and without the right kind of data), but I've seen this distinction come up time and time again. I think it's the most interesting part (also the only part that is interesting) about typing disputes.
Tooling in this case replaces a lot of boiler plate unit tests. The stronger and more rigorous the typing the fewer ad hoc tests that need to be hand written. I think the cost is the tooling becomes more finicky about what you feed it.
C weakly typed -> compile -> run-> crash -> WTF?!!!
My point being that when I ask why static is better (from people arguing that), they'll talk about compile-time checks, they won't talk about the tooling.
When I ask them to show me examples, they don't point to bugs that were prevented by compile-time checks, they'll point to how they find it easier to write code because the IDE suggests things.
I think it's hard to point to "bugs prevented by compile-time checks" because of the nature of type-checking.
I could just say that every time the compiler refuses to compile my code due to a type error a bug is averted. Typos, wrong method signatures, interfaces not implemented correctly. All those things had the potential to be bugs. Small, detectable and perhaps even non-breaking, but it's still good to catch them early on.
And of course, most of the time a good enough test suite would catch them. But having the compiler warning you is so much faster! And much more reliable too, because I'm not perfect and sometimes I make mistakes in my tests.
> When I ask them to show me examples, they don't point to bugs that were prevented by compile-time checks
Similarly, if you ask a writer who uses a pencil about the benefits of their approach versus a pen, they likely won't reference any specific times when they would have made un-erasable mistakes.
Pharo Smalltalk has a feature where you can find methods by giving an example of the input and output you expect. Forget how to uppercase a string? Just write:
'eureka' . 'EUREKA'
and the IDE will suggest the method asUppercase.
It does this by dynamically invoking all of string's methods until it finds one whose output matches!
Neat, but not generalizable because methods often have side effects. You could easily know when this search is safe in a language that types side-effects, like Haskell. So types can enhance this feature too.
On a fully dynamic type system, static analysis can't know what functions accept strings. It would have to try every single function, what is not viable, because many have side effects.
Maybe! But you have to admit that having static types with an AST available provides considerably more program synthesis and intellisense possibilities than only having an AST.
The dynamically typed case is not "only an AST," it's a living and breathing program. It made this HTTP request and performed some computation and read that file, and you can talk to it and learn all about its state!
Don't confuse dynamic typing with metaprogramming or fexpr. These are all orthogonal. Tooling for dynamically typed languages as a whole only have access to the AST, although some with the above features may have access to more.
I think the dynamic tooling are things like macros. It seems that JavaScript is being used as the dynamically typed foil, where a better example would be Scheme.
I rarely use IDEs, but compile-time checks are super useful for me during refactoring.
Sometimes I need to rename a class, a method name or a method signature that is used all over the program. I could use tests, but the compiler is much faster and much more comprehensive. So I change everything in one go and then only run the tests after that.
The speed in itself is great, and helps me get much faster feedback than I can get with tests. It's also great during explorational programming, or during the prototype phase: some errors can be detected during compilation, so I'm not greeted with an easily avoidable runtime error.
As for an example of a bug that would have been prevented by compile-time checking, I had one happen just last week. It was in a project with 100% test coverage, but we had undetected refactoring errors due to a lack of integration testing: there were (good) unit tests with mocks, but we missed them during refactoring, triggering a runtime error in production. We fixed that by writing a few simple integration tests and fixed the code. This happened just last week. No biggie, but would be avoided by a compiler.
Your first example has _nothing_ to do with static or dynamic typing. There are plenty of dynamic languages that will fail to compile or bootup with a helpful error message on a failed rename.
This is what's frustrating about trying to have this discussion (and it's covered in the article): "The problem, in this case, is that most programmers have limited experience, and haven’t tried a lot of languages."
If you've used JavaScript and you assign all of the challenges of writing JavaScript to dynamic typing, then ya, dynamic typing sucks. But the problem isn't dynamic typing, the problem is JavaScript.
In the first example I was merely answering the grandparent. They mentions that people talk about the advantages of compile-time checking but fail to give examples. I'm merely giving an example that other people failed to give them.
> This is what's frustrating about trying to have this discussion (and it's covered in the article): "The problem, in this case, is that most programmers have limited experience, and haven’t tried a lot of languages."
Friend, this is completely uncalled for. A single HN answer doesn't say anything about my experience with multiple languages.
An intermediate goal when using static types is that the checked-in code should have type annotations that are guaranteed correct, so you can rely on them when reasoning about the code, either manually (just by reading it) or automatically (using tools).
Traditionally this is done in a compiler, but it could also be done in another tool like some kind of linter, provided that it's always run before committing a patch and/or as part of a continuous build.
There is an argument that a static type system is a set of built in unit tests, that you can activate by adding a command word in a particular position.
And that the test harness on code in a dynamic languages is a domain specific compiler.
The problems come when you want to turn off specific type tests for a particular bit of code because it is deliberately bending the rules (I see this a lot in Go), or the domain specific compiler you've built isn't comprehensive in its coverage not just of lines of code, but the structural complexity of the code.
I recently implemented a simple object system [0] using only closures and macros in a Lisp-dialect [1] I'm working on.
One thing that struck me is how little it bothers me that objects look like functions from the outside, and that any callable value could be used in their place.
It helps that the whole thing is quarantined in a separate library and that objects live in a separate namespace. And that it rests on top of a strongly typed environment.
Judging a type system outside of its context based on some arbitrary check list is pretty useless from my experience.
> I give the following general definitions for strong and weak typing, at least when used as absolutes:
> Strong typing: A type system that I like and feel comfortable with
> Weak typing: A type system that worries me, or makes me feel uncomfortable
What is he talking about? Is he trying to make a point? I always understood the difference to be mostly about implicit type conversions. I think weak-typing is also likeable.
I think he's trying to say that "strong-typing", in practice, as used by some speakers implies a static type system. And when used by other speakers does not imply a static type system.
For some languages it's more about providing features for the IDE and less about providing features for the language itself. It's probably the main reason Microsoft started working on TypeScript for example.
> If pressed for an answer, though, yes I still do believe in the central conclusions of the article. Namely:
> That “static typing” and “dynamic typing” are two concepts that are fundamentally unrelated to each other, and just happen to share a word.
> That “static types” are, at their core a tool for writing and maintaining computer-checked proofs about code
> That “dynamic types” are, at their core, there to make unit testing less tedious, and are a tool for finding bugs.
> That the two are related in the way I outline: that one establishes lower bounds on correctness of code, while the other establishes upper bounds, and that questions about their real world use should come down to the possibility and effectiveness of addressing certain kinds of bugs by either computer-checked proof, or testing.
EDIT: When I wrote this comment the post linked to Steve Klabnik's 'reprint' of the temporary lost original article. Currently it links to the original. The 'reprint' missed the prologue.