Hacker News new | ask | show | jobs
by different_base 768 days ago
One of the crimes of modern imperative programming languages is not having ADTs (except maybe Rust) built-in. It is such a basic mental model of how humans think and solve problems. But instead we got inheritance and enums which are practically very primitive.
15 comments

Moreover, they have already been proposed by John McCarthy in October 1964, 60 years ago, for inclusion in the successor of ALGOL 60, which makes even more weird the lack of widespread support.

(And in fact Algol 68 had a better implementation than most later languages, but Algol 68 was missing completely any documentation suitable for newbies, like tutorials and programming examples, while not being promoted by any hardware vendor, like IBM or DEC, so it was doomed.)

>The more I ponder the principles of language design, and the techniques which put them into practice, the more is my amazement and admiration of ALGOL 60. Here is a language so far ahead of its time, that it was not only an improvement on its predecessors, but also on nearly all its successors.

https://web.eecs.umich.edu/~bchandra/courses/papers/Hoare_Hi...

I've never written Swift, but it seems like they have it too https://docs.swift.org/swift-book/documentation/the-swift-pr...

I also would love a future where ADTs are more common in imperative languages

Swift “enumerations” are very nice.
Zig is a modern imperative programming language with ADTs: https://ziglang.org/documentation/master/#Tagged-union
While this is a step up from C, it is still a long way from the full power and generality of algebraic data types. The key word here is algebraic. In a language with ADTs, such as Haskell, you can pattern match on an arbitrarily complex types, not just the outermost tag. A contrived example (from [1]):

    contrived :: ([a], Char, (Int, Float), String, Bool) -> Bool
    contrived    ([],  'b',  (1,   2.0),   "hi",   True) = False
To achieve a result like this using Zig's switch syntax would seem to involve a huge amount of boilerplate code and nested switch statements.

[1] https://www.haskell.org/tutorial/patterns.html

This is more of syntax sugar than power and generality, since nested pattern matching can be mechanically translated into "top-level" matching (e.g., see [1] and [2]).

[1] L. Augustsson. Compiling Pattern Matching. In Functional Programming Languages and Computer Architecture, pages 368– 381, 1985.

[2] P. Wadler. Efficient Compilation of Pattern Matching. In S.L. Peyton Jones, editor, The Implementation of Functional Programming Languages, pages 78–103. Prentice Hall, 1987.

This is where I like to cite Dijstra's "Go To Statement Considered Harmful".

What a lot of people miss about that paper is that he wasn't just talking about goto statements. He was also making a more general observation about how more powerful and general programming language features are not necessarily desirable, because they tend to adversely impact developer productivity.

The reason I, as a user, prefer structured control flow statements over goto is not that I believe they are powerful. It's precisely because they are less powerful. The resulting constraints on how the program can be structured make it easier for me to read and reason about existing code. That makes maintaining code easier. It also makes optimization and static analysis easier. And it makes writing tests easier, too.

I have similar feelings about ADTs. The reason I prefer them to other ways of doing composite data types is not that I think they're more powerful. It's that they create constraints that tend to reduce the semantic complexity of the domain models people create in programming languages that use them. And that makes my job easier.

The corollary to that, though, is that I'm not actually all that hype about adding ADTs to existing languages. For reasons that are similar to how the mere availability of structured, reentrant function calls is small consolation in a codebase that's already riddled with goto statements. The real win doesn't come from using ADTs, it comes from not having to worry about all those other confusing overpowered things that aren't ADTs.

That's exactly where I am. Pattern matched sum types "feel great" to code in to an expert because they are a concise and reasonably tight way to express the otherwise boring "enumerate over possibilities" code.

But they're hard to read for anyone who isn't an expert on not just the language but the type in question (c.f. Rust's Option() idioms all looks like line noise to newbies, etc...). And that's a bad trade.

In essence, this stuff is just Perl all over again. It's a language feature that prioritizes concision over comprehension. And I say that as someone who really likes coding in perl. But "people" don't like perl, and the community moved on, and the reasons are... the same reason that uptake in ADTs is lagging where the experts want it to be.

You've got to distinguish syntax from semantics, though. I agree, it's easy to turn a big semantic win into a net readability loss if you choose to represent it with an overly terse syntax that promotes code golf.
Pattern matching on the "top level branch" of the ADT, whatever you call it, is pretty darned useful.

Pattern matching on the next level down is a power tool to be used with care.

Having used some pattern matching languages for quite some time, I find anything much deeper than that is a code smell at best and pathological at worst. Pattern matching creates coupling proportional to the depth/complexity/precision of the pattern match. The top-level coupling is often unavoidable; if you're pattern matching at all, you certainly care which "branch" you are on and there is likely no refactoring that away. But the danger rises rapidly the deeper in you go. It's just so easy to pattern match on a third-level part of the complex object when you really ought to be wrapping that behind a function somewhere, possibly itself emitting a sum type value.

... but if all you really need is that "top level" match, a lot of pattern matching syntax and features are not really necessary (if not positively dangerous).

This argument is the most common fallacy I see in programming language discussions. I might as well give it a name right here: "Turing equivalence fallacy" or perhaps "syntax sugar fallacy."

All Turing Complete programming languages are Turing equivalent to one another. Programs written in one language can be mechanically transformed into those written in another. This is irrelevant to the discussion of programming languages. The whole point of creating different programming languages is to explore different ways to express the same program!

In programming language design, we tend to distinguish between global and local analysis. While type checking and elaboration is an example of global analysis, desugaring is inherently local to some piece of code. Therefore, "power" or "expressiveness" usually mean that something cannot be syntactically "expanded"; e.g., while type classes elaborate into explicit dictionaries, they still require information from the type checker, and therefore considered a "real" feature of a programming language. On the other hand, nested pattern matching can be formulated as local syntax transformation, and therefore it doesn't bring anything fundamentally new to the type system or dynamic semantics.

There's also a great talk on the matter [1], if somebody is interested in formalities.

[1] https://www.youtube.com/watch?v=43XaZEn2aLc

You're approaching this from a PL design standpoint where the distinction is important, but from a user perspective it doesn't matter if it's just "syntax sugar" or if it's a super complicated to implement all that matters is whether the feature is available or not.
Of course the syntax sugar is a good thing if it makes it easier to write the code, but if the question is about "expressive power of the type system", it's not really relevant: Zig's type system can properly express a sum type. In addition: pattern matching is orthogonal to ADT, you can have pattern matching in both languages with and without algebraic types. Neither one implies the other.
> Zig's type system can properly express a sum type.

Surely any Turing complete PL can express a sum type? I can't imagine a language that can support products but not sums.

> Turing equivalence fallacy

Better known as the Turing Tar Pit.

syntax sugar IS power. also if the mechanical transformation is nontrivial, then so is the power of the sugar
I once explored the Epigram dependently typed programming language. It used to preclude many types of free form pattern matches, due to heavy dependence on structured editor (you speciy a type, it generates pattern matching, you cannot change the structure), so it was almost completely unuseable for many, many tasks.

So, while you are formally right, the need of shortcuts in pattern matching is undeniable to me.

Union types are not the same as sum types.
TS narrows union types cases based on conditionals like "if" (called discriminated unions in the docs in the past), and supports exhaustiveness checks. How do they differ in functionality from sum types?
Sum types are disjoint unions. This `T` has three cases

     L = { tag: "a", payload: string } | { tag: "b", payload: number }
     R = { tag: "b", payload: number } | { tag: "c", payload: boolean }
     T = L | R  
whereas a proper sum type `L + R` would have four.
Isn't that a completely useless distinction?

For all purposes and intents, the "b" type in L and R should be treated the same, no? What do you gain by not doing that??

As you've demonstrated, you can always construct sum types in typescript with the use of explicit discriminants:

    T = {tag: "L", payload: L} | {tag: "R", payload: R}
The real issue is typescript doesn't have pattern-matching, which make operating on these sum types inelegant
Supports exhaustiveness checks only if you explicitly opt-in it (by coding to pattern where you use helper function that accepts `never` type). "Dicriminated Unions Type"/"Sum Types" feels very hacky there, at least syntax-wise, because it is constraint by being "JS + types" language. It's remarkable what Typescript can do, but having native Discriminated Unions in JS (hence in TS too) would be much more ergonomic and powerful.
F# too. And Elm. But I get your point.
>> not having ADTs (except maybe Rust) built-in

Most of the common languages today have product types.

Java[1], Rust, Haskell, etc. have sum types.

I think it gets a bit more escoteric beyond that though - i don't doubt that there's probably some haskell extension for quotient types[2] or some other category theory high-jinx.

Most languages have ADTs built in.

[1] https://blogs.oracle.com/javamagazine/post/inside-the-langua... [2] https://en.wikipedia.org/wiki/Quotient_type

Does Java sealed classes enable something like an exhaustive pattern matching? (A form of pattern matching that will fail at compile time if you add a new class that extends the sealed class)
Yes, since Java 21.

Example:

    sealed interface BinaryTree {
      record Leaf(int value) implements BinaryTree {}
      record Node(BinaryTree lhs,
                BinaryTree rhs,
                int value) implements BinaryTree {}
    }

    public class Hello {
      static int sum(BinaryTree tree) {
        return switch (tree) {
        case BinaryTree.Leaf(var value) -> value;
        case BinaryTree.Node(var lhs, var rhs, var value) -> sum(lhs) + value + sum(rhs);
        };
      }
    
      public static void main(String... args) {
        var tree = new BinaryTree.Node(
                                       new BinaryTree.Leaf(1),
                                       new BinaryTree.Node(
                                                           new BinaryTree.Leaf(2),
                                                           new BinaryTree.Leaf(3),
                                                           4),
                                       5);
        System.out.println(tree);
        System.out.println("Sum: " + sum(tree));
      }
    }
If you added a new subtype to BinaryTree you would need to fix the switch.

EDIT: I didn't handle the `null` case above... so it would be a NullPointerException if someone passed null... apparently, Java decided to make handling `null` optional. More information: https://www.baeldung.com/java-lts-21-new-features

Okay that's better than I expected!
It absolutely does. Here is a (modified) snippet of my Java code from yesterday.

    final boolean hasUncollectedSecret =
       switch (each)
       {
               
          case Wall()    -> false;
          case Goal()    -> false;
          case Player p  -> false;
          case BasicCell(Underneath(_, var collectible), _)
             ->
                switch (collectible)
                {
                        
                   case NONE, KEY -> false;
                   case SECRET -> true;
                        
                };
          case Lock()    -> false;
               
       };
> The intent is to introduce a more-advanced construction called pattern matching in a later release.
You read that in a blog post from 2019.

Java has had comprehensive pattern matching since Java 21, like one year ago (current Java version is 22).

I posted an answer to the same parent comment with the C example written in Java...

You can read more about it here: https://www.baeldung.com/java-lts-21-new-features

Java's sealed classes are still somewhat more limited than Rust's or Haskell's sum types, in that each instance of the superclass holds a fixed variant (i.e., subclass), so you can't change the variant without creating a new instance. Clearly, this limitation is necessary for references to stay intact, but I've personally ran into this issue when trying to represent a sum type in an ORM.
> so you can't change the variant without creating a new instance.

Isn't that true of ADTs in all languages? I can't think of a single language with ADTs that lets you change the tag/variant of an existing value.

In Rust [0]:

  #[derive(Debug)]
  pub enum Example {
      Foo(i32),
      Bar(&'static str),
  }

  let mut ex: Example = Example::Foo(42);
  println!("{ex:?}"); // Foo(42)

  let ex_ref: &mut Example = &mut ex;
  *ex_ref = Example::Bar("hello");

  println!("{ex:?}"); // Bar("hello")
Given a mutable reference to a value of enum type, you can replace it with another variant. Or you can swap it out with any other value of the same type, even if the variants are different. This is most commonly used for Option<T>, where you can insert or remove the contained value as long as you have a reference.

The limitation here is that for as long as the mutable reference is live, no other code can access the value. So when you do change the variant, you can't have any other references sitting around that point to the removed subfields.

[0] https://play.rust-lang.org/?version=stable&mode=debug&editio...

This doesn't have anything to do with ADTs. It's because Rust has both in-place variables and references. You aren't changing the variant of an existing ADT, you're replacing the entire ADT value with a new one. That replacement is visible in multiple places because the language allows you to take references to variables (the `&mut ex` expression).

You can accomplish the same thing in C and C++ because they also have in-place value semantics and allow you to take the address of any variable. You can't do that in Java only because Java doesn't let you take references to variables.

> You aren't changing the variant of an existing ADT, you're replacing the entire ADT value with a new one.

'Values' in Rust have no identity, except for their address. They're just a bunch of bytes in a row. What could it mean for a value to exist, except for it to be present at a set place in memory? If I have an instance of a Java class, and I change all the fields, I'd hardly say the instance has been replaced with a new instance.

If you insist, I'd say "Java's sealed classes are more limited in that when you have a bunch of references to the same thing, you can change the values in the fields of that thing (and have it be reflected in other references), but you can't change which variant it is." Call that thing a 'value' or a 'variable', it doesn't change the visible outcome compared to enums in Rust, or discriminated unions in C/C++.

I don't really think it's useful to do that though?? Can you give an example?

By the way, I would claim Java's sum types are less limited than Rust because in Rust, variants don't have their own type. The consequence is that you can't have functions that only accept some variant, as far as I know (I remember having this problem once), or add "methods" only to one variant... while in Java, because variants are just normal types, you can do both, and doing that is pretty damn useful.

> I don't really think it's useful to do that though?? Can you give an example?

For an exercise, I had to write a system with Admins and Customers, with the ability to upgrade a Customer into an Admin, or vice versa.

My thought was to put them as two subclasses under a User superclass, so that I could put them under a single Users table, and not have to link and unlink things over the conversion. Hibernate ORM supports storing subclasses by adding an implicit discriminator field.

However, its object model specifies that a single row always corresponds to a particular instance, so it has no support for changing the subclass of a row. Ultimately, I ended up with a hacky solution of creating a new record with the primary key copied over.

> By the way, I would claim Java's sum types are less limited than Rust because in Rust, variants don't have their own type. The consequence is that you can't have functions that only accept some variant, as far as I know (I remember having this problem once), or add "methods" only to one variant... while in Java, because variants are just normal types, you can do both, and doing that is pretty damn useful.

At least in Rust, you can simulate this pretty trivially by having each variant store a struct value with all the data and methods you want. See proc_macro::TokenTree [0] for an example of this. Of course, it's not ideal in how verbose it is (though not much worse than Java!), but it can be workable on the consumer's side if you add some extra From impls.

[0] https://doc.rust-lang.org/proc_macro/enum.TokenTree.html

Java sum types work but still need a bit of syntax sugar on the declaration side, IMHO.
Java doesn't have pattern-matching yet. Haskell is not an imperative language.
Your examples, on the TIOBE index, are #4, #18, and #28. https://www.tiobe.com/tiobe-index/
Product types are one kind of algebraic data type, only 5 languages from that TIOBE page don’t have them so most common langs have ADTs
When you have natural numbers and a multiplication operation (product) would you say that those form an algebra? (ADT means algebraic data type)
You’ve got both a carrier (the set of natural numbers) and a morphism (product operation) so yeah you have an algebra.
NAND is a universal circuit primitive because it can be used to create all of the other circuit primitives. But if you think about it, this is more of an argument of manufacturing than it is in comprehensibility. Only needing to manufacture NAND is easy, but if you could only create your circuit this way, then you would have an unmaintainable mess.

You can do the same thing with boolean logic and just have not-and, but thankfully we have and, or, not, xor. Similarly, you don't need greater-than-or-equal because you can just write 'x > y || x == y'.

Comprehension is linked to how closely you can express the idea of what you're doing in the object language that you have to look at. It might be convenient to compile everything down to SK combinators so that your optimizer and evaluator can be simpler, but people should never look at that level (at least not until you suspect a compiler defect).

So we get to object oriented programming. Where our data expression has an AND property (a class has an INT field AND a STRING field), an existential property (interfaces: there exists some object with these methods), and inheritance (a truly bizarre feature where we duck tape subtyping to a method and field grouping mechanism with a bunch of hooks).

With interfaces and inheritance you can simulate both a universal property (generic) and an OR property. But because it's not a direct expression, we leave this giant gap for what people intended to happen to diverge from what actually happens. Especially after time passes, defects are found, and requirements change. [For example, when using interfaces to simulate an OR property, there really isn't any mechanism to let everyone know that this construct is closed. So if something erroneously gets added, you won't know to check the entire code base. And if requirement change and you need to add a new case, then you have to check the entire code base. Completeness checking of ADTs give you this for free in your pattern matches.]

Too many non-trivial architectural messes that I've encountered in my career have been due to either someone trying to solve all of their problems with interfaces or the same with inheritance* when a simple OR data structure would have made everything simple, clear, and correct.

[*] - Inheritance being more problematic when someone tries to create a non-trivially sized category hierarchy, which ruins the day when requirements change and suddenly the tree needs to be reorganized but doing so would invalidate entire swaths of the code base already accepting types with a different assumed (and undocumented) hierarchal tree structure. Thankfully most people have gotten the memo and switched to interfaces.

ADT feels like an unfortunately acronym-collision with "Abstract data types."
Yep, people that eventually buy a Modula-2 ADT book, when hunting old stuff, are in for a surprise. :)
It's a term that is commonly used in computer science education to refer to any data type independent of its concrete implementation (so, basically, its interface.) I don't think it's just restricted to Modula-2?
Indeed, however CLU and Modula-2 were the ones used mostly for practical teaching purposes, until ML became widespread enough for ADT to gain yet another meaning.
I just realised something terrible.

This code is ADTs for C.

At some point somebody's going to call it CADT and jwz will explode.

More often than not, when I say ADT to someone outside of the FP world, they assume I mean abstract data type.
Or the company that sells the security stickers, for houses.
ADT feels like an unfortunately acronym-collision with "algebraic data types."

They were both introduced in the same decade.

> [Algebraic Data Types are] such a basic mental model of how humans think and solve problems

I think that's actually wrong for "Sum types". Product types, sure. The idea of storing a bunch of fields in a single thing matches the way we've been organizing information since we started writing things down.

But I genuinely don't think I've seen an attempt at a sum/union/enumerant/whatever syntax in a programming language that wasn't horrifyingly confusing.

Where by extension: class-based inheritance is actually pretty simple to understand. The classic "IS-A" relationship isn't as simple as "fields in a struct", but it's not hard to understand (c.f. all the animal analogies), and the syntax for expressing it is pretty clean in most languages.

Is it the "best" way to solve a problem? Maybe not. Neither are ADT sum types. But I think there's a major baby-in-the-bathwater problem with trying to be different. I really don't think, for the case of typical coders writing typical code, that ADTs are bringing as much to the table as the experts think.

> But I genuinely don't think I've seen an attempt at a sum/union/enumerant/whatever syntax in a programming language that wasn't horrifyingly confusing.

Syntaxes like: `A | B(Int) | C(String)`. That means A, B, or C.

> Where by extension: class-based inheritance is actually pretty simple to understand. The classic "IS-A" relationship isn't as simple as "fields in a struct", but it's not hard to understand

This value is either `A`, an Int (`B(Int)`) or a String (`C(String)`). Or: this knapsack either contains an A, B, or C. Difficult?

> (c.f. all the animal analogies),

Reminds me of the fact that software isn’t typically as static as animal taxonomies.

> and the syntax for expressing it is pretty clean in most languages.

I’m most used to Java where you spread `extends` over N files. (Sealed classes in Java is an exception.)

It’s fine. I don’t understand how it is particularly clean.

> Is it the "best" way to solve a problem? Maybe not. Neither are ADT sum types.

Is this an argument? ’Cause I don’t see it.

> But I think there's a major baby-in-the-bathwater problem

Inheritance is something that needs to have some concrete implementation affordance. Baby in the bathwater? I don’t see how you bolt this onto the struct model in a way that gets out of the way for people who don’t want to use it (the zero-cost-abstraction (in the if you don’t use it sense) is important to some low-level languages).[1]

Maybe the designers of hypothetical language X thinks that algebraic data types is enough. What baby are you missing then?

[1] For algebraic data types: structs are straightforward enough. While the “sum type” can be implemented by leaving enough space for the largest variant. That one-size-fits-all strategy isn’t perfect for all use-cases but it seems to have been good enough for Rust which has a lot, a lot of design discussions over minutiae.

> trying to be different.

With a technology from the ’70s. I also saw your “one oddball new idea is a revolution” snark. You’re clearly being very honest.

FWIW, I'm talking about the pattern matching syntax required to enforce coverage of all the sum types. Obviously the idea of the representation as a set of alternatives is easy enough to understand. The idea of "Now You Have To Show The Compiler You Know What To Do In All Circumstances" is very much not, c.f. how much basically everyone struggles with Option() in Rust.

Inheritance addresses similar issues by delegating the responsibility, something that is easy to understand. Does it have downsides? Sure. But I'm not sold that ADT has the advantage here at all.

> But I genuinely don't think I've seen an attempt at a sum/union/enumerant/whatever syntax in a programming language that wasn't horrifyingly confusing.

  data Bool = True | False
> class-based inheritance is actually pretty simple to understand

Simple to understand, a nightmare to debug, as you'll be chasing where your data and data contracts across a ton of files.

That's a bit much. Type inheritance has been a core abstraction in software development since before most working developers were born. We as a society know how to do this. The idea that one oddball new idea is a revolution that turns a "nightmare" into sunshine is way too hyperbolized.

Sum typing might be better! But frankly the jury is still out, and the impact is clearly going to be smaller than what you're imagining.

The new lowest level pls (zig, rust) have ditched class based inheritance. Higher level PLs are going more functional, to include JS, where entire frameworks are encouraging functional (not to mention how everyone complains about the opacity of trying to use inheritance in place of declarative i.e. Amazon CDK)
Yeah, when I first learned Haskell a million years ago, and Erlang slightly less than a million years ago, the pattern matching was so plainly obviously the "correct" way to do things; it just felt like it was exactly how I thought about problems, and all the constructs with if/switch/enums had been an attempt to force my brain thinking into something that executes.

It honestly does annoy me that a lot of mainstream languages still haven't really adopted ADTs; when Java 8 added a lot of (well-needed) new syntax, it felt like that was an ideal opportunity to add ADTs and pattern matching (though I'm sure that was easier said than done).

> when Java 8 added a lot of (well-needed) new syntax, it felt like that was an ideal opportunity to add ADTs and pattern matching

Well at least Java does now (as of Java 21) have pattern matching (including nested record destructuring) and sealed classes, which let you have decent sum types.

The one issue is that everything is nullable, but that's a wider Java issue.

Yeah, but the annoying part of Java is that people stick with old versions for a long time. Java 21 looks pretty great but most companies are still using Java 17, or even Java 11 still.
Some have even older Java versions in 'prod'. 6 is still alive in some places out there, because management refuses to pay for either upgrade, replacement or dismantling.

I have a mid-sized application I built on 17 that's used to deliver a particular project, really looking forward to finish the project so I get to move to 21 and refactor it with these new features and make it more general.

Oof, I didn't know that anyone still used Java 6 anywhere. I have to think that it's a potential security nightmare at this point isn't it?
It is. We run an old version of our application on Java 6. Apparently multiple people have tried to upgrade it in the past but it proved too difficult because it's using a bunch of completely obsolete technology with little available documentation that seems to randomly break when you upgrade even minor things.

The plan has been to gradually replace it with the new version of the software (which runs on Java 17), unfortunately this plan has been ongoing for 10 years and it's not gonna be done anytime soon.

Such are the sad realities of working on legacy code sometimes.

Absolutely, so you try to seal it in hermetically, only talk to it over a special message bus or something.

Sometimes the upgrade path from 6 onwards isn't as nice as it usually is from 8 up, especially if you built with some old undead libraries that require an heroic effort to understand well enough to reimplement. Takes a very special organisation to divert some person-years to pull it off, and as some middle or other manager it's highly unlikely to catch you a new, better job even if it goes well.

"Haskell is the best imperative language," (C) various software engineers.

Also, algebraic data types can be seen as hierarchy consisting of abstract base class and several final children classes. So it is an inheritance model, just restricted one.

I'm curious on supporting evidence for it being a basic mental model of how humans think? That sounds like a fairly strong claim.
I'm a huge proponent of ADTs being a more comprehensible way to write code than some of the alternatives. But I do have to agree with you that there isn't really evidence that this is a basic mental model.

However

What we do see is a bunch of mathematical disciplines that end up creating properties like: AND, OR, Universal, Existential, Implication, (and a few others). They end up in places like: set theory, type theory, category theory, various logics, lattice theory, etc.

Now, maybe they're only copying one another and this is more of a memetic phenomena. Or maybe they've hit upon something that's important for human comprehensibility.

That would be the 'evidence' of the positive effect of ADTs (scare quotes because it might just be math memes and not fundamental). But we can also think about what I feel is legit evidence for the negative effect of lacking ADTs.

Consider what happens if instead of having the standard boolean logic operators and, or, not, xor, we only have the universal not-and operator. Now a straightforward statement like: A && B || C becomes (((A !& B) !& (A !& B)) !& ((A !& B) !& (A !& B))) !& (B !& B) [I think...]. It's more complicated to tell what's actually supposed to be going on AND the '&&' simulation can get intertwined with the '||' simulation. The result being that requirements changes or defect fixes end up modifying the object level expression in a way where there is no longer any mapping back to standard boolean logic. Comprehensibility approaches zero.

And we've seen this happen with interfaces and inheritance being used to implement what would otherwise be a relatively simple OR property (with the added benefit that pattern matching ADTs often comes with totality checking; not something you can do with interfaces which can always have another instance even up to and including objects loaded at runtime).

Appearing in symbolic reasoning tools we have invented doesn't really support them being how brains work, though? This is akin to saying that gears are how nature works because gears are everywhere in how we build things. I could maybe buy that with "friction" being a fundamental thing, but feels like a stretch for the other.

Now, I should add that I did not mean my question to be a criticism of them! I'm genuinely curious on evidence that they are a basic building block. Feels save to say they are a good building block, and those aren't the same thing.

As an easy example for them not being basic building blocks, I can't remember ever seeing anything like them in any assembly instructions for things. Put together a batting net for the kids. Lots of instructions, but nothing algebraic, in this sense. Looking at recipes for food. Nothing algebraic, really? Maybe I can squint and see some, but it would be hard. Exercise plans? Music lessons? Playbooks for a sport?

Again, though, I /do not/ intend this as a criticism of them. Genuinely curious on any investigation into them.

I think you're right to point out that it's too strong a claim to say that sum types are a basic building block of thought, although I believe they are very useful in coding regardless of that claim.

There is the still the ongoing debate about how much human perception and human reason are shaped by cultural forces vs. universal forces (where the latter asserts humans reason in the same/similar ways).

There's evidence that certain optical illusions don't work across cultures for example (I seem to remember those in Western countries have a tendency to mentally group things in rectangular boxes). The exact balance between cultural and universal forces isn't known and I doubt we could say anything about sum types in that regard.

Verdex's explanation is detailed but too long IMO. The short version is that ADTs/sum types formally correspond to the OR-logical connective, and records/product types formally correspond to AND-logical connective. I think you'd be hard-pressed to argue that people don't think in terms of "X AND Y OR Z". These are core primitives of any kind of reasoning.
I can easily argue that people don't think in terms of boolean logic. For one, the evidence seems as strong that people generally think backwards from the answer far more than they do forwards from the ingredients. This is often why new things are so long to be discovered. It isn't that people couldn't have gotten there, but they didn't know to go for it.

For two, addition is a wildly disparate thing everywhere we use it. We like to joke that computers made that hard, but literally half of intro chemistry is learning how to get thing to add together in a meaningful way, no? Balancing a chemical equation is a thing.

> For one, the evidence seems as strong that people generally think backwards from the answer far more than they do forwards from the ingredients.

Logic doesn't really have a direction, it works backwards or forwards. Even if you're solving a system "backwards", whatever that means, you still have to satisfy all of the necessary AND and OR constraints for a solution to be valid, so you're effectively still building ADTs or records just using a different evaluation order.

And this logic is how folks convince themselves that ball players are doing trigonometry when playing. It is just wrong.

You can /model/ it that way. But you are making a symbolic model to justify how a solution is reached.

Now, it can be frustrating to consider that this model could produce an agent that is better at the ball game than the players. But it is silly to think that means you have mirrored them.

> And this logic is how folks convince themselves that ball players are doing trigonometry when playing. It is just wrong.

You're attempting a sleight of hand here by saying "they're" not "doing trig". Clearly they are not doing anything like that consciously, but equally clearly some part of their brain is triangulating objects and predicting trajectories based on gradients, meaning that part is "doing trig and calculus" subconsciously. What else does it mean to "do something" if not "process X is isomorphic to process Y"?

> You can /model/ it that way. But you are making a symbolic model to justify how a solution is reached.

I really don't understand what you think people are doing when they're compiling a grocery list. They're clearly thinking, "I need x AND y OR I can substitute z".

Or if they're planning to paint their fence, they're thinking, "I need paint AND brushes AND I have to start before lunch OR I won't finish before dinner".

> Logic doesn't really have a direction, it works backwards or forwards.

Implication is one of the primitives in logic, and gives us several of the classic logical fallacies: affirming the consequent, denying the antecedent, fallacy of the converse, and fallacy of the inverse.

All of which are examples of trying to work logic as though it doesn't have a direction.

Implication is not primitive, "x->y" is reducible to "not(x) or y". None of those fallacies derive from a lack of direction, but from not being invalid logical deductions.
> except maybe Rust

Swift, Kotlin and Scala all have had ADTs for a while, even Java has it now.

Pascal has had variant records since the 1970s.
But Pascal's variant records (1970-11) had very ugly design errors in comparison with the unions of Algol 68 (1968-12), which made them either useless or annoying for most applicatons.

Niklaus Wirth is well known as a critic of Algol 68 (before the design of Algol 68 was finalized), but in the case of his variant records he has completely failed to create something competitive.

Doesn't look too bad particually the match tagged version last:

  MyType= (Scalar4, Real4, NullTerminatedStringC);
  
  MyUntaggedRecType=
      RECORD
      CASE MyType OF
      Scalar4: (longC:  ARRAY[1..150] OF longint);
      Real4:   (floatC: ARRAY[1..150] OF real);
      END;
  
  MyTaggedRecType=
      RECORD
      CASE tag: MyType OF
      Scalar4: (longC:  ARRAY[1..150] OF longint);
      Real4:   (floatC: ARRAY[1..150] OF real);
      END;
  
  ...
  
  { set all to 0.0 without running through the MC68881 }
  FOR j := 1 TO 150 DO
      longC[j]:= 0;
  
  ...
  
  CASE tag OF
      Scalar4: percentReal = longC[1];
      floatC:  percentReal = floatC[1]*100;
  ELSE
      percentReal = 0.0/0.0;
edit: don't have a pascal compiler handy, but that's the idea
Given where Algol 68 ended up, I would say Wirth was quite right.
Algol 68 was a failure mainly due to its inappropriate documentation, not due to the quality of the language.

It included many innovations that appeared again in other programming languages only decades later.

Niklaus Wirth was a good teacher and writer and the success of his languages is due mostly to his books and due to his languages being used for teaching in many universities, not due to their technical qualities.

I beg to differ, after Modula-2 and Object Pascal (Apple designed it with Wirth's feedback).

Most of his languages ended up losing, because they weren't being shipped with a free beer OS, coupled with a free beer compiler.

Algol 68 may have been a failure because it was ahead of its time, which meant it was hard to write a compiler for it. For example, Algol 68 had closures (apparently Knuth snuck them in).
> not due to their technical qualities

AFAIK Pascal is C and Algol 68 is C++

people used Pascal because the compiler was blazing fast, it was easier to implement and learn and the features it lacked against Algol did not really matter most of the time (at the time)

More features doesn't automatically means "better"

Also Pascal had quite strong technical qualities, not very common among other contemporary languages

edit: can I ask the reason for the downvote? I would really like to hear an opinion on what Pascal did wrong, having used it extensively in the late 80s until the end of the 90s and why my comment was awarded with a negative score.

Extended variants of Pascal, like Turbo Pascal, should not be confused with the Pascal language as designed by Niklaus Wirth.

Wirth's Pascal was a language designed for the purpose of teaching programming and it was adequate for that, but it was completely inappropriate for any serious work.

It had no means for writing big programs that must be divided into multiple source files and it had a lot of design errors, like including the size of an array in its data type (which made impossible the writing of any linear algebra library) or the way of handling the variant records (which was insecure and verbose).

The well known paper "Why Pascal Is Not My Favorite Programming Language" by Brian W. Kernighan contains valid arguments against Pascal (against Wirth's Pascal, there have been many extended Pascals, especially following Turbo Pascal, which have corrected some of the most egregious defects of the original Pascal).

Isn’t ADT abbreviation for Abstract Data Type? Or does it depend in context nowadays?
Context. It means algebraic data type here.
You answered your own question: it depends and the context and is confusing imo. Both are very common in compsci
No I didn’t, I provided two options and asked which case it was. I could’ve assumed OP had used the wrong abbreviation.
Wait until you switch to unions in rust and ask yourself whether it is a union or a struct.
Oh dear
C has always had them, it's called union.

In practice you need to couple it with an enum, and your visitation mechanism is a switch statement. But C doesn't impose that on you and lets you do it as you see fit.

You're confusing semantics for implementation. The point of union and discriminated union types (not what C calls union) is to enable compiler checked pattern matching, which tagged enums in C plus a switch statement do not get you.
>C has always had them, it's called union

It also has all the features of Haskell, since you can implement a Haskell compiler in C.

Tagged unions + pattern matching is what gp wants. You can always encode whatever model you want using any programming language, but language features/ergonomics matter.
That you can sort of simulate the skeleton of algebraic data types does not mean that C has algebraic data types. The whole point of the algebra part is that the syntax has a compositional semantics which is completely absent in C, unless you go to great lengths as with this macro header.
lol this is like saying C doesn't need structs, you can just declare the variables with a common prefix separately! See ma, product types!
Yep, or C doesn't need arrays just use pointers! And C doesn't need strings, just use a pointer to the first byte and assume it's ASCII!
That doesn't work since you cannot pass that as a single argument to a function.
So? make the function accept multiple arguments?
There are restrictions on calling conventions; it's not equivalent.
That is not a proper alternative to real pattern matching.
May not be built in but many mainstream languages such as typescript have libraries or the tools to easily implement them.