←back to thread

Parse, Don't Validate (2019)

(lexi-lambda.github.io)
389 points melse | 10 comments | | HN request time: 1.223s | source | bottom
Show context
seanwilson ◴[] No.27640953[source]
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.

replies(10): >>27641187 #>>27641516 #>>27641651 #>>27641837 #>>27641858 #>>27641960 #>>27642032 #>>27643060 #>>27644651 #>>27657615 #
1. benrbray ◴[] No.27641187[source]
Yeah, 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.
replies(2): >>27641257 #>>27642274 #
2. k__ ◴[] No.27641257[source]
Is using "row polymorphism" the same as using a "structutal type system"?

I never heard about the former.

replies(1): >>27641425 #
3. inbx0 ◴[] No.27641425[source]
Not 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.
replies(2): >>27641551 #>>27641893 #
4. seanwilson ◴[] No.27641551{3}[source]
> 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) 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/C4TwDgpgBAKlC8UDeU...

5. mixedCase ◴[] No.27641893{3}[source]
FWIW, 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)
6. touisteur ◴[] No.27642274[source]
On 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...
replies(2): >>27642971 #>>27644280 #
7. Jtsummers ◴[] No.27642971[source]
Ada 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).
replies(1): >>27647234 #
8. DougBTX ◴[] No.27644280[source]
I 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.
replies(1): >>27647194 #
9. touisteur ◴[] No.27647194{3}[source]
Yes 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...
10. touisteur ◴[] No.27647234{3}[source]
I 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).