Remix.run Logo
tombert 7 hours ago

"Composition" is a word that can mean several things, and without having read the original source I never really understood which version they mean. As a rule, I've always viewed "composition" as "gluing together things that don't know necessarily know about each other", and that definition works well enough, but that doesn't necessarily eliminate inheritance.

So then I start thinking in less-useful, more abstract definitions, like "inheritance is vertical, composition is horizontal", but of course that doesn't really mean anything.

And at some point, it seems like I just end up defining "composition" to mean "gluing together in a way that's not inheritance". Again, not really a useful definition.

crystal_revenge 7 hours ago | parent | next [-]

I find the Monoid/Semigroup typeclass pretty concisely captures what is generally meant by "composition" in the minimal sense.

> As a rule, I've always viewed "composition" as "gluing together things that don't know necessarily know about each other"

The extension to this definition given the context of Monoids would be "combining two things of the same type such that they produce a new thing of the same type". The most trivial example of this is adding integers, but a more practical example is function composition where two functions can be combined to create a new function. You can also think of an abstraction that let's you combine two web components to create a new one, combining two AI agents to make a new one, etc.

> "inheritance is vertical, composition is horizontal", but of course that doesn't really mean anything.

This can actually be clearly defined, what you're hinting at is the distinction between sum types and product types. The latter of which describes inheritance. The problem with restricting yourself to only product types is that you can only add things to an existing thing, but in real life that rarely makes sense, and you will find yourself backed into a corner. Sum types let you have much more flexibility, which in turn make it easier to implement truly composable systems.

tombert 6 hours ago | parent [-]

I actually knew most of that (I've done a lot of Haskell). I don't really disagree with what you said, but I feel like like you eliminate a lot of stuff that people would consider "composition" but aren't as easily classified in happy categories.

For example, a channel-based system like what Go or Clojure has; to me that is pretty clearly "composition", but I'm not 100% sure how you'd fully express something like that with categories; you could use something like a continuation monad but I think that loses a bit because the actual "channel" object has separate intrinsic value.

In Clojure, there's a "compose" function `comp` [1], which is regular `f(g(x))` composition, but lets suppose instead I had functions `f` and `g` running in separate threads and they synchronize on a channel (using core.async)? Is that still composition? There are two different things that can result in a very similar output, and both of which are considered by some to be composition. So which one of these should I "prefer" instead of inheritance?

Of course this is the realm of Pi Calculus or CSP if you want to go into theory, but I'm saying that I don't think that there's a "one definition to rule them all" for composition.

[1] https://clojuredocs.org/clojure.core/comp

kannanvijayan 4 hours ago | parent | next [-]

I think there's still a category theoretic expression of this, but it's not necessarily easy to capture in language type systems.

The notion of `f` producing a lazy sequence of values, `g` consuming them, and possibly that construct getting built up into some closed set of structures - (e.g. sequences, or trees, or if you like dags).

I've only read a smattering of Pi theory, but if I remember correctly it concerns itself more with the behaviour of `f` and `g`, and more generally bridging between local behavioural descriptions of components like `f` and `g` and the global behaviour of a heterogeneous system that is composed of some arbitrary graph of those sending messages to each other.

I'm getting a bit beyond my depth here, but it feels like Pi theory leans more towards operational semantics for reasoning about asynchronicity and something like category theory / monads / arrows and related concepts lean more towards reasoning about combinatorial algebras of computational models.

anon291 16 minutes ago | parent | prev [-]

The thing about inheritance is it limits you to one relation. Composition is not a single relation but an entire class of relations. The user above mentioned monoids. That is one very common composition that is omnipresent in computation and yet completely glossed over in most programming languages.

But there are other compositions. In particular, for something like process connection, the language of arrows or Cartesian categories is appropriate to model the choices. The actual implementation is another story

In general when you want to model something you first need to decide on the objects and then you need to decide on the relations between those objects. Inheritance is one and there's no need for it to be treated specially. You will find though that very objects actually fit any model of inheritance while many have obvious algebras that are more natural to use

hansvm 6 hours ago | parent | prev | next [-]

"Gluing together in a way that's not inheritance" is useful enough by itself. Most class hierarchies are wrong, and even when they're right people tend to implement th latest and greatest feature by mucking with the hierarchy in a way which generates wrongness, mostly because it's substantially easier, given a hierarchy, to implement the feature that way. Inheritance as a way of sharing code is dangerous.

The thing composition does differently is to prevent the effects of the software you're depending on from bleeding further downstream and to make it more explicit which features of the code you're using you actually care about.

Inheritance has a place, but IME that place is far from any code I'm going to be shackled to maintaining. It's a sometimes-necessary evil rather than a go-to pattern (or, in some people's books, that would make it a pattern like "go-to").

tombert 6 hours ago | parent [-]

I don't think that it really is a useful enough definition. There are lots of ways to glue things together that aren't inheritance that are very different from each other.

I could compose functions together like the Haskell `.`, which does the regular f(g(x)), and I don't think anyone disputes that that is composition, but suppose I have an Erlang-style message passing system between two processes? This is still gluing stuff together in a way that is not inheritance, but it's very different than Haskell's `.`.

hansvm 5 hours ago | parent [-]

But both of those avoid the pitfalls of inheritance. "Othering" is a common phenomenon, and I think it's useful when creating an appropriate definition of composition.

tombert 5 hours ago | parent [-]

But I don't think it's terribly useful; there are plenty of things that you could do that the people who coined the term would definitely not agree with.

Instead of inheritance, I could just copy and paste lots of different functions for different types. This would be different than inheritance but I don't think it would count as "composition", and it's certainly not something you should "prefer".

hansvm 3 hours ago | parent [-]

That's fair. I'd agree that isn't composition. I'm not sure the thing you describe is worse than inheritance.... It's not composition though.

duped 6 hours ago | parent | prev [-]

I have always heard "prefer composition to inheritance" also referred to as "has a" instead of "is a." Meaning:

    class Dog : Animal; // inheritance 
    class Car: 
        Wheels wheels; // composition
creata 5 hours ago | parent [-]

Yep. "Composition" has many meanings, but in the context of "inheritance vs. composition" it's just referring to "x has a y".