27 ms·
From the Twitter link: > IME, people in dynamic languages almost never program this way, though—they prefer to use validation and some form of shotgun parsing.
by seanwilson 5y ago
From the Twitter link:
> IME, people in dynamic languages almost never program this way, though—they prefer to use validation and some form of shotgun parsing. My guess as to why? Writing that kind of code in dynamically-typed languages is often a lot more boilerplate than it is in statically-typed ones!
I feel that once you've got experience working in (usually functional) programming languages with strong static type checking, flakey dynamic code that relies on runtime checks and just being careful to avoid runtime errors makes your skin crawl, and you'll intuitively gravitate towards designs that takes advantage of strong static type checks.
When all you know is dynamic languages, the design guidance you get from strong static type checking is lost so there's more bad design paths you can go down. Patching up flakey code with ad-hoc runtime checks and debugging runtime errors becomes the norm because you just don't know any better and the type system isn't going to teach you.
More general advice would be "prefer strong static type checking over runtime checks" as it makes a lot of design and robustness problems go away.
Even if you can't use e.g. Haskell or OCaml in your daily work, a few weeks or just of few days of trying to learn them will open your eyes and make you a better coder elsewhere. Map/filter/reduce, immutable data structures, non-nullable types etc. have been in other languages for over 30 years before these ideas became more mainstream best practices for example (I'm still waiting for pattern matching + algebraic data types).
It's weird how long it's taking for people to rediscover why strong static types were a good idea.
- benrbray 5y agoYeah, I remember I used to get frustrated when I had to read code that used map() or even .forEach() extensively, thinking a simple, imperative for loop would suffice. I slowly came to realize that a for loop gives you too much power. It's a hammer. It holds the place of a bug you just haven't written yet. Now I'm the one writing JavaScript like it's Haskell. Although Haskell could learn a thing or two from TypeScript about row polymorphism.
- k__ 5y agoIs using "row polymorphism" the same as using a "structutal type system"? I never heard about the former.
- inbx0 5y agoNot really, and correct me if I'm wrong but afaik TS doesn't actually do row polymorphism so much as structural subtyping - although the difference between the two is pretty small and you can get pretty close to row polymorphism with structural subtyping + generics. But even if these were the same thing and we want to be a bit pendantic since this is HN after all, structural type systems often support some kind of subtyping or row polymorphism, but it's not a strict requirement for a type system to be "structural". You could have a structural type system that doesn't allow { a: int, b: int } to be used where { a: int } is expected. How practical such a type system would be... I don't know. Flow type checker for JavaScript makes a distinction between "exact" types, i.e. object must have exactly the properties listed and no more, and "inexact" types where such subtyping is allowed.
- seanwilson 5y ago> How practical such a type system would be... I don't know. Flow type checker for JavaScript makes a distinction between "exact" types, i.e. object must have exactly the properties listed and no more, and "inexact" types where such subtyping is allowed. TypeScript doesn't have this check (https://github.com/Microsoft/TypeScript/issues/12936 https://github.com/Microsoft/TypeScript/issues/12936) and I've found it can be really error prone when you're wanting to return a copy of an object with some fields updated. Spot the bug in this example: https://www.typescriptlang.org/play?#code/C4TwDgpgBAKlC8UDeUCWATAXFAzsATqgHYDmANFAMYD2ANrRJcKtUdnoaQNoC6AvgG4AsAChRAMwCuRJiyJQAhunQAKYJhgUAbgtqSI7AsRIBKKBuSio1qqzxRJYdAuDRESGvUbNWAJmxcAHTBwIGeDLKs2rr6-MIiNlD4EMCS+PJIwYHAFFmOzq6ConyiojREOHQQgbTUJCpKqkgYmABEAB6tFOHecphcrQpdUK0ARq38FK2UrSYm8UA https://www.typescriptlang.org/play?#code/C4TwDgpgBAKlC8UDeU...
- mixedCase 5y agoFWIW, you can achieve row polymorphism in TypeScript, although it's not super intuitive. function rowPolymorphic<R extends { a: number }>(record: R): R & { a: string } { return { ...record, a: record.a.toString(), } } const rec = rowPolymorphic({ a: 123, b: "string", }) console.log(rec.b)
- touisteur 5y agoOn the other end I'm endlessly tired of 'too simple' foreach/map iterators. They're OK until you want to do something like different execution on first and/or last element... Give me a way to implement a 'join' pattern over the foreach iterators, or less terse iterators (with 'some' positional information). I think I'm just ranting about the for-of iterator in Ada...
- Jtsummers 5y agoAda provides a kind of iterator called a Cursor which could be used to build up a package of functions similar to the various C++ standard library algorithms. I believe this has actually already been done. Cursors can also be converted back to positional information if it makes sense (like with a Vector).
- touisteur 5y agoI like the cursor, because it abstracts indexes. Problem is, if I want the element, I still have to go fetch it (calling Element on the Cursor) and then I have to save it in a local variable and then I need to add a declare block for the constant standing for the result of Element and then the thing makes 5 lines instead of two and is not much more readable. Maybe I can just use Element() (especially since AdaCore seems to want to generalize dotted notation) repeatedly and have it inlined, but my past experience says 'expensive'... And sadly Cursors haven't been generalized to all associative iterable structures in Ada (arrays for example).
- DougBTX 5y agoI quite like the “enumerate” pattern. When indexes matter, instead of `for x in v` you would write, `for (i, x) in enumerate(v)`, then the language only needs one type of for loop as both cases use the same enumerator interface.
- touisteur 5y agoYes I was thinking of something like that. Only I wish I could also know whether I is first and/or last without calling into the iterated structure. I know this looks like corner case syntactic sugar but it comes up a lot, e.g. when serializing to JSON. I guess I should write my own iterators but I want them everywhere...
- steveklabnik 5y ago> It's weird how long it's taking for people to rediscover why strong static types were a good idea. I don't think it's weird. Most of those languages were not popular in industry for various reasons, and the ones that were (especially in say, the 90s) did not have particularly capable static type systems. The boilerplate/benefit ratio was all off. The way I describe this dichotomy personally is, I would rather use Ruby than Java 1.5. I would rather use Rust than Ruby. (Java 1.5 is the last version of Java I have significant development experience in, and they've made the type system much more capable since those days.)
- ukj 5y agoEvery programming paradigm is a good idea if the respective trade-offs are acceptable to you. For example, one good reason why strong static types are a bad idea... they prevent you from implementing dynamic dispatch. Routers. You can't have routers.
- kortex 5y agoSure you can. You just need the right amount of indirection and abstraction. I think almost every language has some escape hatch which lets you implement dynamic dispatch.
- ukj 5y agoThis is a trivial and obvious implication of Turing completeness. Why do you even bother making the point? With the right amount of indirection/abstraction you can implement everything in Assembly. But you don't. Because you like all the heavy lifting the language does for you. First Class citizens is what we are actually interested in when we talk about programming language paradigm-choices. https://en.wikipedia.org/wiki/First-class_citizen https://en.wikipedia.org/wiki/First-class_citizen
- kortex 5y agoI mean, GP said "you can't have routers" and maybe I'm being dense by interpreting that as "never or almost never," but even with a generous "too hard to be practical," I still don't think it's correct. And I explicitly said "escape hatch" meaning language feature. You don't need that much indirection to get routers in Haskell, Rust, Go, C, C++... like I fail to see how implementing routers are a barrier in strict type system languages. Is it easier in python or js? Sure. can't? hardly. E: here's some vtable dispatch (unless that doesn't count as "dynamic dispatch") in Rust. Looks really straightforward. https://doc.rust-lang.org/1.8.0/book/trait-objects.html https://doc.rust-lang.org/1.8.0/book/trait-objects.html
- ukj 5y agoWhat C++, Haskell, Rust, Go etc. call "dynamic dispatch" is what a router calls "static routing". The defining characteristic is that the lookup table is pre-determined and immutable at runtime. What routers call "dynamic routing" is having the lookup table mutable at runtime. There can be no equivalent to that in type-safe languages because when you mutate the dispatch table you lose type-safety.
- chriswarbo 5y agoI have the same feeling after spending a few years with Haskell, StandardML, Agda, Idris, Coq, etc. One trick I've found very useful is to realise that Maybe (AKA Option) can be though of as "a list with at most one element". Dynamic languages usually have some notion of list/array, which we can use as if it were a Maybe/Option type; e.g. we can follow a 'parse, don't validate' approach by wrapping a "parsed" result in a list, and returning an empty list otherwise. This allows us to use their existing 'map', 'filter', etc. too ;) (This is explored in more detail, including links to logic programming, in https://link.springer.com/chapter/10.1007%2F3-540-15975-4_33 https://link.springer.com/chapter/10.1007%2F3-540-15975-4_33 ) If we want to keep track of useful error messages, I've found Scala's "Try" type to be useful ('Try[T]' is isomorphic to 'Either Throwable T'). Annoyingly, built-in sum type; the closest thing is usually a tagged pair like '[true, myFoo]'/'[false, myException]', which is pretty naff.
- np_tedious 5y ago> a list with at most one element I've found scala or even LINQ to really hammer down this point, even to those who aren't into FP very much. Doing that map/flatmap makes it click for just about anyone
- specialist 5y agoYa, having 'foreach( null )' be a no-op is my #1 language feature request. Not being a big fan of method chaining, a null saavy foreach would probably eliminate most of my null checks, need for Optional.
- jhgb 5y ago> > IME, people in dynamic languages almost never program this way, though—they prefer to use validation I wonder how many people the author met.
- RHSeeger 5y agoPlenty of people know multiple statically typed and dynamic languages, and multiple functional, imperative, and other languages; and use dynamic languages for some things but not other things. The set of people using dynamic languages isn't just "those that haven't had their eyes opened yet to what static languages can do". Different languages and paradigms make different things easier. I do believe that, for long lasting, larger projects, static typing tends to make the code easier to maintain as time goes on. But not every project is like that. In fact, not every project uses a single language. Some use statically typed languages for some parts, and dynamically typed for others (this is common in web dev).
- seanwilson 5y ago> The set of people using dynamic languages isn't just "those that haven't had their eyes opened yet to what static languages can do". This was more aimed at people who are new to the idea of parsing over validating. In a strong statically typed language, the type system would naturally guide you to use this approach so if this isn't natural to you then time in other languages would probably be worthwhile.
- RHSeeger 5y agoMy misunderstanding then. Your comment came across (to me) as saying that people using dynamic language just don't know any better; that, if they would just learn a static language, they would suddenly understand the error of their ways. That's the thought that I was responding to.
- wpietri 5y agoFor sure. I do most of my work in situations of high volatility of domain and requirements and relatively high risk. (E.g., startups, projects in new areas.) Static typing really appeals to me on a personal level. I enjoy the process of analysis it requires. I love the notion of eliminating whole classes of bugs. It feels way more tidy. I took Odersky's Scala class for fun and loved it. But in practice, they're just a bad match for projects where the defining characteristic is unstable ground. They force artificial clarity when the reality is murky. And they impose costs that pay off in the long run, which only matters if the project has a long run. If I'm building something where we don't know where we're going, I'll reach for something like Python or Ruby to start. This has been brought home to me by doing interviews recently. I have a sample problem that we pair on for an hour or so; there are 4 user stories. It involves a back end and a web front end. People can use any tools they want. My goal isn't to get particular things done; it's to see them at their best. After doing a couple dozen, I'm seeing a pattern: developers using static tooling (e.g., Java, TypeScript) get circa half as much done as people using dynamic tooling (Python, plain JS). In the time when people in static contexts are still defining interfaces and types, people using dynamic tools are putting useful things on the page. Making a change in the static code often requires multiple tweaks in situations where it's one change in the dynamic code. It makes the extra costs of static tooling really obvious. That doesn't harm the static-language interviewees, I should underline. The goal is to see how they work. But it was interesting to see that it wasn't just me feeling the extra costs. And those costs are only worth paying when they create payoffs down the road.
- archsurface 5y ago"It's weird how long it's taking for people to rediscover why strong static types were a good idea." It sounds like you're projecting your limited experience eg you've dismissed all Ms dev. Many people have been using strong static for decades. The benefits have never been out of sight. Many use dynamic, but many have always used strong static.
- lukashrb 5y agoFor what its worth: People don't use dynamic language because they don't know better or never used a static language. To better understand what dynamic languages bring to the table, here are some disadvantages of static types to consider: Static types are awesome for local reasoning, but they are not that helpful in the context of the larger system (this already starts at the database, see idempotency mismatch). Code with static types is sometimes larger and more complex than the problem its trying to solve They tightly couple data to a type system, which (can) introduce incidental complexity >(I'm still waiting for pattern matching + algebraic data types) This is a good example, if you pattern match to a specific structure (e.g. position of fields in your algebraic data type), you tightly coupled your program to this particular structure. If the structure change, you may have to change all the code which pattern matches this structure.
- tikhonj 5y agoThe example with pattern matching doesn't have anything to do with static types. You'll have exactly the same problem if you pattern match against positional arguments in Python: match event.get(): case Click((x, y)): handle_click_at(x, y) (Example from PEP 636[1].) In both Python and statically typed languages you can avoid this by matching against field names rather than positions, or using some other interface to access data. This is an important design aspect to consider when writing code, but does not have anything to do with dynamic programming. The only difference static typing makes is that when you do change the type in a way that breaks existing patterns, you can know statically rather than needing failing tests or runtime errors. The same is true for the rest of the things you've mentioned: none are specific to static typing! My experience with a lot of Haskell, Python, JavaScript and other languages is that Haskell code for the same task tends to be shorter and simpler, albeit by relying on a set of higher-level abstractions you have to learn. I don't think much of that would change for a hypothetical dynamically typed variant of Haskell either! [1]: https://www.python.org/dev/peps/pep-0636/#matching-sequences https://www.python.org/dev/peps/pep-0636/#matching-sequences
- lukashrb 5y agoYou're absolutely right. I guess I mentioned pattern matching in particular because of the cited sentence from OP "I'm still waiting for pattern matching + algebraic data types". > The same is true for the rest of the things you've mentioned: none are specific to static typing! Sure, I could be wrong here. I frequently am. But could you point out why do you think that?
- yashap 5y agoThis matches my personal experience, for sure. I started out writing Python (pre-type-annotations) and JS, with lots of “raw” dicts/objects. For the past ~7 years though, I’ve written mostly Scala, but also a decent amount of TypeScript, Go and Java, and it’s completely transformed how I code, dramatically for the better. Now, even in the rare case where I write some Python, JS or PHP, I write it in a very statically typed style, immediately parsing input into well-thought-out domain classes. And for backend services, I almost always go with 3 layers of models: 1) Data Transfer Objects. Map directly to the wire format, e.g. JSON or Protobuf. Generally auto-generated from API specs, e.g. using Open API Generator or protoc. A good API spec + code gen handles most input validation well 2) Domain Objects. Hand written, purely internal to the backend service, faithfully represent the domain. The domain layer of my code works exclusively with these. Sometimes there’s a little more validation when transforming a DTO into a domain model 3) Data Access Objects. Basically a representation of DB tables. Generally auto-generated from DB schemas, e.g. using libs like Prisma for TS or SQLBoiler for Go Can’t imagine going back to the “everything is a dictionary” style for any decent sized project, it becomes such a mess so quickly. This style is a little more work up front, when you first write the code, but WAYYYYYY easier to maintain over time, fewer bugs and easier to modify quickly and confidently, with no nasty coupling of your domain models to either DB or wire format concerns. And code gen for the DTO and DAO layers makes it barely more up-front work.
- dragonwriter 5y ago> It's weird how long it's taking for people to rediscover why strong static types were a good idea. Its weird how long its taken for languages with static typing and type systems designed for correctness (and designed well for that end) rather than princupally for convenience of compilation to be available that are generally usable (considering licensing model, features, ecosystem, etc.)