13 ms·
Parse, Don't Validate (2019)
- ukj 5y agoSoftware Engineers: Parse, don't validate. Mathematicians: Parsing is validation https://gallais.github.io/pdf/draft_sigbovik21.pdf https://gallais.github.io/pdf/draft_sigbovik21.pdf
- pwdisswordfish8 5y agoThe point being, the converse of ‘parsing is validation’ is not true.
- ukj 5y agoThe word "is" implies an isomorphism. If you see it differently you are implicitly assuming a non-formalist perspective on what "validation" means. Tell us about it.
- codetrotter 5y agoThe word “is” is also often used informally to mean “is a kind of”.
- ukj 5y ago"A kind of" is precisely its formal use from the PoV of a type theorist. Two things are the same type of thing if they share all of their extensional properties. That is what it means for two things to be identical/equal.
- deleted 5y ago[deleted]
- codetrotter 5y agoBut what I am saying is that parsing is a kind of validation. But all validation is not parsing. For example let's say that I have written an HTTP API that accepts application/x-www-form-urlencoded data to one of its endpoints. Let's say `POST /users`, and this is where the client-side application posts data to. Now I can implement this in many ways. I can for example define pub struct Person { name: String, phone_number: String, } But how I populate this struct can determine whether I am actually parsing or not, even if most of the code aside from that is the same. And of course I could go further and define types for the name and the phone number but in this case lets say that I have decided that strings are the proper representation in this case. If the fields of my structs were directly accessible pub struct Person { pub name: String, pub phone_number: String, } And in my HTTP API endpoint for `POST /users` I do the following: // ... let name = post_data.name; let phone_number = post_data.phone_number; let norwegian_phone_number_format = Regex::new(r"^(\+47|0047)?\d{8}$").unwrap(); // ... And I didn't bother to write out the rest of the code here for this example but you get the gist. The point is that here I am doing some rudimentary validation on the phone number, requiring it to be in Norwegian format. But I am enforcing this in the implementation of the handler for the HTTP API endpoint, rather than in the data type itself. Whereas if I was instead doing pub struct Person { name: String, phone_number: String, } impl Person { pub fn try_new (name: String, phone_number: String) -> std::result::Result<Self, PersonDataError> { // ... let norwegian_phone_number_format = Regex::new(r"^(\+47|0047)?\d{8}$").unwrap(); // ... } } Now I've moved the validation into an associated function of the type itself, and I've made the fields of the struct unaccessible from the outside. And in this manner, even though my validation is still rudimentary, and a type purist might find the type insufficiently constrained, I have indeed in my own book gone from just validation to actual parsing. Because I have made it so that the construction of the type enforces the constraints on the data.
- ukj 5y agoYou are over-complicating this into obscurity. General case: Validating random data as input into some program. Particular case: Validating random source code (data) as input into some compiler (program). Do compilers parse or validate? "parsing is validation, but validation is not parsing" if that were true then you should be able to give an example of a compiler doing some sort of validation on the random source code (data) that is not parsing. The very thing which determines the validity of random source code is the compiler's ability to parse it.
- thereare5lights 5y ago> The word "is" implies an isomorphism. Are you talking about a bijective mapping or are you saying it's a synonym for identical? Because the former doesn't make any sense here and the latter is not true. Red is a color does not imply that all colors are red.
- ukj 5y agoI am talking about the polymorphic use of the verb "is" during the process of formalization. "Red is a color" can be formalized as "Red is a type of color" or "Red is member of set Colors". You can't formalize "Color is red" because it doesn't mean anything. When I say "Parsing is validation" I am using the verb "is" to mean an isomorphism.
- thereare5lights 5y agoJudging by all the other disagreeing comments, your in some sort of idiosyncratic context that only you understand. Good luck with that.
- deleted 5y ago[deleted]
- pwdisswordfish8 5y ago‘A square is a rectangle’ means squares are isomorphic to rectangles?
- ukj 5y agoYou are tripping up over polymorphism. "Is" means many things - which meaning you infer is precisely parsing! "A square is a rectangle" means "A square is a TYPE of rectangle" (at least, that is what I am parsing it as). "Parsing is Validation" means Parsing is isomorphic to Validation. How do I know? Because that is how I want you to parse it.
- pwdisswordfish8 5y ago> 'When I use a word,' Humpty Dumpty said in rather a scornful tone, 'it means just what I choose it to mean — neither more nor less.'
- ukj 5y ago+∞ parse verb. resolve (a sentence) into its component parts and describe their syntactic roles. In computer science what we do is precisely syntax analysis. Determining the meaning of operators. Mathematicians have the exact same problem with respect to the equality operator. https://ncatlab.org/nlab/show/equality#DifferentKinds https://ncatlab.org/nlab/show/equality#DifferentKinds
- jhgb 5y ago> "A square is a rectangle" means "A square is a TYPE of rectangle" (at least, that is what I am parsing it as). In that case your former statement that 'The word "is" implies an isomorphism' seems to be wrong.
- ukj 5y agoIt may be wrong in your model/interpretation of my words, but it's not wrong in my interpretation of my words.
- ukj 5y agoThen you have some formally inexpressible/impredicative notion of "validation" in mind. For posterity (lifting from the depths of the threads): General case: Validating random data as input into some program. Particular case: Validating random source code (data) as input into some compiler (program). Do compilers parse or validate? > "the converse of ‘parsing is validation’ is not true." If that were the case then you should be able to give an example of a compiler validating random source code (data) but not parsing it. What determines the validity of random input is precisely a compiler's ability to parse it.
- cjfd 5y agoSoftware engineers like efficiently running software. Mathematicians like beautiful definitions. Scientists like non-trivial discoveries. This paper.... uh.... what exactly is it good for? I suppose it could be kind of nice as some kind of undergraduate paper writing project kind of thing but it looks too professional for that.... I am kind of at a loss why this was written. Maybe it is some strange kind of satire....
- ukj 5y agoThis paper is good for parsing/validating your source code (from the view-point of your compiler/interpreter). Code is data after all.
- squiddev 5y agoIt's written for sigbovik 2021 [1][2], which is very much a joke conference. Other papers this year were "Lowestcase and Uppestcase letters: Advances in Derp Learning" and "On the dire importance of mru caches for human survival (against Skynet)". [1]: http://www.sigbovik.org/ http://www.sigbovik.org/ [2]: http://www.sigbovik.org/2021/proceedings.pdf http://www.sigbovik.org/2021/proceedings.pdf
- m3koval 5y agoSIGBOVIK is a parody of computer science conferences. It's a running joke hosted on April Fools Day every year at CMU - and apparently a quite convincing one. ;-) Source: I attended SIGBOVIK a few times in grad school.
- Drup 5y agoTo everyone in this subthread: sigbovik is a conference published every 1st of April. This paper is an April's fool joke. I didn't think people could take that one seriously. I guess it's a good April's fool then. :)
- rualca 5y agoThe wide adoption of Flask as Python's backend development framework of choice makes it quite clear that software developers have a hard time picking up April fool's jokes.
- 411111111111111 5y agoOr how great April fool's ideas can actually be if they turn out to be real
- ukj 5y agoThe conference is indeed a spoof, but in so far as what Mathematicians call a "proof" - the paper contains one. Agda is a proof assistant in the spirit of the Calculus of Constructions ( https://en.wikipedia.org/wiki/Calculus_of_constructions https://en.wikipedia.org/wiki/Calculus_of_constructions ). So is the joke on Computer Scientists or Mathematicians? You decide ;) Beware of bugs in the above code; I have only proved it correct, not tried it --Donald Knuth
- Drup 5y agoSigbovik's jokes are of the kind where the premise is completely bonkers. The rest of the development is made with the utmost rigor to highlight said bonkersitude, Reductio ad absurdum.
- ukj 5y agoYeah, but that is precisely how inductive types work. "Bonkers" premises. Iterate, iterate, iterate. "Bonkers" conclusions. GIGO. And yet the result is reified, exists and speaks for itself. So what is so "absurd" and "bonkers" about a result that is right before your eyes? https://en.wikipedia.org/wiki/Reification_(computer_science) https://en.wikipedia.org/wiki/Reification_(computer_science)
- gbrown_ 5y agoPrevious discussion https://news.ycombinator.com/item?id=21476261 https://news.ycombinator.com/item?id=21476261
- Attummm 5y agoThat's how the validation tool for Python Maat works. By creating a completely new dictionary. https://github.com/Attumm/Maat https://github.com/Attumm/Maat
- errnesto 5y agoFor js / typescript I like: https://github.com/paperhive/fefe https://github.com/paperhive/fefe basically ist’s just functions that take a value of one type and return a other one
- vorticalbox 5y agoThere is also joi, zod, myzod just to name a few. I personally use myzod as its fast it parsing, zero dependancies and you can infre types from your schemas.
- catlifeonmars 5y agoDon’t forget https://github.com/gcanti/io-ts https://github.com/gcanti/io-ts
- mulmboy 5y agoHow does Maat compare with pydantic? https://github.com/samuelcolvin/pydantic https://github.com/samuelcolvin/pydantic
- rmnclmnt 5y agoIf only for conciseness, readability and speed, I'd take Pydantic over any day. Being able to express 80% of type checking using Python native type hints + dataclasses is just so intuitive! And it's getting some wide adoption, for instance FastAPI which uses it for request validations.
- Attummm 5y agoEngineering is about tradeoffs, even though both projects do validation. The points you made are all very valid points. At my employer we use both projects. If the data is very nested, or really large Maat is used.
- mirekrusin 5y agoIn typescript parsing/asserting types with combinators works very well merging runtime with static type system [0], it has to be used at i/o boundary, then it enters static type system guarantee and no assertions are necessary, makes very nice codebase. [0] https://github.com/appliedblockchain/assert-combinators https://github.com/appliedblockchain/assert-combinators
- lloydatkinson 5y agoI wish it had actual proper examples. I've no idea how to use that.
- uryga 5y agofrom a look at the readme, you combine those `$.TYPE` things to build a validation function that checks if its argument matches some pattern (and throws an exception if it doesn't). import * as $ from '@appliedblockchain/assert-combinators' const validateFooBar = ( $.object({ foo: $.string, bar: $.boolean }) ) // probably roughly equivalent to /* const validateFooBar = (x) => { console.assert( typeof x === 'object' && typeof x.foo === 'string' && typeof x.bar === 'boolean' ) return x } */ const test1 = { foo: "abc", bar: false } const test2 = { foo: 0, quux: true } const { foo, bar } = validateFooBar(test1) // ok const oops = validateFooBar(test2) // throws error the source is pretty readable too if you want to get an idea how it works. https://github.com/appliedblockchain/assert-combinators/blob/master/boolean.ts https://github.com/appliedblockchain/assert-combinators/blob... https://github.com/appliedblockchain/assert-combinators/blob/master/object.ts https://github.com/appliedblockchain/assert-combinators/blob...
- mirekrusin 5y agoYes, the difference between console.assert and assert-combinators is that assert combinators return well typed ts result (and are more terse/minimal).
- hermanradtke 5y agoCheck out https://github.com/gcanti/io-ts/blob/master/index.md https://github.com/gcanti/io-ts/blob/master/index.md instead. I find it more composable and you can define a codec and get a native type from it so you are only defining things once.
- Vosporos 5y agoWonderful piece, it has really opened my eyes on foreign data ingestion.
- wodenokoto 5y agoWhen I think of validation I think of receiving a data file and checking that all rows and columns are correct and generating a report about all the problems. Does my thing have a different name? Where can I read up on how to do that best?
- quickthrower2 5y agoI thought of input validation for web forms. Similar thing I guess. In Haskell you can create a type that you know is a validated email address but you still need a validation function from String -> Maybe Email to actually validate it at runtime
- jacoblambda 5y agoThat's just a parser though. Like described in the post, parsers sometimes can fail but importantly they always pass along the result if they succeed. Validation functions on the other hand only validate that said data is valid. The argument is that if you need to interact with or operate on some data you shouldn't be designing functions to validate the data but rather to render it into a useful output with well defined behaviour.
- WJW 5y agoI think for the usecase GP gives it'd be even better to have a function `String -> Either (LineNumber,String,[Problem]) Email`, so that you can report back which of the lines had problems and what kind of problems. For web form validation you can skip the line number but it'd still be useful to keep the list of problems, so that you can report back to the user what about their input did not conform to expectations.
- foota 5y agoData validation?
- rmnclmnt 5y agoYou can use the now widely adopted Great Expections[0] library, which fits exactly this use-case for data validation! [0] https://greatexpectations.io https://greatexpectations.io
- bruce343434 5y agoThis still sounds like validation but with extra steps. (or less?)
- plesiv 5y agoThe post is saying: - don't drop the info gathered from checks while validating, but keep track of it - if you do this, you'll effectively be parsing - parsing is more powerful that validating "Extra steps" would be keeping track of info gathered from checks.
- bruce343434 5y agoRight. My takeaway was "verify and validate once, then put it in a specially marked datastructure, or if your language allow it make the typesystem guarantee some conditions of the data, then work with that from there". Where does parsing come in the picture?
- vvillena 5y agoWell, ain't that it? If you validate a string so that it contains some angle bracket tags at the beginning and the end, ensure that both the tag values contain the same string (except for one extra marker in the end tag), and store the tag name and the string within the tags in a purpose-made data structure, you can call it whatever you want. Some will call that a rudimentary XML parser.
- kortex 5y ago> verify and validate once, then put it in a specially marked datastructure Not to steal vvillena's thunder, but that's pretty much the dictionary definition of "parsing" > analyze (a string or text) into logical syntactic components, typically in order to test conformability to a logical grammar. Parsing is taking some collection of symbols, and emitting some other structure that obeys certain rules. Those symbols need not be text, they can be any abstract "thing". A symbol could be a full-blown data structure - you can parse a List into a NotEmptyList, where there's some associated grammar with the NEL that's a stricter version of the List grammar.
- dgb23 5y agoThis principle can be applied to dynamic languages as well if you have some mechanism such as type hinting, pre-conditions etc. that are checked by a linter during development, even if it isn't, you can still use it at runtime with sufficient error handling. The essential point of this blog post is to avoid "shotgun parsing", where parsing/validating is done just from a procedural standpoint, where it matters when exactly it happens. In the paper "Out of the Tar Pit" it is asserted that this leads to "accidental complexity" (AKA "pain and anxiety"), which is something every programmer has experienced before, possibly many times. I've become a fan of declarative schema to (json-schema/OpenApi, clojure spec etc.) to express this kind of thing. Usually this is used at the boundaries of an application (configuration, web requests etc.) but there are many more applications for this within the flow of data transformations. If you apply the "parse don't validate" principle you turn schema-validated (sic!) data into a new thing. Whether that is a "ValidatedData" type or meta data, a pre-condition or runtime check says more about the environment you program rather than the principle in discussion. The benefit however is clear: Your code asserts that it requires parsed/validated data where it is needed, instead of when it should happen.
- frogulis 5y agoI think the article goes a little further than what you describe -- it would have you use a strong type that cannot represent illegal values. There's a follow-up article by the same author (that I unfortunately can't find), in which she explains this point. As an example, returning a NonZero newtype over Int is not as type safe as using an ADT that lacks a zero value altogether. Using a NonEmpty newtype over List is not as type safe as using the NonEmpty ADT that has an element as part of its structure. Basically newtype still has use, but it is not as airtight as a well-designed ADT.
- runeks 5y ago> There's a follow-up article by the same author (that I unfortunately can't find), in which she explains this point. I think this is it: https://lexi-lambda.github.io/blog/2020/11/01/names-are-not-type-safety/ https://lexi-lambda.github.io/blog/2020/11/01/names-are-not-...
- 5y ago
- spinningslate 5y agoThis is a great post. I come back to it frequently. There's beautiful clarity in the articulation, and the essence is easy to grasp yet powerful. It reminds me a bit of Scott Wlaschin's Railway Oriented Programming (ROP) [0]. As a technique, ROP nicely complements "parse don't validate". As an explanation, it's similarly simple yet wonderfully effective. I've a real admiration for people who can explain and present things so clearly. With ROP, for example, the reader learns the basics of monads without even realising it. [0]: https://fsharpforfunandprofit.com/rop/ https://fsharpforfunandprofit.com/rop/
- ChrisMarshallNY 5y agoI agree. It's a very well-written post. I am not a Haskell person, but it was quite clear to me. I feel that we don't put enough value, these days, on the ability to write clear, articulate exposition. Also, I believe that many people are not willing to read articles, books, or papers, of any meaningful length. Everything needs to be boiled down to <10 min. read time, or <18 min. TED talks.
- themulticaster 5y agoThe way you somewhat randomly mention the value of clear and articulate exposition makes me assume you just had to wade through a 300-page specification document for a government contract regarding pencil sharpeners or something similar. If that's the case, you have my sympathy. Anyways, I definitely agree.
- ChrisMarshallNY 5y agoNot recently (thank the Gods), but I used to work for a defense contractor, and I have dealt with many specification documents (like the Bluetooth spec).
- wrycoder 5y agoThose are the extras. This is the post: https://fsharpforfunandprofit.com/posts/recipe-part2/ https://fsharpforfunandprofit.com/posts/recipe-part2/
- TheAceOfHearts 5y agoRelated discussion from last month which links to this article in the repo description: https://news.ycombinator.com/item?id=27166162 https://news.ycombinator.com/item?id=27166162
- iamwil 5y agoA related mantra is to "Make impossible states impossible" https://www.youtube.com/watch?v=IcgmSRJHu_8 https://www.youtube.com/watch?v=IcgmSRJHu_8
- nathcd 5y agoI think the original formulation is "make illegal states unrepresentable" from Yaron Minsky: https://blog.janestreet.com/effective-ml/ https://blog.janestreet.com/effective-ml/ and https://blog.janestreet.com/effective-ml-revisited/ https://blog.janestreet.com/effective-ml-revisited/ (or maybe there are older sources than 2010?)
- seanwilson 5y agoFrom 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.
- k__ 5y agoCould someone rewrite the examples in TypeScript? Some points really elude me because Haskell uses many symbols and is very dense.
- adamlett 5y agoI think making this about the type checker is a bit of a red herring. There is nothing in this – otherwise excellent – advice that can’t be applied to a dynamically typed language like Ruby. It’s the same insight that leads OOP folks to warn against the Primitive Obsession code smell (http://wiki.c2.com/?PrimitiveObsession http://wiki.c2.com/?PrimitiveObsession). It’s also the insight that leads to the Hexagonal Architecture ( https://en.wikipedia.org/wiki/Hexagonal_architecture_(software) https://en.wikipedia.org/wiki/Hexagonal_architecture_(softwa... ).
- Zababa 5y agoThe advantage of the type checker is that it automatically check types for you. If you have only one function that produces a parsedArray and multiple functions that accept a parsedArray, you can be sure where they come from.
- adamlett 5y agoYes, that’s literally the advantage of a static type checker. I don’t dispute that. I’m just saying that the advice in the article is just as applicable in a dynamic language and confers the same benefits. True, you’re not protected against accidentally using one type where another was expected, but that’s not really the point as I see it. The point is to use better types. Types you can lean on instead instead of nervously tiptoe around.
- garethrowlands 5y agoI don't think types are a red herring here. Because if you follow this advice, then just using logic and your source code, you can prove what data is valid. And, since types and (constructive) logic are so strongly related, then the types are, in some sense "there" even if you don't see them. To put it another way, it's nice if your computer can make the proofs but if it can't, does that make the theorems any less true?
- imoverclocked 5y agoI agree but I also think it’s on the right path. This seems partially like a “why Haskell” in disguise to me. I’ve run across DSLs that have three or more layers of parsing and validation. Embedding different languages within each other (eg: JSON snippets within your own DSL) definitely leads to the issues the article talks about. Also, growing your own parser without understanding standard lexer/parser basics seems far more common than it ought to be. I’m not talking brilliant design, rather the extremely naive one-character-at-a-time-in-a-really-complex-loop variety of design. The better level of bad is, “I know what lexers/parsers are, now I’ll write something to basically implement a type-checking parser with the lexed+parsed tree as input.” This article is basically stating, “Why not just get your parser to do it all for you in one swell foop?” When I have refactored code to follow this kind of design, I have never regretted the outcome.
- flqn 5y agoWhat would you rather have: an object of value potentially outside your domain and an expensive boolean function saying if it's ok that you need to apply everywhere just to be sure, or a method of producing values that you know are always within your domain which you have to apply just once and no expensive boolean function?
- kortex 5y agoThis principle is how pydantic[0] utterly revolutionized my python development experience. I went from constantly having to test functions in repls, writing tons of validation boilerplate, and still getting TypeErrors and NoneTypeErrors and AttributeErrors left and right to like...just writing code. And it working! Like one time I wrote a few hundred lines of python over the course of a day and then just ran it... and it worked. I just sat there shocked, waiting for the inevitable crash and traceback to dive in and fix something, but it never came. In Python! Incredible. [0] https://pydantic-docs.helpmanual.io/ https://pydantic-docs.helpmanual.io/
- dnadler 5y agoWe've had a similar experience using pydantic. We integrated it quite tightly with a django project and it's been awesome.
- theptip 5y agoWhere did you find it valuable to wire in to Django?
- dnadler 5y agoWe effectively use them as serializers, and also as a way to allow users to interact with the models in a controlled manner. In our app, django essentially sits between a compute cluster and a front end. The pydantic objects are used to define the work to be done on the compute cluster.
- jimmaswell 5y agoI've found this to be simply a matter of experience, not tooling. As the years go by I find the majority of my code just working right - never touched anything like pydantic or validation boilerplate for my own code, besides having to write unit tests as an afterthought at work to keep the coverage metric up.
- vikingcaffiene 5y ago
- AlexSW 5y agoI couldn't agree with this post more. I found myself replacing the configuration parsing code in a C++ project that was littered with exactly the validation issues described, and converted it to that which the author advocates. The result was a vastly more readable and maintainable codebase, and it was faster and less buggy to boot. Another nice advantage is that the types are providing free/self- documentation, too.
- pengwing 5y agoI'd call it: pattern match, don't validate. Gotta go and program more Elixir...
- pansa2 5y ago“Parse, don’t [just] validate”. Say I have a string that’s supposed to represent an integer. To me, “Validate” means using a regex to ensure it contains only digits (raising an error if it doesn’t) but then continuing to work with it as a string. “Parse” means using “atoi” to obtain an integer value (but what if the string’s malformed?) and then working with that. I first thought this article was recommending doing the latter instead of the former, but the actual recommendation (and I believe best practice) is to do both.
- b3morales 5y agoYou seem to suggest that it's possible to parse without validating, which I'm not sure I follow. Surely validation is just one of the phases or steps of parsing?
- pansa2 5y agoFunctions like `atoi` parse strings into integers, but will happily accept “ 10blah” and return 10. In my experience it’s best to validate that the string is well-formed (e.g. contains only digits) before passing it to one of those functions.
- nsajko 5y agoThe point is that validation is (or should/can be) a byproduct of parsing. I.e., you shouldn't "do both", rather the validation should be encompassed by the parsing, as much as it makes sense.
- pickdig 5y agoa tiny advice: can the blog author do some separation between his next and previous article? it's really hard to read unless you hover your mouse over one of them otherwise. link to what i'm talking about: https://ibb.co/kc0HKy9 https://ibb.co/kc0HKy9
- ChrisArchitect 5y agodiscussed alot a month ago https://news.ycombinator.com/item?id=27166162 https://news.ycombinator.com/item?id=27166162