5 ms·
1: In a static type system with inference, both examples would just be (add [x y] (+ x y)) as the `+` operator is enough to infer that the parameters nee
by bitwalker 9y ago
1:
In a static type system with inference, both examples would just be
(add [x y] (+ x y))
as the `+` operator is enough to infer that the parameters need to be a number. So your point is true for some statically typed languages, but certainly not all (ML-likes, Haskell-likes, and others with strong type inference).
2:
In my experience, ADTs (and pattern matching in particular) make coding problems much easier to solve (and the code itself considerably simpler) than the alternatives in many languages. For example, I'm writing a compiler in Go, for fun, and a lot of tasks would be far easier and more concise to express with ADTs/pattern matching. Another example is working with pattern matching in Elixir/Erlang, especially with binary data, which is beyond easy to express a parser for with pattern matching. Elixir and Erlang are dynamically typed, and have gradual typing via Dialyzer (though it's rather different from clojure.spec I believe), but pattern matching is pattern matching, and it's indispensable in my opinion.
Using something dynamic, like Lisp/Javascript/Ruby/Etc. would make it much more difficult to navigate the code for something like a non-trivial compiler (in my experience), and more difficult to reason about how it will execute. The other thing I don't see mentioned very often is that if the compiler can't reason well about your code, then it can't optimize it very well. Even with a JIT, you are paying a performance cost that is non-trivial, unless you happen to be using a language with a particularly good JIT, performing tasks that the JIT is good at optimizing at runtime. For a lot of tasks, this overhead doesn't matter, but I would rather lose some flexibility to be able to use the same language for a broader array of tasks.
Recently I was taking a stab at translating some code for an Earley parser from some dynamic languages (Javascript and Python) to Go, again for fun, and in both cases found it was impossible to do so because of the way the dynamic features of the language were abused - basically I would have to throw out everything and write it from scratch. The code was hard to reason about, because while reading it, you think you are working with values of a certain type in one function, but the same value somewhere else is getting treated like a different type. You basically have to execute the code to see what it does. Such flexibility can make for more concise code, but when the code _isn't_ concise, it can make it incredibly difficult to untangle. That's just my experience though. I'm an Elixir/Erlang programmer during the day, so I'm not anti dynamic languages by any means, but I have definitely found it easier to build certain types of applications in statically typed languages.
- wellpast 9y agoWell articulated. 1: The inference allows me to express that part of my program without thinking about types. But somewhere in Haskell (or what-have-you) I have to express the types so that the compiler (type-prover) can do its proof-thing. This is required afaik by the non-optional static typed language. In a dynamic type language I can go into production without having to (help my compiler) prove anything. 2: Interestingly Rich Hickey goes to great lengths to make a distinction between writing device drivers, logic systems, etc and the other kinds of (majority) industry programming. He kind of stays hands-off on the former types of systems where he might agree with your points. For your example, in a compiler I might have an ADT represent a node in an AST and this helps facilitate a lot of what you're doing. And you're willing to take on the rigidity of having the ADT there b/c you probably don't suspect it to be a volatile construction, constantly undergoing revision, etc. But in the other kinds of programs the Hickey talks about, your Person ADT/class is only hurting you. You are no longer in the cozy safety of some compiler/logic system. And (almost?) always your Person class is not stable (you'll be processing these Persons in many different contexts over time and the whole conception of those first-name, last-name, age, etc. properties having to clump together in one type is (almost?) always going to change). So in the other world of these "situated" programs (his term) you are creating a lot of work for yourself by introducing that ADT/Person class.
- bitwalker 9y agoGood points! 1: That's true for anything which isn't a primitive type, or built in to the language, sure. Most of the time though, the type you have to define is not exactly complex, take records for example (using a Haskell-ish syntax): data Person = {name: String, age: Int} deriving (Eq, Show) That's something not only easy to define, but something that you'd _want_ to define anyway even in dynamic languages, i.e. via `defstruct` in Elixir, or `class` in Ruby/etc. The language (at least in Haskell, and in the language I'm working on) can help you derive the common interfaces you'd want (structural equality and inspection/stringification in the example), and then that's it, type inference takes over from there. In Haskell/ML, you typically only need type annotations when working with higher-ranked polymorphism (e.g. functions which operate on polymorphic functions), and you can go a long way without needing to write code like that. In other words, the burden at this level is extremely lightweight, and I would argue that it imposes no more burden than you would already undertake in well-written dynamic code (i.e. defining structures/classes). Once you get into a place where you are writing abstract code to work across types, with no type constraints, where higher-rank polymorphism is likely to be front and center, then yes, you will spend some extra time up front annotating those functions with their polymorphic type, and that can be annoying sometimes. Even that's not a given though, in Haskell for example, one can define a typeclass (such as Eq, for equality), and write code which operates on instances of that type without needing to do anything more than: foo :: (Fooish a) => a -> a foo a = # do something Fooish with 'a' That function is polymorphic across any type which is an instance of `Fooish`. I mean, I guess one could consider that an unbearable burden, but to me that's at least as much "extra" as I would do in Elixir with typespecs, and in another language via function docs at a minimum. Writing abstract code like that in a dynamic language probably still requires you to perform some checking of the arguments to ensure that they are actually of a type that is valid for the function. You can go into production writing code without your compiler proving that what you expressed to it is valid, but if something is wrong, you don't have any tooling (short of debugging) to fall back on, you have to figure out what went wrong, try to fix it, deploy again and hope you actually fixed the problem. With (good) static type systems, you may be slower to production that first time, but the compiler has ensured that you haven't made any mistakes based on what you told it you were trying to do. One can of course still make mistakes with a type system helping you, but at that point it's a conceptual mistake, i.e. you've told the compiler to do A, and it did A like you asked, but the right thing was actually B. Nevertheless, I understand the desire for a compiler to just do what I say, regardless of whether the compiler thinks it's ok; but most of the time when I do that, it means the next person working the code behind me isn't going to understand it - if it's not clear to the compiler, it's probably going to be unclear to a reader of the code too. That isn't a hard and fast rule, but I have certainly seen instances of that. In my experience, most of the dynamicism I rely on in such languages is for metaprogramming, all of which I would rather do at compile-time via macros anyway, and thus doesn't require a dynamic type system. For everything else, I'm having trouble thinking of an example where types would interfere, other than writing code which ignores things like checking if a result exists or not, or is a datastructure vs an error - and I would rather have the proper handling of those things done and enforced by the compiler rather than ignore it until something blows up in production. That's my take though, and for those who are desperate to get something in to production ASAP, it's probably not the right fit. 2: I'm not sure that working with arbitrary data leaves you in a situation where you are no longer within the realm of safety the compiler can provide you. You will have code that cares about aspects of that data, imposing some kind of logical structure to it, whether that's encoded in a type or not. The only difference between dynamic and static processing of that kind of data is that the dynamic code has to search for the structure it expects, while the static code defines that up front, and only has to try to match the data to that structure - working with it after that point is not going to differ much between the two. With dynamic code you'll still need to check if a field is present or not, and whether it's of the type you expect it to be. It is certainly easier to create arbitrary heterogeneous structures in a dynamic language (I won't argue that!), I don't buy the argument that a static type system is somehow ill-suited to the processing of such data. I will say that it is annoying that before you can start writing code to process the data in a static type system, you have to define the types/interfaces that the data will take - sometimes I would rather just mold the raw datastructure via traversing it and pattern matching on it - but I think in the end that extra time up front ends up a wash with a dynamic type system because you spend less time on the tail end chasing bugs that the compiler can catch. For Rich's argument to resonate with me, I think I just need a good example of what he's talking about - every time this topic has come up, I haven't been able to come up one, and I haven't seen anybody else do so either. An example where Rich (or whoever) thinks the dynamic solution is clearly superior, and no static solutions come close. At the end of the day, I say use the right tool for the job, and if a dynamic language clearly solves a problem better than the static one, then hey, I'm not complaining (after all, it's why I'm using Elixir/Erlang to solve problems, though I don't think the problems they solve are necessarily at the dynamic/static divide).