'A more meaningful statement would be: "use inheritance to express sum types, use composition to express product types."'
I'm not sure how well that works as a meaningful statement, because it assumes that the reader has a solid grasp of what a sum type and a product type are. If not (and I make the perhaps dubious assumption based on my own experience and knowledge that most programmers, especially most people using mainstream languages like Go or C++ or Python will not) then it doesn't really convey any information, unfortunately. I think a lot more people will know roughly what inheritance and composition are.
> I'm not sure how well that works as a meaningful statement, because it assumes that the reader has a solid grasp of what a sum type and a product type are.
I think you're misparsing what I mean by "meaningful" here, which is simply a sentence or phrase having (more) meaning, not that it's easy or easier to understand. It's not about being more comprehensible, it's about being more "accurate" (I avoided the terms "accurate" and "correct", because they imply an absolute objectivity that you generally don't have when talking about software engineering concepts).
>I'm not sure how well that works as a meaningful statement, because it assumes that the reader has a solid grasp of what a sum type and a product type are.
That's true about all statements: they assume some prior knowledge from the one hearing them. That doesn't make a statement less meaningful -- just more demanding. Whether a statement is meaningful or not is an orthogonal concept (to it requiring prior knowledge).
That's ... sad. Sum types and product types are simultaneously easier to understand and are more useful to know about than inheritance.
Sum types are simply types whose values can be one of several choices - surely that's something a child can reasonably understand!
Product types might be a little harder to grok - they're essentially structs - but surely no harder to understand than inheritance and the whole IS_A/HAS_A mess
No, sum types and product types relate to values, inheritance and composition relate to objects, which include values AND behavior. Inheritance is therefore abused to share behavior. This distinction is important, and relates to some common OOP patterns of popular languages.
In that model, composition of behaviour over inheritance is definitly important to understand, as overriding behaviour gets tricky quickly, and functionality is spread accross many class in the inheritance chain, it becomes difficult to follow and see the full picture.
> No, sum types and product types relate to values, inheritance and composition relate to objects, which include values AND behavior. Inheritance is therefore abused to share behavior
I remember back in high school or early college I was having a conversation with a programmer about when the right time to use inheritance is, and he stated that "inheritance should be used for polymorphism, not just to share code", which to this day I think it's a fairly good heuristic for determining whether or not inheritance is the right tool for a given problem.
Inheritance is not a way to express sum types, it's a form of subtyping. A sum type is like a discriminated union, it can only be one thing at a time. Subtyping allows a value to have multiple (related) types simultaneously, which is much more expressive. I suppose you can use one level of single inheritance to emulate a sum type, but you could just as well emulate it with a discriminated union in Go, e.g. a struct with a type identifier and an interface{} holding the value.
> Inheritance is not a way to express sum types, it's a form of subtyping.
A distinction without a difference. This is probably most visible in languages like Scala and Kotlin, which implement algebraic data types by way of inheritance.
That inheritance creates a subtyping relationship is irrelevant; there's a similar subtyping relationship between variants (or groups of variants) and the overarching type using a traditional sum type notation as in Haskell or ML. This is most clearly visible in OCaml's polymorphic variants [1, 2].
Pony (https://ponylang.org) uses sum types, perhaps excessively. Just yesterday, I wrote:
(None | (In, USize))
I.e., None (the null valued type) or a pair made of a type variable (In) and a USize.
The thing is, the values that satisfy this type are not subtypes of None and a pair. (That would be silly, given None.) Such a value is either None, or a pair.
> The thing is, the values that satisfy this type are not subtypes of None and a pair. (That would be silly, given None.) Such a value is either None, or a pair.
Unless I'm misreading you, this seems to be a misunderstanding of what sum types are. A sum type `T = A | B` represents the (disjoint) union of all possible values of `A` and of all possible values of `B`, simply put, not the intersection (as you seem to indicate by the phrasing of "not subtypes of None and pair"; correct me if you meant something else).
Recall what subtyping means (I'm going with Wikipedia's definition here for sake of accessibility):
> [S]ubtyping (also subtype polymorphism or inclusion polymorphism) is a form of type polymorphism in which a subtype is a datatype that is related to another datatype (the supertype) by some notion of substitutability, meaning that program elements, typically subroutines or functions, written to operate on elements of the supertype can also operate on elements of the subtype.
This holds in the case of sum types. Operations that work on the sum type will generally also work on the variants that constitute the sum type.
The same goes for inheritance. If an abstract class T has two concrete subclasses `A` and `B`, then a value of type `T` belongs to the union of values of type `A` and of type `B`.
Not true. You can model sums with inheritance along with an exclusivity constraint, but it's a weird model and subtyping is more general. Further, the idea of each variant being its own type is inherently a subtyping sort of idea. Sums don't give names to their conponents, only distinctions.
> You can model sums with inheritance along with an exclusivity constraint, but it's a weird model and subtyping is more general.
You don't need an exclusivity constraint. Exclusivity is purely a modularity concern; you will still have a finite number of variants in any given software system; traditional ADTs and exclusivity just limit the declaration of variants to a single module. See also "open sum types" vs. "closed sum types", because it can be beneficial to have extensible sum types [1]. Not all sum types are closed; see polymorphic variants and extensible variants in OCaml.
Also, do not confuse the language mechanism used to specify a type with the type itself.
I do agree that inheritance is a generalization of algebraic data types.
> Further, the idea of each variant being its own type is inherently a subtyping sort of idea. Sums don't give names to their conponents, only distinctions.
Try polymorphic variants in OCaml (mentioned above); or GADTs:
# type _ t = Int: int -> int t | String: string -> string t;;
type _ t = Int : int -> int t | String : string -> string t
# Int 0;;
- : int t = Int 0
# String "";;
- : string t = String ""
There's nothing inherent about summands not having a distinct declared type in ML and Haskell, only convention. Obviously, they do have distinct actual types.
Edit: A practical use case is the representation of nodes for an abstract syntax tree. An `Ast` type can benefit from having abstract `Expr`, `Statement`, `Declaration`, `Type`, etc. subtypes that group the respective variants together in order to get proper exhaustiveness checks, for example.
[1] See the question of how to type exceptions in Standard ML; in OCaml, this led to the generalization of exception types to extensible variants.
I'm aware of polymorphic variants and row types and the like. My concern is one of modularity in that I consider a running system pretty dead, extensions during coding are where language features and their logics are interesting. Closing your sums is valuable to consumers: they have complete induction principles, e.g.
Open sums and row types are a little different in that they represent a fixed/closed type but retain enough information to extend it more conveniently and to see it as structurally related to other (open) sums/products. This is no doubt super useful, but I see it more as an application sitting atop polymorphism rather than a fundamental concept.
Finally, I am exactly confusing the language mechanism with the type it intends to model because exactly now we have to think about things as a mechanism and model. This is where breakdowns occur.
Anyway, I doubt there's a real difference of opinion here. I'm very familiar with the concepts you're discussing, but perhaps argue that they are not as fundamental as regular, closed sums/products and language support for those simplest building blocks is important.
I'm finding that wrapping an interface in a struct can be a good technique. However, the interface{} contains a type identifier, so adding another one seems like wasted space. Usually you can compute it using a type switch.
Yeah, that's a perfectly valid solution as well. Depending on the application it may be faster to do the assertion on your type identifier rather than introspecting the type though. And having a list of type options more directly maps to a true sum type. I probably wouldn't actually do it in real Go code if I could avoid it though
If you find yourself not knowing something apparently important, maybe it's worth spending half an hour with Wikipedia and the like to gain the understanding?
Among other things, the idea of a sum type helps understand the nature of the "billion dollar mistake" which is the inclusion of null in C, and why it pops up in other languages, and what more civilized methods of handling it might be. It will help you as a mainstream programmer, too.
I did look at wikipedia, yes, which gave me a page of type theory related stuff without any apparent grounding in practicalities, which gives the conclusion that it's apparently unimportant unless you particularly like mathematical theory. Yes, I could go and research functional programming languages and type theory; but my point is that if you're critiquing a blog post on a non-functional programming language then doing it in terms that only FP advocates will understand is missing the target audience.
A sum type `T = A | B` means that a value of type `T` can be either of type `A` or of type `B`. Such a type is used to express polymorphism.
A product type (from "Cartesian product", i.e tuples) `T = A * B` means that a value of type `T` has a component that is of type `A` and another component that is of type `B`. It is used to aggregate parts into a whole.
> Yes, I could go and research functional programming languages and type theory
It has absolutely nothing to do with functional programming and touches only upon the barest essentials of type theory (and calling it type theory is already stretching it, because it's just about defining a couple of common computer science concepts).
Sum and product types are fundamental computer science vocabulary to such an extent that it's not really possible to have a useful discussion about programming language semantics without them.
>Color can only be Red OR Green. In your example you can have an Animal, a Dog, a Cat, and other things can inherit Animal and create more variants.
True. I used traditional OO style inheritance in my example, and my question was about understanding the relationship between it and sum types, based on rbehrends' comment that I was replying to. Interesting, did not know this. So you are saying that sum types are sort of like inheritance with some limits - pre-defined types only. Sort of like an enum (though those do not have inheritance in them) but for classes or types.
> So you are saying that sum types are sort of like inheritance with some limits
Traditionally sum types don't have inheritance, you have the sum type is a type and the variants (or constructors) are values of that type, but yes you could also model it as a "closed inheritance" hierarchy, in fact some languages do exactly that: Scala with sealed traits and case classes (the trait is the sum type, each class is a variant) and Kotlin with sealed classes.
The boundaries can also get fuzzy the other way around, OCaml has polymorphic variants and extensible variants which blur the line. Polymorphic variants are structural types where multiple sum types share the same value (polymorphic variant types are venn diagrams essentially), not entirely unlike integral types, and extensible variants (as their name denotes) can get cases added to them by users of the base package/type.
> Sort of like an enum
Sum types can be seen as an extension of enums yes, Rust and Swift call them that, in both languages they're enums where each "value" (variant) can carry different data sets (unlike e.g. Java enums where variants are instances of the enum class). Here's a "degenerate" C-style enum with Rust:
On a side note, that syntax seems a little counter-intuitive - the extends word seems to indicate that Red is a subclass of Color, but from what you said seems more like Red and Green are values for Color. So it seems like an enum would be more appropriate here (for this use of Red, Green and Color)?
A Scala “object” declaration declares a singleton; it's an object, but you can also define object-specific class features.
If the instances need behavior (which in practice they often do), this is useful. Presumably, in the example, they would in a substantive program, but the behavior is irrelevant to the illustration and thus omitted.
Both. Assuming that you can actually exclude instances of class Cat in case 1 and of Animal in case 2, that is.
In case 1, the actual type of a1 would be Animal | Dog. In case 2, the actual type of a2 would be Dog | Cat. (Either a1 or a2 might be declared with a different type, this is about the values that they can actually hold, according to your stated premises.)
I personally didn't like the comparison, because inheritance is more powerful (what is not always good), and because algebraic types takes most of their usefulness by merging sums and products on the same type.
Most familiar with Python currently. Done some Ruby and Java and C and Pascal earlier. Some D and a bit of C++ and a bit of Go. I do understand that structs and tuples are examples of product types (because the range of the values for a struct or tuple is the Cartesian product of all the possible values for each field). My question was mainly about sum types as described by rbehrends, was trying to relate them in my mind (somewhat, if it makes sense) to traditional inheritance as in Python, Java or C++.
Cool, so unions in C and C++ are a kind of sum type because the variable can have one of a set of types. More usually people think of tagged unions when they think of sum types, I believe Pascal's Variant Records are an example of tagged unions.
Thanks. Yes, I was thinking on the same lines. Don't remember whether Pascal also has this right now (more time since I used it than C), but in C, being the somewhat more flexible language that it is, IIRC you can also store a value of one type (out of the types of the sum type) in the union, and then read it back out as one of the other types. E.g. define a union with an int (say 16-bit) and a two-char (or two-byte) array, and then write into it as an int, and read it back out as two chars or two bytes. There are uses for such things, though I can't think of a good example of the top of my head. Okay, one use might be hardware interfacing, another might be conversion between big-endian and little-endian data values if you need to roll your own for some reason, I know there are libs for that).
Type theory isn't FP as much as logic--it applies everywhere. That said, totally agree that you're unlikely to have encountered it outside of an FP context in 2017.
Product and sum type are fundamental structures for construction of information. A product of two pieces of information is a piece of information equivalent to having both at once: it's sort of like "and". A sum of two pieces of information is a piece of information equivalent to having exactly one or the other and knowing which of the two you have: it's sort of like "xor".
Remember when you first encountered some seemingly hard topic early in your developer's life. Pointers? Virtual functions? Futures? Pick something from your experience on what you had to spend a couple of weeks to get a grasp.
Now think:
* Was it useful?
* Is it hard, from your current perspective?
A practical example is a function that can return either a value or an error. Those are two different types that are combined into a Sum type called a Result. When you write some code that handles results, you need to account for both possible outcomes or you'll get a compiler error (or a panic).
A product type is basically an object where you have a person type which in includes attributes like 'Name', 'Age', etc.
I'm not sure how well that works as a meaningful statement, because it assumes that the reader has a solid grasp of what a sum type and a product type are. If not (and I make the perhaps dubious assumption based on my own experience and knowledge that most programmers, especially most people using mainstream languages like Go or C++ or Python will not) then it doesn't really convey any information, unfortunately. I think a lot more people will know roughly what inheritance and composition are.