I dueled a potion named Elixir for four days. I won. I’m still dazed, still confused, and fairly sure I’m still under the spell.
Turns out “Elixir” is also a D&D-adjacent term, and by day two that stopped feeling like a coincidence and started feeling like a warning label nobody read out loud to me.
Cal Poly, CSC 101, Fundamentals of Computer Science, in C++, first four weeks’ worth of concepts were apparently wrong for Elixir:
CSC 101 Week 1: variables. They update. index++.
Elixir: nope. Nothing mutates. Ever.
CSC 101 Week 2: conditionals, if statements.
Elixir: try not to even reach for the word if.
CSC 101 Week 3: loops.
Elixir: nope. Never supported. Do dangerous recursion instead.
CSC 101 Week 4: functions. Overloading should be rare.
Elixir: watch how many versions of one function name we can stack before you lose track of which one’s actually running.
I have never once wanted to flip a table at my keyboard until Week 4.
A closer look at what actually happened, week by week:
Variables. score_map["joe"] = 42 doesn’t work. Nothing mutates, ever. Map.put or %{map | key => value} hands you a brand new map; the old one just quietly stops existing. Want to increment something? count = count + 1 — you’re not changing a value, you’re rebinding a name and letting the old one go.
Conditionals. They exist, but the idiomatic path routes around if constantly in favor of pattern matching and multiple function clauses. Which is where I got tangled up next:
elixir
def number(n) when n == 7 do
"seven"
end
def number(_n) do
"not seven"
end
My actual reaction in the moment: “it feels like you’re defining five functions?!” I was right to be confused and wrong about the diagnosis — it’s one function, five clauses, tried top to bottom until one matches. And when isn’t a check tucked inside the body. It’s part of the function’s actual signature — I had to ask directly whether the guard was “part of the function prototype.” It is.
Loops. Not “advanced, unlock later” — absent from the entire beginner track. Recursion and Enum are the replacement, full stop. Tail call optimization means recursion won’t blow the stack the way it would in C++, which is genuinely clever engineering underneath — but it’s still the only tool in the drawer, a strange thing to accept as an adult after years of for loops being load-bearing in nearly everything I’ve written.
Functions. Multiple clauses, guards, pattern-matched arguments, overloading by shape instead of by name. This is the one that finally made me want to flip the table.
A few more that didn’t fit neatly into a single week but earned their place on this list:
Lists have no index. name_list[0] isn’t a thing. Lists are linked lists, not arrays — you get the head, the tail, or a pattern match: [first, last] = list. I learned this the hard way when tl(["Miller"]) handed me [] instead of "Miller" — I’d chopped the only element off and kept the leftovers, which were nothing.
A single space breaks compilation. %{name: "test"} compiles fine. %{name : "test"} — one stray space before the colon — does not. name: is a single atom token; name : is two tokens actively disagreeing with each other.
String concatenation is strings-only. <> won’t touch a number. "Battery at " <> remote_car.battery_percentage <> "%" fails outright, no PHP-style . operator that quietly coerces things for you. You need Integer.to_string/1 or interpolation, "#{value}", first.
Nothing is imported by default. Enum.map, String.split, Map.put — every function needs its module name stapled to the front. JavaScript, PHP, Python, and apparently Ruby all hand you common functions bare. Elixir doesn’t, and if you name your own function the same as a Kernel function, you’re writing import Kernel, except: [abs: 1] just to reclaim your own name.
No return keyword, anywhere. The last expression in a function is the return value, silently, always. Coming from PHP — where I never once considered return verbose — trusting this took longer than it probably should have.
The pipe operator doesn’t shorten anything; it just re-sorts it. I came into this already slightly cross-eyed from chaining five dot-methods in a row in JavaScript. |> promised to clean that up. What it actually does is take the same long sentence and stack it top-to-bottom instead of left-to-right. It reads cleaner once your eyes adjust. It is not, by any honest measure, shorter.
Processes are beginner material. In C++, the actor model and concurrent processes are the advanced elective you earn access to senior year, if you get there at all. On Exercism’s Elixir track, processes show up before regex, before structs, before enums, before comprehensions. Four days in, I was already handling receive blocks and message passing — territory that took most of a quarter to reach the first time I learned to program.
Here’s the thing about all of it: every pitch I’ve read for Elixir calls it a “modern language.” I would genuinely like someone to name another modern language that shipped without a loop, without mutation, and without a return keyword. I’m not saying any of this is badly designed — it’s Erlang’s descendant, built for swarms of small processes passing messages to each other, not a script marching top to bottom through a loop. I’m saying the branding oversells how familiar any of it is going to feel walking in.
Four days. Cal Poly’s first month of CSC 101, inverted almost line for line, at four times the speed, in a language quite literally named after a potion.
At this rate, a Vulcan logic puzzle, taught entirely in Klingon, might’ve been the gentler on-ramp. Yes — I know Kal-toh is Vulcan, not Klingon. That’s the point. Even the wrong alien language pairing sounds easier than the four days I just had.