Dynamic Languages Are Static Languages (2011)
existentialtype.wordpress.com
existentialtype.wordpress.com
Or, more accurately, they mean different things to different people.
There's unfortunately very little in the way of standardized vocabulary for discussing this topic in a meaningful way. My theory is that this reflects the religious nature of the discussion.
In C++, if I declare the type of something incorrectly, the compiler yells at me. That's restrictive. I have to get it "right".
Python doesn't ask me, because it doesn't care, so I can't be wrong. That's freeing.
But if I were to take a staunch SPJian-McBridian position, I would say this attitude arises from really useless type systems.
A good type system is EITHER capable of letting you say as little as necessary, and only telling you when something is messed up (like Haskell or ML, which have type inference and therefore don't require you to write types) OR is capable of letting you say what you mean, and then doing a lot of work for you.
This is the wonderful benefit of dependent types: you can say enough about your intention that you stop fighting the type checker, and actually get the type checker to do work for you.
This is really something that hasn't percolated into the mainstream of programming yet, but is slowing creeping into Haskell with typed holes, but in dependently typed languages like Agda or Epigram, types can be expressive enough that your editor can write half the program for you just because the type you wrote has only one program as its inhabitant.
McBride likes to describe it as, Dependent Types warp code space so that the direction we want to travel is "down", and all we have to do is let ourselves fall.
As an example, here's a demonstration of some simple (but exemplary) interaction with Agda. If we define this type:
data Vec (A : Set) : Nat -> Set where
Nil : Vec A 0
Cons : forall {n} -> A -> Vec A n -> Vec A (1 + n)
where `Nat` is the type of natural numbers and `Vec` is the type of lists indexed by their length, then we might be inclined to write this function: append : forall {A m n} -> Vec A m -> Vec A n -> Vec A (m + n)
append xs ys = {! !}0
where the `{! !}0` is a portion of the program that hasn't yet been defined, called a hole. We can put the cursor in the hole (hole 0) and ask the editor to do a case split on `xs` with `Control-c Control-c x`, and the program will change to be append : forall {A m n} -> Vec A m -> Vec A n -> Vec A (m + n)
append Nil ys = {! !}0
append (Cons x xs) ys = {! !}1
if we put the cursor in hole 0, we can hit `Control-c Control-a` (a for agsy, or auto) and itll fill in the hole with the solution `ys`, and if we do the same in hole 1, itll fill in `Cons x (append xs ys)`.The type of the function we're defining is enough that agda can actually find the definition with almost no help from us.
Not knuckle wrapping here :)
This doesn't make much sense if you want to avoid errors.
The "lot of abstractions" is the distinction between type and class the article addresses, I guess?
I'd rather do the gymnastic and have the compiler tell me where I screw up right away. That's a tighter, more accurate feedback loop than a REPL. (Source: trying to implement a simple depth first search in both Lua and Ocaml.)
There's really two problems hidden in that that deserve to be considered:
(1) Static languages with insufficiently-expressive type systems may require more complex code (in terms of actual operations, not just type declarations) for the code to be valid when compared to dynamic languages (or static languages with sufficiently-expressive type systems.), and
(2) static languages may, depending on the situation and the completeness of their type inference system, require arcane incantations to the type system before it accepts that correct code is, in fact, correctly typed.
Some statically-typed languages are good on one or both of these measures, and so make less of one or both types of problems (Haskell, IMO, is pretty good on both, as static languages go, but lots of more popular static languages are really bad at one or both.)
For example I don't see much about "classes" in MDN:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
https://developer.mozilla.org/en-US/docs/Web/API/Window/loca...
Seems like this documentation talks about little else than types to me?
This seems... factually incorrect? Shouldn't it say "a double"? (or a "number", which just means "double" in JS)
It might ignore any characters after a ".", but it's impossible to return an integer in JS...
radix: An integer between [...]
The Window.location read-only property returns a Location object
You gave supporting evidence for my point.
$foo = "hello, world!";
$foo = 12;
When you use a string-related function in Python, you expect the parameters to be strings, yet there's no type-safety enforced until runtime. let foo = "hello, world"
in let foo = 12
in foo
Whether bindings can be shadowed has nothing to do with whether typing is static or dynamic.C++ has no sum types (afaik, the language is so huge I might very well be wrong)
It is much easier to quickly prototype or experiment in a dynamic language. Note that I am thinking versus a language like Scala or Haskell where types are a integral part of the language. This applies less to languages like Java. You don't have to stop and figure out your types for every function and then go back and change them every time you figure out there is a better way to represent the data.
That being said, I get their 'Dynamic Languages really just have one type' argument. I understand what they are saying. Doesn't mean we can't still use the term dynamic to describe such languages. Words mean whatever we(as a group) want them to mean.
I disagree. I can say what I mean with types, and if I didn't mean what I said, refactoring is a breeze.
"You don't have to stop and figure out your types for every function and then go back and change them every time you figure out there is a better way to represent the data."
You don't have to do this in languages like Haskell either. Type inference will help you most of the time when figuring out a function's type, and you don't have to use the type system as it was intended at first. You can have a prototype with Strings and Lists, the compiler won't be able to help you much though.
All of these languages offer tools to handle similar duck typing via interfaces or traits or polymorphism, but they are far from simple to use.
When Haskel gains a mindshare outside of its current niche, or the type inference becomes more broadly implemented in languages used across our industry, then let's talk about how it makes statically typed languages better and easier to use than "unityped" languages.
This is one of those things that is perhaps true in theory, but often falls short in practice. Static type systems tend to require (more or less frequent) additional incantations, but may not be sufficiently-express, even with those incantations, to express the desired intent.
(Or, the required complexity of the incantations may be a greater cognitive workload than actually writing the functional code.)
> You don't have to do this in languages like Haskell either.
Most static languages -- particularly, the ones with the strongest standard libraries and ecosystems that are likely to support whatever it is you are doing -- aren't like Haskell.
Hmm, if you change your data representation from an array to a dict, say, don't you anyway need to go back and change code in every function that accesses this data?
Compared with your average statically typed language, you do have to update the function signature and everything that calls it.
Now then, admittedly, Haskel is not your average statically typed language - but then again it's very rare to actually run up against Haskel in production code.
I don't think you can iterate thought the values in an array and in a dict in e.g. python with the exactly same syntax.
> just passing it along
In languages with global type inference (e.g. OCaml) you would not have written the type in the code in the first place, so changing the type of an argument that you just pass through, would not require any changes in the code.
But yes, in Haskell people are in the habit of writing the type signatures, even though the compiler does not require them, so in this case edits are needed.
That depends on what you mean by "values". The default iterator over dictionaries in python iterates over the keys, so if you use the exact same syntax as iterating over members of a list you iterate over the keys of a dictionary, not what are usually called the values (there is a separate iterate for that, and for key/value pairs). But, yes, dicts and lists are iterable via the same syntax since they both support Pythons iteration protocol.
message JsonArray {
repeated JsonValue value = 1;
}
message JsonObject {
map<string, JsonValue> properties = 1;
}
message JsonValue {
oneof value {
JsonObject object_value = 1;
JsonArray array_value = 2;
bool is_null = 3;
bool boolean_value = 4;
string string_value = 5;
// Represented as a string because JSON doesn't restrict the
// range/precision of numbers.
string number_value = 6;
}
}Dynamically typed languages are, in my opinion:
* Simple
* Typically require less design upfront
* Easier to get started with
There's real value in that. Perhaps not for the author. It's easy to scoff at languages like ruby, python or javascript and claim they're completely inferior to 'real languages', but there are completely valid reasons to use them. The real value comes from simplicity.
If you use a language where runtime/dynamic typing is a subset of what's possible, you lose that simplicity.
Having used mostly dynamic languages throughout my career, personally, a huge number of the errors I've encountered have boiled down to getting the wrong type of thing — an unexpected null, a Business where I expected a Person, an array where I expected a string, an escaped HTML string where I expected an unescaped one, etc. Sometimes they quack kind of like a duck, but still do it differently enough to trigger faulty behavior (e.g. people and businesses both have names and addresses, but the latter can be in multiple places at the same time).
It usually works right, but in the cases where you're passing sensible types, you'll probably be OK in a statically typed language too — not always, but usually. Static type systems introduce some edge cases where you need to think about the type system even though you shouldn't have to, and dynamic type systems introduce other edge cases where your program's behavior can be unexpectedly brittle. They're both kinds of unnecessary complexity.
I disagree with this (possibly depending on your particular value of "require"). The assistance that static typing can give in refactoring means I don't have to be nearly as confident in my initial design. Without the types, it's a lot more important that I get everything right initially. I find this to be true even when prototyping.
More to the point, things that are formally equal to each other do not necessarily have equal value in certain uses. (For example, think about Stokes' Theorem. It says that an integral of a function on the boundary of a manifold is equal to the integral of the derivative of the function on the whole manifold. That's useful precisely because sometimes the integrals, while they have equal results, are not equally hard to do.)
A dynamically typed language may be formally equivalent to a static unityping, interpreting the tag at runtime. That doesn't mean that they're equally easy to use, though. When I want to do that kind of thing, I want to do it like it's dynamic, not where I have to do the book-keeping to make it dynamic.
The arguments for flow seem to resonate nicely with the points raised in the article.
foo = bar()
if foo:
fooo = baz()
in a static language, you'd have to write int foo = and the third line where you have an assignment fooo would be caught by the compiler. Of course there's advantages to dynamic languages, like being able to write something like mydata = {}
mydata['foo'] = {}
mydata['foo']['bar'] = MyObject()
without a lot of boilerplatethe only plugin i have is ES6/Babel syntax highlighting.
it mostly "just works"
It won't catch this.fooo, though, so it's only a partial solution. My editor has great completion, though, so as long as the string is IN the file somewhere, I usually autocomplete it. Way fewer typos that way.
I see there's a PyLint, so MAYBE it has a way of spotting typos like the one you cite? Not sure how it would work in Python, though, other than on variable reads.
The second case is easily handled by static languages, for example, in Swift:
var mydata = ["foo": Dictionary<String, MyObject>()]
mydata["foo"]["bar"] = MyObject()
This is only longer than it needs to be because of using two lines, you wouldn't need the explicit declaration to do it in one: let mydata = ["foo": ["bar": MyObject()]]
This also gets rid of mutable state!As for whether "fooo" is a valid symbol, whether you can declare a variable without a keyword is unrelated to the type system. See, e.g., Go, which has different operators for declaration-with-assignment and reassignment (:= vs. =), but this is merely a syntactical safety net, not an outright necessity.