https://stackoverflow.com/a/28096079
> 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.
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.
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.
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.
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
(https://github.com/thautwarm/MLStyle.jl if you want the syntax) but that's somewhat beside the point.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.
[1] I should credit the Northeastern PL group for the term "tag-based semantic subtyping" here to describe this notion (https://github.com/julbinb/ftfjp-2019/blob/master/paper/mini...).
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.
auto var = 123;
is inference, but std::any var2 = 123;
is dynamic typing, because you can do `var2 = "foo bar";` unlike the first case.https://docs.racket-lang.org/ts-guide/
Your objection doesn't really hold water..
To me, the purpose of those proofs, is to tell the compiler about all the programs I'm not writing. So that it doesn't emit them.
Also: documentation.
> 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.
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.
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.
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.
> 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.
Totally fair!
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.
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.
Right, and autoconversion according to spec is something the type system does want to do. It uses type information to do it, in fact.
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.
... or plain doesn't care?
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.
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".
> 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.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): Complete<Version<Of<Generic, Foo>>>> f(int x) {
Complete<Version<Of<Generic, Foo>>>> foo = (Complete<Version<Of<Generic, Foo>>>>) something(x);
return foo;
}I suppose it helps that the types aren't strictly required too, so you can side-step them if necessary.
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.
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.
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.
This means, in Java you can do
Object obj = ...
List<?> lst = (List<?>) obj
Object item = lst.get(3)
Whereas, in C#, to do something similar, you have to dynamically compile at runtime a specialization of a generic delegate.> 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.
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
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.
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.
JS isn't dynamically typed to make testing easier. JS uses dynamic types because you couldn't build the web any other way.
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.
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?
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.
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.
1: https://chromium.googlesource.com/chromium/src.git/+/62.0.31...
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.
FWIW, that sort of field existence check (and much more) can be done fully at compile time in many mainstream programming languages.
> 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.