10 ms·
Tagged Unions Are Overrated
- dmitriid 6y ago> the limits of pattern-matching in languages that do support it And then goes to discuss Rust only. Nope. In many languages pattern matching is crippled because it is only allowed to be used in some contexts, but not in others. Or are crippled by things like "can't pattern match if something-something pointers". For example, in Erlang you can use pattern matching and guards in function definitions, creating overloaded functions based on pattern-match alone. And you can match arbitrarily complex structures (because it doesn't have pointers etc.). This makes working with ASTs in such a language a breathe.
- jerome-jh 6y ago> "can't pattern match if something-something pointers" The main confusion I see is that in many languages it is unclear whether sum types are a runtime or compile time thing, which decides whether pattern matching is semantic or syntactic. Of course nobody wonders whether "if then else" is semantic, but for pattern matching I admit I am myself confused: - in C all types are compile time (and loosely enforced) so if you want sum types, do them yourself. Implementing the matching is quite straightforward but unsafe and pedantic. - in Java all type information is available at runtime: sum types can be implemented easily (casting to Object), matching is safe and can be compact if done with e.g. arrays. - in C++ thinks start getting trickier: you can certainly do a lot mixing static and dynamic casts, but will this remain readable? - in ML languages: AFAIK this is completely implementation defined. The compiler may choose to box some or all variables. Obviously in such a language you much less care about what is going on under the hood. In return you get a true pattern-matching construct. - in Rust and others: frankly I don't know where they stand in regard of compile time vs runtime :)
- xyzzy_plugh 6y agoAside: what the fuck is going on with this webpage? The only scripts I have blocked are from stripe.com (why are there stripe.com scripts on an newsletter?) and the webpage totally breaks rendering in Chrome. Instead there's a horizontal scroll bar that, when scrolled, renders a different part of the article but ruins the older parts. Is this some method of tracking users reading through the entire article? Also the entire newsletter content is in a variable in a script tag? If javascript is disabled, you see the entire article. What is the point of all this javascript?
- frou_dh 6y agoIt's against HN's written Guidelines to moan about the format of articles
- diroussel 6y agoBut it would be interesting to those of us that build web pages why this one works this way. This is not just bad formatting, it’s a deeper mystery.
- frou_dh 6y agoThe caricature of HN is indeed a café of webdevs talking shop the whole time
- mhh__ 6y agoAnd yet people still do it, always accompanied by a reference to the guidelines. This isn't HackerParliament, I don't mind people venting about bad web design. Moaning about clickbait, however, I immediately downvote because it actually does contribute nothing.
- arduanika 6y ago> HackerParliament The right honourable website is rather slapdash with its scripts, without which it cannot render well upon Her Majesty the Chrome. Hear hear!
- jcelerier 6y ago> In object-oriented languages like Java or C++, you can implement your AST as an abstract base class and a set of subclasses, and do matching using straightforward type tests — instanceof in Java, or dynamic_cast in C++. but at a pretty large cost : now everything has to be boxed, will be dynamically allocated by default (yes, object pools and small-size optimization, but that does not produce very readable code and needs constant care), you risk slicing, every access has an indirection, etc. Migrating some C++ code from OOP inheritance to variant-like stack-allocated value types has always resulted in n-fold improvements to runtime performance in my case.
- tom_mellior 6y ago> now everything has to be boxed [...] every access has an indirection, etc. The article makes clear that it is speaking of compiler intermediate representations. Those are pretty much by definition boxed, linked data structures that live on the heap. > Migrating some C++ code from OOP inheritance to variant-like stack-allocated value types has always resulted in n-fold improvements to runtime performance in my case. Was this in the context of processing some form of programming language IR?
- otabdeveloper4 6y agoYes, C++ is not an "object-oriented language" and never was. The OO features were added because at the time it was the raging fad and people thought that every language simply must have "OOP". But "OOP" was never a foundational principle for C++, more like a tacked-on sidecar.
- tom_mellior 6y agoYou're arguing that a language that arose specifically to add classes to C was not meant to be object-oriented? I'd be interested in seeing that argument fleshed out in more detail.
- gpderetta 6y agoIt is true that subsequent c++ evolution de-emphasised OOP, but you are completely right that the original selling point of C++ over C definitely was (Simula style) OOP.
- yoneda 6y agoThe primary advantage of proper pattern matching compared to ad hoc "instanceof" checks is exhaustiveness checking. I love the comfort in knowing the compiler will tell me all the places in the code that need to be updated when I add a new case to an algebraic data type. I know the article touches on this, but IMHO the author under-appreciates this important feature.
- ncmncm 6y agoThe author specifically discusses that topic, toward the end of the article. You don't need to guess about the author's experience there. Just read the rest of the article.
- yoneda 6y agoSeems like you didn't read my comment in its entirety. I acknowledged that the article discusses exhaustiveness checking, so you don't need to point that out to me. I didn't make any guesses about the author's experience; I stated my opinion of their opinion.
- tom_mellior 6y ago> The primary advantage of proper pattern matching compared to ad hoc "instanceof" checks is exhaustiveness checking. In programming language intermediate representations (the context of the article) exhaustiveness checking might not be as useful as in other contexts. You will have many variants, but for every concrete operation you want to do on the IR you will only consider a few of them, with a default catch-all case for all others. For example, imagine you're writing a tool to represent and process source code for a Python-like language. Here are the statement types: type Statement = | Assign var value | ExprStatement value | If cond true_branch false_branch | While cond body | Return value | Break | Continue | With context body (I'm sure I forgot some interesting ones.) You want to do some analysis related to branching control flow statements. These are If and While, so you write: let analyze_control_flow stmt = match stmt with | If _ true_branch false_branch -> do_something_with true_branch; do_something_with false_branch | While _ body -> do_something_with body | _ -> // not a branching control flow statement () This works nicely. Except that Python recently got pattern matching itself, so you add a new variant to your Statement type: ... | Match pattern cases ... and then you recompile and fix a few exhaustiveness errors and feel happy that you have handled everything, except that you haven't. You would also need to update your definition of analyze_control_flow, but due to the catch-all clause your compiler didn't alert you to this. (On the other hand, such fundamental language/IR changes are relatively rare.)
- deleted 6y ago[deleted]
- meheleventyone 6y agoWent in with righteous anger at the title came out agreeing with the article. The main advantage of any pattern being a part of the language is the ergonomics of it. If you want to use the pattern in another language for whatever reason that's just as valid but obviously less straightforward.
- AzzieElbab 6y agoI am reading this as tagged unions are overrated in rust because many use cases are not properly supported in rust, or at least not yet
- arduanika 6y agoYes. Or to generalize, language features are overrated. Ergonomics and support are underrated. Sadly, this reinforces the moat of legacy language. Why move to a new language with marginally better language features, when you can stick to languages that already have good JetBrains support?
- AzzieElbab 6y agoI would agree on most features, but I absolutely love properly implemented, exhaustive pattern matching in strongly typed languages.
- mrkeen 6y agoWhy move to garbage collection when C already has good valgrind support?
- siraben 6y ago> In Rust, pattern matching breaks down as soon as your IR contains pointers to its children > Another way this manifests is that it’s often frustrating that patterns aren’t first-class; I can’t pass around a pattern, or build up a pattern on the fly. To me this seems like limitations of pattern matching in Rust that perhaps is addressed by Haskell's ViewPatterns language extension. The idea is that the usual ML-style pattern matching exposes too much implementation detail of the ADT, for instance, consider instead the use of a "view function" viewer to convert the structure into a initial segment xs and a last element x. pattern (:|>) :: Seq a -> a -> Seq a pattern xs :|> x <- (viewr -> xs :> x) where xs :|> x = xs |> x lastElem :: Seq a -> a lastElem (_ :|> e) = e :|> would let one treat the sequence data type as abstract (it could be a finger tree[0] underneath, for instance). In the author's use case of representing IRs, one could move the unboxing work into the view. [0] https://en.wikipedia.org/wiki/Finger_tree https://en.wikipedia.org/wiki/Finger_tree
- dgellow 6y agoI know that's not part of the article title, but could someone add "...for implementing language ASTs and IRs" to the end of this submission title? That's what the article is about and changes quite a lot the context of the discussion. The context is "tagged unions when programming languages", not just "tagged unions".
- mrkeen 6y agoOr "Tagged unions are great, but not as good as they could be (depending on whether your language has good pattern matching) and maybe you could roll something worse yourself".
- mamcx 6y agoThe problem with unions/enums is that is hard to pick a subset or superset of them, despite it will be not hard to add to any language. Is similar to projection on the relational model (aka:select), sometimes I need: enum Value { Int32(i32), Int64(i32), Str(String), } fn only_int(of:Value[Int32, Int64]) -> Value.Str And other times: enum ExtraValue:Value { Bool(bool), } This is analog to structs, where eventually is "discovered" that is nice to: let user2 = User { email: String::from("another@example.com"), username: String::from("anotherusername567"), active: user1.active, sign_in_count: user1.sign_in_count, }; let user2 = User { email: String::from("another@example.com"), username: String::from("anotherusername567"), ..user1 }; But somehow enums are still considered second-class in the algebraic type space.
- vmchale 6y agoThere's a hack! You can do data Value a = I32 Int32 a B Bool and then type Value1 = Value () type Value2 = Value Void as a hack
- mpartel 6y agoScala and TypeScript have (partial) answers to this. In your example, Scala 2 would let Int32 and Int64 inherit a trait that Str doesn't. TypeScript would let you write a union type Int32 | Int64 that doesn't include Str (or add ExtraValue), and I think Scala 3 will have something very similar.
- Ari_Rahikkala 6y agoI used to think that this was only ever going to be possible with some sort of incomprehensible type trickery that I would never be able to understand. Then TypeScript came along and showed me that no, actually, in a structural type system, it's just about as simple as you could imagine: type Foo = { kind: 'A', aItem: number } | { kind: 'B', bItem: string } | { kind: 'C' }; type SubsetFoo = Foo & { kind: 'B' | 'C' } type SupersetFoo = Foo | { kind: 'D', dItem: boolean }; I'm sure there are imperfections here. For a start, SubsetFoo's normalized form looks rather ugly when you mouse over it in VSCode. But it does get you the niceties of exhaustiveness checking and type-aware suggestions with control flow awareness, etc..
- MetaDark 6y ago> However, the crux of my argument is that it’s easy enough to emulate this construction in other languages. It's also easy enough to emulate all control flow statements with goto. Tagged unions are valuable because they help communicate restraints. Not only to other developers, but also to the compiler.
- TheCoelacanth 6y agoI've got to disagree. If anything they are underrated. Not having them in your language in 2021 should be considered the same as not having if/then/else in your language in 1980.