Hacker News new | ask | show | jobs
by rbehrends 3219 days ago
> It’s better to compose than inherit

I know that this is just a restatement of the "composition, not inheritance" mantra in Go, but it still makes about as much sense as "product types, not sum types". A more meaningful statement would be: "use inheritance to express sum types, use composition to express product types." There's no "better" relation between the two concepts, each has its own distinct purpose.

Yes, inheritance has traditionally sometimes been abused to implement a limited form of product type (for example, to work around Java's lack of proper value types), but that's a misunderstanding of what inheritance is used for.

> Go doesn’t have the concept of inheriting structs by design.

Go has automated delegation, and as we know, (automated) delegation IS inheritance [1, 2]. Some implementations of delegation are a bit more limited, some are a bit more expressive, but fundamentally they have the same purpose.

[1] http://dl.acm.org/citation.cfm?id=38820

[2] http://dl.acm.org/citation.cfm?id=900985

4 comments

'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].

[1] http://caml.inria.fr/pub/docs/manual-ocaml-400/manual006.htm...

[2] https://stackoverflow.com/questions/16773384/why-does-ocaml-...

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 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.

>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.

Taking this example (in English / pseudocode):

Define a class Animal.

Define Dog as a subclass of Animal.

Define Cat as a subclass of Animal.

Case 1) Now if we have a variable a1 that can, at runtime, contain (or refer to) either a Dog or an Animal instance.

Case 2) And if we have a variable a2 that can, at runtime, contain (or refer to) either a Dog or a Cat instance.

Referring to your quoted sentences above, would you say that Case 1, Case 2 or both, are about sum types?

Just trying to understand the terminology.

I'm thinking you just violated the Liskov substitution principle (https://en.wikipedia.org/wiki/Liskov_substitution_principle).

A sum type is the same thing as a tagged union, a variant record, or a discriminated union (https://en.wikipedia.org/wiki/Tagged_union).

I'm not sure if you can really express it in OO. Sum types are used to represent a closed set of variants. Inheritance like that isn't closed.

In languages that have both OO and ML/Haskell-style type systems like Scala, the fact that the set is closed is denoted by the keyword 'sealed'.

example

sealed trait Color

final case object Red extends Color

final case object Green extends Color

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.

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.)

Animal is a sum type. It's definition is:

    Animal = Cat | Dog
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.
What languages are you familiar with? It might help with explaining the concepts.

Since we're on a Go thread, I'll point out that Go's structs and tuples are both examples of product types.

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?
Ponder.
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.

What wikipedia page did you look at? This has plenty of practicality

https://en.m.wikipedia.org/wiki/Sum_type

"but that's a misunderstanding of what inheritance is used for"

I'm not sure what you imply. Most OOP languages where people tell you not to use inheritance, but composition instead, so java, c++, c#, etc. In those languages inheritance can be used to create forms of product types, but also to share and override behaviour hierarchically. They can also create sum types, and all possibility of hybrids, like weird mix of sum and product types, partially closed, etc.

By allowing inheritance to do all this, I say it doesn't matter what the designer of these languages intended inheritance to be used for, the truth is that it allows for much more, and so best practices have been put in place to help programmers not use the construct in troublesome ways. One of those is to use composition instead.

Can you simulate closed sum types with inheritance, yes, but they are not the same thing clearly, since closed sum types can not emulate all usage of inheritance (the kind of java).

Maybe you're right, a closed sum type pattern could be created and evangelized, having an abtract class with no fields and a set of methods, then having a one level inheritance hierarchy where each subclass has its own disjoint set of fields, and overrides all methods to work on its fields. But I already feel like in practice this sounds like a nightmare. Too much good intentions are needed to maintain this, its too easy to create a degenerate case of it.

> I'm not sure what you imply. Most OOP languages where people tell you not to use inheritance, but composition instead, so java, c++, c#, etc. In those languages inheritance can be used to create forms of product types, but also to share and override behaviour hierarchically. They can also create sum types, and all possibility of hybrids, like weird mix of sum and product types, partially closed, etc.

My point wasn't to give an exhaustive list of use cases for inheritance (which would require a small essay); I was pointing out that "composition over inheritance" is a nonsensical statement, just as (say) "loops over modules" would be, as it's a qualitative comparison of orthogonal concepts.

Okay, but its not, not from the perspective the best practice comes from. The most common use cases for object inheritance can be delivered with object composition instead. This is much more like loop vs recursion.
The dominant use case for inheritance is polymorphism, which composition cannot do.
You'd use an interface for polymorphism, much better.

That said, I have to thank you for your suggestion of doing sum types with inheritance. I hadn't thought of it, and the use case presented itself at work yesterday. So I learned something new, thanks. It worked well, an abstract class with shared fields, and a derived class for every type in my sum type with distinct fields added to them. Now a variable of the abstract type is effectively constrained to one of the set of its derived children. Limit this to one level and you've got a pretty nice simulated sum type. Just need to remember to handle all cases when working with it. It worked like a charm, wouldn't have thought of it without your comment.

> You'd use an interface for polymorphism, much better.

Interface inheritance is simply the special case of inhering a purely abstract class. I've never seen a good argument why restricting abstract classes to purely abstract methods is worthwhile and several that speak against it [1, 2]. Languages that separate implementation and interface inheritance (such as Sather) have been tried, but never caught on, because it just leads to a lot of code duplication in order to write the interface twice. It can be useful to have inferred interfaces (as in Dart or OCaml), but there's nothing inherently better about using interfaces over more general abstract classes.

A common use case is to represent types of the form `T * A | T * B`, which (without implementation inheritance) just leads to code duplication for `T` or extra destructuring efforts (if you turn it into a representation of form `T * (A | B)`).

[1] Example 1: it gets in the way of doing Design by Contract as part of the interface of a class, even if you want interface-only inheritance.

[2] Example 2: Abstract classes with significant implementation parts show up all the time in design patterns. (Note that this is not about whether design patterns are good or bad, just that they reflect observed common practice).

> Go has automated delegation, and as we know, (automated) delegation IS inheritance [1, 2]. Some implementations of delegation are a bit more limited, some are a bit more expressive, but fundamentally they have the same purpose.

No it is not inheritance, and Go doesn't have automated delegation, it has type embedding.

Given struct A, if a function requires A, you can't pass any type B that embeds A, you must pass A.

    type A struct {
        Foo int
    }

    type B struct {
        A
    }
func acceptA(a A}{} // you can't pass B here
> No it is not inheritance, and Go doesn't have automated delegation, it has type embedding.

Different names for the same thing.

> func acceptA(a A}{} // you can't pass B here

This says that the function isn't polymorphic in its argument, not that the types aren't polymorphic. Function resolution that is not polymorphic is not limited to Go, but occurs in inheritance-based languages, too. Example in OCaml:

  class a = object method foo = 0 end
  class b = object inherit a method bar = 1 end

  let f (x: a) = ()
  let () = f (new b)
You will get an error that the type of `new b` (= `b`) is not compatible with `a`, because they're not identical, even though `b` is a subclass of `a`.

If you replace the declaration of `f` with:

  let f (x: #a) = ()
it'll work, because `#a` denotes a polymorphic type, matching `a` or any subclass of `a` (same as though you'd specify an interface in Go [1]). You can also cast the type explicitly to `a` to work around the error.

[1] Like Go, OCaml uses structural subtyping.

> Different names for the same thing.

No, different names for completely different concepts.

> This says that the function isn't polymorphic in its argument, not that the types aren't polymorphic. Function resolution that is not polymorphic is not limited to Go, but occurs in inheritance-based languages, too. Example in OCaml:

Struct types in Go are not polymorphic in anyway period, the only way to achieve polymorphism in Go is through interfaces which are not concrete types, unlike classes.

> [1] Like Go, OCaml uses structural subtyping.

No it doesn't. There is no subtyping in Go. There is only type conversion and type assertion.

Whatever you wrote with OCaml is completely irrelevant to the discussion as the type systems are fundamentally different. but let's pretend it is.

It's interesting that you didn't bother try writing the equivalent of `let f (x: #a) = ()` in Go, because you CANNOT. An interface IS NOT a substitute for OCaml inheritance, as the later is more precise and specialized.

So no, Go doesn't support inheritance at its core. That's a false assertion. Go interfaces do not give a damn about what the actual implementation is, unlike OCaml sub classes.

> There is no subtyping in Go.

There is. Go simply has structural subtyping [1] rather than nominal subtyping.

> It's interesting that you didn't bother try writing the equivalent of `let f (x: #a) = ()` in Go, because you CANNOT.

You forget that OCaml also uses structural subtyping. Writing `#a` is effectively the shorthand for the inferred interface. So you can write it also as:

  let f (x: < foo: int; .. > ) = ()
where

  < foo: int; .. >
is the interface of any class implementing at least a method `foo` of type `int`, i.e. what you'd write as

  interface {
    foo() int
  }
in Go. You just don't in practice, because `#a` is both more convenient and readable.

And the corresponding Go function would be:

  func f(x interface { foo() int }) {
  }
[1] https://en.wikipedia.org/wiki/Structural_type_system
> There is. Go simply has structural subtyping [1] rather than nominal subtyping.

No there is not, period.

  interface {
    foo() int
  }
is not sub typing. but it's interesting how you move the goal post on each comment. You go from inheritance to sub typing to "structural subtyping". You're not interested in a serious discussion.
> No there is not, period.

This is an assertion, not an argument. It's not how the literature sees it. Plus, you can even have it from Rob Pike himself if you don't believe me: https://twitter.com/rob_pike/status/546973312543227904

> is not sub typing. but it's interesting how you move the goal post on each comment.

I didn't say that this piece of code constituted subtyping. Here, I was refuting your specific claim that the equivalent of `f` cannot be written in Go.

> You go from inheritance to sub typing to "structural subtyping".

This is not how the thread went. I added a reference to structural subtyping as a purely explanatory footnote to illustrate a well-known similarity between OCaml and Go; subtyping had not been mentioned at all so far. Starting at that footnote, you introduced a digression by claiming that Go does not have subtyping at all.

What affordances does Go even have for real composition though?