We can build a tetromino now. We can move it left, let it fall, and rotate it. The struct carries all of that information:
%Tetromino{name: :z, location: {3, 1}, rotation: 90, color: :red}
But a screen does not know what to do with that.
The canvas is not asking for a struct. It is asking for points. Your program wants one representation because it makes the game easier to reason about. The thing drawing the game wants another representation because it needs pixels, cells, or coordinates.
Somebody has to translate.
That translation is the last step of CRC. Construct gives you valid data. Reducers move that data forward. Convert turns your internal shape into something another part of the system can consume.
In Blockr, that converter is to_group.
See the Shape Before You Code It
Before writing the converter, Bruce does something simple and easy to skip.
“The best thing to do is to start with some pencil-and-paper representation.”
— Bruce Tate
That advice matters because the hard part is not typing the list. The hard part is knowing which points belong in it.
So he opens a live book, draws a 4x4 grid, and places points until the pieces look right. The I piece, centered vertically, lives in the second column:
[{2, 1}, {2, 2}, {2, 3}, {2, 4}]
The L piece starts almost the same way, but the bottom point kicks out to the right:
[{2, 1}, {2, 2}, {2, 3}, {3, 3}]
That is the whole move: solve the geometry where you can see it, then bring the data back into the module.
The live book is not a toy here. It is where the uncertain part becomes concrete. By the time those lists reach Tetromino, they are already Elixir data and already shaped like the canvas expects.
The struct is what the game needs. The list of points is what the canvas needs. to_group is the boundary between those two worlds.
🎯 Join Groxio's Newsletter
Weekly lessons on Elixir, system design, and AI-assisted development -- plus stories from our training and mentoring sessions.
We respect your privacy. No spam, unsubscribe anytime.
Before: Split the Shapes Across Function Heads
The first version uses pattern matching in the function head. A tetromino is a struct, and a struct is a map, so this works cleanly:
def to_group(%Tetromino{name: :i}), do: [{2, 1}, {2, 2}, {2, 3}, {2, 4}]
def to_group(%Tetromino{name: :l}), do: [{2, 1}, {2, 2}, {2, 3}, {3, 3}]
def to_group(%Tetromino{name: :j}), do: [{3, 1}, {3, 2}, {3, 3}, {2, 3}]
There is nothing broken about this. Pattern matching in function heads is one of Elixir’s best tools.
But this converter is not doing different work for each function clause. It is making one decision: look at the name, return the matching points.
Bruce points out the idiom he wants students to notice:
“There’s been a recent movement to make more decisions in case statements and cond statements than in function heads themselves. That’s what’s called idiomatic, meaning the typical Elixir you might see in the field.”
— Bruce Tate
So this is a good place to change the shape of the code.
After: Put the Decision in One Case
The version that reads better keeps the decision in one function:
def to_group(tetro) do
case tetro.name do
:i -> [{2, 1}, {2, 2}, {2, 3}, {2, 4}]
:l -> [{2, 1}, {2, 2}, {2, 3}, {3, 3}]
:j -> [{3, 1}, {3, 2}, {3, 3}, {2, 3}]
:o -> [{2, 2}, {3, 2}, {2, 3}, {3, 3}]
:t -> [{2, 1}, {2, 2}, {2, 3}, {3, 2}]
:s -> [{3, 1}, {3, 2}, {2, 2}, {2, 3}]
:z -> [{2, 1}, {2, 2}, {3, 2}, {3, 3}]
end
end
Now the seven shapes sit together. They line up visually. They read like a small table.
That is the practical rule: use function heads when the clauses do genuinely different work or need different guards. When every branch dispatches on one field and returns the same kind of thing, a case usually says the intent more clearly.
This is not about cleverness. It is about making the next reader see the whole decision at once.
It also lets the comments disappear. If the old code needed comments explaining what each shape looked like, that was a sign the data was still outside the file. Once the point lists are there, the code can show the shape directly.
Let the Struct Finish the Conversion
To try different pieces in the live book, the constructor needs to accept a name while keeping the old default:
def new(name \\ :i) do
%__MODULE__{name: name}
end
Now the converter composes with the group code:
Tetromino.new(:z)
|> Tetromino.to_group()
|> Group.rotate(90)
That works, but passing 90 by hand should feel suspicious. The tetromino already has a rotation field. We added that state earlier so the struct could remember where the piece is facing.
The converter should use it:
def to_group(tetro) do
points =
case tetro.name do
# ... the seven shapes
end
Group.rotate(points, tetro.rotation)
end
Bruce describes the expected result plainly:
“What should come out of this is a group of points that is rotated in the correct direction for the tetromino.”
— Bruce Tate
That is the moment to_group becomes a real converter. It does not just return the base shape. It turns the tetromino’s internal state into drawable points.
One field is still waiting: location. Right now the shape appears in its little 4x4 box, not where the piece belongs on the board. The next step is to move every point by the tetromino’s location, then color can follow the same pattern.
Once you see converters this way, you stop leaking canvas concerns into your structs. Your model gets to speak the language of the game, and the outside world gets the data shape it needs.
📚 Learn Elixir Data Boundaries by Mental Model
This comes from our Learning Elixir course, where Bruce teaches mental models for production architecture decisions through real-world scenarios. Learn how constructors, reducers, and converters keep domain data separate from the shapes other systems need. Available via monthly subscription -- try it for one month.
— Paulo & Bruce