Two Types of Composition
Author’s note: I am writing this post so that I can reference it in the near future (famous last words, I know). If it seems odd and from out of nowhere, it’s because it lives in a greater context in my head. One that I haven’t put into words writing yet. Thank you for your patience in advance.
-Kyle
I’ve been thinking about composition for a long time. I use the word a lot, but I’m often met with confused faces when I do. I could be wrong, but I feel like “composition” in programming is one of those words that many people sort of understand but would really struggle to clearly define. That’s ok. I’m not calling anyone out.
You hear over and over, “Prefer composition over inheritance!”, but no one really breaks down what composition is. Let’s fix that.
First, “composition” is the act of combining and recombining existing “elements” to create new ones. It’s like combining atoms to make molecules, except without needing a laboratory or a chemistry degree.
Second, in my opinion, there are two different “types” of composition: function composition and object composition, but I think it might be more useful to think of these as nesting composition and merging composition. I think learning to recognize these two types of composition is beneficial.
Function (nesting) composition
I’ve written about function composition before, so I won’t go into great detail. Suffice it to say, this is when we nest functions together, such that the result from one becomes the input to another. We make new functions by combining and recombining other functions in different orders to achieve new results. For example:
const repeat = str => str + ' ' + str
const exclaim = str => str + '!'
const shout = str => str.toUpperCase()
const result = shout(repeat(exclaim('composition over inheritance')))
However, if we use our imaginations a bit, we can see that this very same thing happens in other contexts that we would not normally think of as functions. Such as with CSS and HTML.
For the last few years, I’ve worked on applications where the only layout mechanism available was Flexbox. Therefore, I had to use nesting compositions to create various layouts I wanted to achieve.
For example, if I have a Flexbox column, all of it’s child elements are set to align-items: stretch
by default. The stretch
value means that each child is stretched on the cross-axis, and thus takes up the full width of the parent container.
<div class="flex flex-col gap-4">
<p class="border">
Here is a paragraph with a border to show that it stretches the full width.
</p>
<p class="border">
Notice that the button below stretches the full width of the parent
container as well.
</p>
<button>Click me</button>
</div>
Here is a paragraph with a border to show that it stretches the full width.
Notice that the button below stretches the full width of the parent container as well.
But let’s say I want the button
only to take up the space of its text (and only the button should do this, the paragraph should remain the same), as if it was display: inline-block
?
Rather than try and add ad hoc CSS to this particular button (the go-to move for so many developers in my experience), or modifying the parent in some way, what if we just alter the composition?
Let’s wrap our button in a flex row container instead:
<div class="flex flex-col gap-4">
<p class="border">
Here is a paragraph with a border to show that it stretches the full width.
</p>
<p class="border">
Notice that the button below now only takes up the space of its minimum
content width.
</p>
<div class="flex">
<button>Click me</button>
</div>
</div>
Here is a paragraph with a border to show that it stretches the full width.
Notice that the button below now only takes up the space of its minimum content width.
Through nested composition, we’ve changed the appearance of our UI. In this case, a flex row container shrinks its children to their minimum content width by default, and thus we achieve the button look we’re going for.
Our HTML elements (with the proper CSS) behave sort of like functions in that they take their children as inputs and modify their output. If we were working within a system like React, where components are functions, we really would be doing function composition:
function MyComponent({ children }) {
return (
<Stack gap={4}>
<Text>{children}</Text>
<Row>
<Button>Click me</Button>
</Row>
</Stack>
)
}
Object (merging) composition
Objects can be merged together to form new objects, primarily with the ...
spread operator. Some common places this technique is used are extending configuration objects, or updating state objects.
import baseConfig from 'something'
export const myConfig = {
...baseConfig,
someProperty: {
foo: 'bar',
},
}
function reducer(state, action) {
switch (action.type) {
case 'TOGGLE_OPEN': {
return {
...state,
isOpen: !state.isOpen,
}
}
default:
return state
}
}
Object composition differs from function composition in a couple ways:
- For the most part, the objects are merged at a single level. Yes, we can merge at a deeper level in the object, but it has no impact on any of the higher levels of the object.
- Merging is a destructive act. When one object is merged into another, any key in the second object that matches a key in the first object replaces the first property’s value with the second object’s value.
Another way that you may have used this form of composition is with style objects:
const primaryButtonStyles = {
backgroundColor: 'blue',
color: 'white',
padding: '1em',
}
const dangerButtonStyles = {
...primaryButtonStyles,
backgroundColor: 'red',
}
console.log(dangerButtonStyles)
// { backgroundColor: 'red', color: 'white', padding: '1em'}
Taking this a step further, we might be able to think of CSS class composition as object (merging) composition. Consider these classes:
.primary {
background-color:;
}
.danger {
background-color: red;
}
If we were to (accidentally) apply both of these styles to an element, the order of our classes would have an impact on the element’s appearance, but maybe not in the way you expect.
<div class="primary danger">I will be red</div>
<div class="danger primary">I will also be red, lol</div>
This occurs because of where our object composition is really occurring. Because our .danger
class comes after our .primary
class in our stylesheet, the latter class’ values replace the values of the former class when there are matching keys.
We always have to be aware of how merging composition may have unintended consequences!
Why does any of this matter?
The main reason I find this important has to do with how easy it is for someone to understand one form of composition versus the other.
I have found that people find object/merging composition far easier to understand, even though it often has more pitfalls. It often provides developers with more “escape hatches” and ways to “throw stuff at the wall until it sticks”. In other words, there’s often more than one way to achieve the desired result, at the tradeoff of potentially making a destructive mess in the process.
On the other hand, function/nesting composition is often superior in terms of strictness of inputs and outputs, but is more difficult for developers to intuit. There’s less room for trying things to see if it works by design.
Be honest with yourself. How well do you know CSS? Do you know precisely what you’re doing, or do you need to be able to throw stuff at the wall? There’s no shame in the answer, I promised, but you might be able to guess what my future post will be about based on how you respond.
Consider our simple example above with the Button
. It’s so much easier for devs to throw more classes at the CSS, or use the style
prop, rather than recognize, “If I wrap this Button
in a Row
, it will achieve the result I’m looking for!” I know devs can get there. I’ve trained them to do it and seen it happen, but it’s not intuitive for many of them.
Summary
You might not find it useful to think of composition in these two ways, but I do, and I hope it helps you in some small way.
Here’s a quick break down of what I’ve discussed:
- Composition is combining existing elements to create new ones
- Nesting composition uses wrapping elements to modify functionality
- Parents change the functionality of child elements
- Merging composition smashes elements together destructively to achieve new ones
- The order of the smashing is important