← JamDojo Why Containers

Why Containers

The Problem: Values Alone Aren’t Enough

A note name is just a string:

"c4"

But to play music, you need more than that. When does it play? How long? What sound? A bare value has no context.


Containers Add Context

A container wraps values and adds behavior. You already know several:

Array — values with position:

[1, 2, 3]   // first, second, third

Promise — a value that arrives later:

fetch(url).then(data => ...)

Pattern — values with timing:

note("c4 e4 g4").log()

Press play and check the console. Each note has timing information:

{ value: 60, begin: 0, end: 0.333... }
{ value: 64, begin: 0.333..., end: 0.666... }
{ value: 67, begin: 0.666..., end: 1 }

The container (Pattern) manages the timing so you can focus on the notes.


Why Containers Enable Reuse

Without containers, timing would infect everything:

// Without containers: every function needs timing
playNote("c4", startTime: 0, endTime: 0.25)
playNote("e4", startTime: 0.25, endTime: 0.5)
transpose(note, amount, startTime, endTime)
// ...timing parameters everywhere

With containers, timing is the container’s job:

// With containers: functions just transform values
note("c4 e4").add(7).sound("piano")
// The pattern handles when things happen

This separation lets you write transformations once and apply them to any timing structure.


Same Operations, Different Containers

Here’s the key insight: containers share common operations.

Transform each value:

// Array
[1, 2, 3].map(x => x * 2)         // → [2, 4, 6]

// Promise
promise.then(x => x * 2)           // → promise of doubled value

// Pattern
"1 2 3".fmap(x => x * 2)           // → pattern of 2, 4, 6

The operation is conceptually identical. Only the container differs.

// Transform values inside a pattern
n("0 2 4".fmap(x => x + 7))
.scale("C4:major").sound("piano")

This isn’t a coincidence. It reflects a deep structure that all containers share.


Types: What’s Inside the Container

A type tells you what kind of value you have:

TypeMeaning
NumberA number like 42
StringText like “c4”
Array<Number>Array containing numbers
Promise<String>Promise that resolves to string
Pattern<Note>Pattern containing notes

The angle brackets show what’s inside. Pattern<Note> means “a pattern that contains notes.”

Why does this matter?

// These types tell you what operations are valid:
"c4 e4 g4".fmap(x => x + 1)     // Pattern<String> — can transform strings
note("c4 e4 g4").sound("piano")  // Pattern<Note> — can add sound

// And what isn't:
note("c4").fmap(x => x + 1)      // ❌ note() returns a control pattern, not a string pattern

Types catch errors and guide composition.


The Four Container Operations

Containers that share certain operations have names from mathematics. These aren’t scary—they’re just labels for patterns you’ll recognize:

NameOperationExample
FunctorTransform contents.fmap(x => x + 1)
ApplicativeCombine two containers.add("<0 7>")
MonadFlatten nesting.squeezeJoin()
MonoidBuild from piecesstack(a, b)

Don’t worry about memorizing these. We’ll explore each one in detail. The point is: these operations appear in arrays, promises, patterns, and many other containers. Learn them once, recognize them everywhere.


What Makes Pattern Special

Most containers have one obvious way to transform or combine. But Pattern has timing—and timing creates choices.

Combining arrays:

// Zip: pair up elements by position
[1, 2, 3] + [10, 20, 30] → [11, 22, 33]

// No ambiguity—position determines pairing

Combining patterns:

// Which timing wins?
n("0 2 4 6".add("<0 7>"))
.scale("C4:major").sound("piano")

The melody has 4 notes. The interval has 2 values. How do they combine?

  • Keep the melody’s 4-note rhythm?
  • Keep the interval’s 2-value rhythm?
  • Only play when both have events?

Pattern has multiple valid answers. That’s why we have .add(), .add.out(), and .add.mix()—different ways to combine timing.

This is what makes Pattern interesting: timing adds a dimension that other containers don’t have.


Containers as Architecture

Think of containers as architectural decisions:

ContainerWhat It ManagesYou Focus On
ArrayPosition, orderValues
PromiseAsync timingEventual value
PatternMusical timingNotes, sounds

By choosing a container, you decide what complexity it handles for you. Strudel chose Pattern because musical timing is hard, and patterns handle it elegantly.


What’s Next?

You’ve seen why containers exist: they wrap values and manage context, letting you write reusable transformations.

Now let’s look at Pattern specifically. What makes it tick?

Continue to Patterns as Containers

You understand containers in general. Next, you’ll explore Pattern: how it represents time, how it computes lazily, and how to peek inside.