4 ms·
Good 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
by bitwalker 9y ago
Good 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).
- wellpast 9y ago> For Rich's argument to resonate with me, I think I just need a good example of what he's talking about Let me go through some work I just went through. Scrape web blog post, generate some semantic data about the post, send that data through some processing which perhaps infers some new facts, maybe discards some posts, then sends the data over the wire to be mapped and written to a searchable data repo. Which of course will be queried, read, and displayed in a web app. This is pretty standard-fare programming in industry/web world. I wrote this in Java, but here's my take: the easiest way to both implement and maintain this code is to model the web page data as a simple Map of facts -- properties with clear semantics ("author", "title", "content", etc.) and their values. The very minute that my scraper needs to introduce a new fact it does literally nothing from a code declaration point of view. It simply puts a new fact (say, "publication-date") into the Map. Absolutely no downstream code needs to be touched in order for me to do this. I just produce the fact/knowledge. That was just one use case. Now to do this same thing with an ADT/class/record, I have to at minimum go declare that property in the ADT. But usually I also have to go update the ADT serialization code to make sure that it is writing this new property into the serial format -- the JSON, or what-have-you. Furthermore, on the other side of the wire, I have to make sure that guy is either using my ADT or map to his ADT, whatever. Oh my gosh, this is just one use case and the non-ADT scenario asked me to do literally nothing to introduce some knowledge, some simple little fact, and the ADT scenario has me jumping through hoops to get my release out. Mind you, this data-process-to-web-display architecture is very common in industry programming. And it is very common to find this jumping-through-hoops. And we haven't even started talking about the webapp/display side of things. Now I wrote this in Java, the whole way dodging ADTs and types where I didn't need them (ie, lean on Strings when in doubt), and using simple Maps. But even then the syntactic negotiation was quite large. Clojure embraces this map-centered/fact-centered view of the world at its core and this program would have been much much less lines of code and much much less cognitive overhead written in Clojure. Now I know a static typer person is going to say, okay fine, but jumping through those hoops I've saved you a bunch of bugs. But I say no you haven't. Unless you go all the way with your types and have a Content type and a Title type ... because my bugs were to do with payload sizes and things like that. Things that even static typers will end up checking via some dynamic logic in their code.