1 Python Pro meets AI Mentor to Learn Elixir using 80/20 Rule
1.1 Preface
I stared my professional life as a developer in the year 2000, but left the programming world after my MBA and moved into Management Consulting. Nearly after a 15-year gap, I came back to programming to build an app for a personal pet project. I learnt Python and built an application using Django in a very short time and maintained it for 3 years. I liked a lot of things about Django - though it was complicated to get started, it made me very productive. But I wanted my front-end to be more responsive and avoid full page loads - So, I started evaluating JS SPA frameworks like React.
I have evaluated a lot of JS frameworks - both frontend (React, Angular, Vue, Svelte) and fullstack (NextJS, Remix, RedwoodJS) to see whether I could settle on using Javascript only for the entire stack. But most of the approaches in the JS world seemed too complicated. I was really shocked to see the amount of code one has to write to get a basic Counter working in React. And the amount of boilerplate in NextJS to make it a full-stack app.
So, it was a refreshing change to see HTMX and how simple and declarative it was to get a lot of the reactivity on the front-end without too much boilerplate code. I was pleased to read the Hypermedia systems book to realise how the web was originally envisioned and how that original vision solved many of the problems that are faced by the API-driven front-end based approach for full-stack applications. I also evaluated FastAPI and was pleased to see how many things they have done to make the developer experience better compared to Django. So, when I saw that FastHTML took the ideas of FastAPI and combined with HTMX, I jumped in, learnt it and started to port my application. It was such a nice feeling to be able to iterate on UI code using Jupyter notebook. We built the app relatively quickly and moved it into production. That is when we started facing the challenges with FastHTML.
Being a new framework, it did not have many of the things I have taken for granted in Django - database migrations, admin app, authentication, well-thought testing framework, etc. So, One day I was researching on the best way to organize the different HTTP routes to standardize our approach for testing using Playwright and pytest combo. And then I was chatting with Claude AI to see what other frameworks are done.
That’s when I discovered Ruby on Rails conventions. I was amazed to realize that all the architectural decisions I was wrestling with had been thoughtfully codified in Rails for over a decade. I have heard a lot of positive things about Rails, but I have also heard that people moved out of Rails as it did not scale well and there was no way to write front-ends like React. I watched a demo of Ruby on Rails 8 and was impressed by the framework. So, I started studying Ruby to build a sample app in Ruby to evaluate it.
When I started reading the Ruby on Rails book, the preface was written by Derek Sivers, who I met a few years back. I wrote to him saying that finally I am looking at the language that he loves a lot - Ruby. But surprisingly, Derek asked to look at Phoenix instead of Ruby on Rails. I was not in favour of learning another new language. Though I have heard very positive things about Phoenix, I did not have a very positive opinion of Elixir as I tried it out a year back in a Functional Programming week, by Exercism. I felt that the language had too many annoying rules and the syntax was clumsy. So, I was not willing to even consider Elixir as part of my choices. But for some reason, Derek gently pushed to relook at Elixir, mentioning that Dave Thomas, who wrote the book on Rails told him that Phoenix is a better web development framework these days. Hmmm. I really did not want to leave the lure of Rails as I was already reading one of the great books on Ruby on Rails. Then to confront the confirmation bias in me, I decided to give myself a week to see whether Elixir is really that bad, as per my initial quick judgement and whether Phoenix is really that good, like what everyone keeps saying.
I bought the 2 most recommended books on Elixir - Programming Elixir 1.6 by Dave Thomas and Elixir in Action by Saša Jurić. I also bought the Elixir and Phoenix video courses by Pragmatic Studio. The video courses were phenomenal as they launched into building a HTTP server from scratch in Elixir and I loved every bit of it - though it was complex, I was, in some sense prepared for it, as I had built a HTTP server from scratch in python last year through CodeCrafters.io. So, I was able to see how Elixir fares in comparison to Python for building a HTTP server from scratch. This is when I realised the elegance behind some of the abstractions in Elixir.
After a week, I started to like Elixir and I slowly started to see the wisdom behind some of the annoying rules (here is an example - you have to use a dot to call a function defined in local scope, but for a function defined in a module, a dot is not needed - I wondered why?). I watched through the full Elixir and OTP course, but found it was just too much information for me as my main aim was to understand Phoenix, rather than being an Elixir pro. Then I jumped into Phoenix course and the Elixir syntax did not seem magical for me and I was able to easily follow the Phoenix course.
I am not a video guy - after watching videos for a few days, I felt that I needed some reading material. But I went back to reading the books I have bought - but both books dived deep into Elixir. Elixir in Action took the typical approach of starting with covering all the data types, then variables, etc. Though Dave Thomas skipped a lot of such material, a lot of concepts he discussed were not really needed for me to work in Phoenix. I came across a few blog posts where people have documented how they have learnt Elixir and Phoenix and they mentioned all these materials plus some more. So, it looks like we have to do a PhD level study in Elixir before starting on Phoenix.
I wished there was a book which applied the 80/20 rule to learning Elixir - 80% of the outcomes come from 20% of effort. So, I wanted to identify the core 20% of the Elixir language that can help a newcomer to be productive in Elixir in a week. My goal is for a newcomer to Elixir to be productive in Phoenix as quickly as possible - yes, there might be parts that he/she may not understand well, but that is OK, they can always pick these up, by going to these excellent deep dives offered by the materials I have mentioned above, when they feel they need to master Elixir. I hate theory - that these are the data types and these operators, etc. - I want to learn by doing small, progressively complex examples that build real understanding.
I’ve designed this book specifically for developers coming to Elixir from Python, using contrast between the two languages to reinforce learning. The exercises incorporate proven learning science principles—spacing, interleaving, and recall testing—to help you build muscle memory for the core language patterns you’ll encounter most frequently in Phoenix.
My goal is simple: get you productive with Phoenix in a week, not a month. Let us see whether anyone is able to get there in a week.
There is one more thing - I am gonna use AI extensively to help me write the book fast. I am using Claude AI directly and through the excellent SolveIt platform from AnswerDotAI. This book is structured as a conversation between a python pro and AI as Elixir Mentor.
1.2 Rules of the game
- You are going act as a seasoned programmer who knows python, JS, and many other OO-based languages. You have worked with Ruby from its early stages and then moved to Elixir. So, you are the veteran who is gonna help me to see things in proper perspective.
- I am a python programmer. Though I know JS well, I have done a lot more work in Python than JS. So, when you are talking to me to explain some of the key ideas in Elixir, you are gonna compare it to Python first and then if needed JS. But keep the JS examples minimal, so that someone without much JS experience can still understand this book.
- This book is going to be a conversation between a mentor (you) and a student (me). We talk things out, and you give me things to do, code to write, other material to read or watch, and I would come back to you with my observations and questions.
- You are also going to question me on whether my understanding is correct by asking me questions, giving me simple yet challenging exercises to see whether I really understood things at a deeper level.
- Whenever you explain any concept, please use simple and concrete examples that a beginner can easily visualise and keep it in his memory. I am aiming this book at people who are new to programming who might have had a brief exposure to Python. If a seasoned python pro reads this book, it should make sense to him and should not bore him to death.
- Keep it minimal - focus on the 20% of the language that will the reader 80% returns on his time invested
- Point out the joyful developer experience in Elixir’s design whenever possible.
Python Pro
So, where should I start with Elixir?
AI Elixir Mentor
Here are 3 key ideas that you will see mentioned a lot in Elixir - we will go through some hands-on exercises to understand them.
- Pattern matching
- Immutability by default
- Piping as way to use Functions for transformations
But before we get started, make you have installed iex
- the interactive Elixir shell running in your system. If you need help, go to this page, install elixir and do the steps mentioned there to say hello world
interactively in iex
and by running an elixir code file. Elixir Installation Guide
Here is a test to check whether you got iex installed - what happens when you type i “cat” in iex
?
1.3 Interactive Elixir Shell
Python Pro
here is what I see in my iTerm - I am on a Mac.
(38)> i "cat"
iexTerm
"cat"
Data type
BitString
Byte size
3
Description
This is a string: a UTF-8 encoded binary. It's printed surrounded by
"double quotes" because all UTF-8 encoded code points in it are printable.
Raw representation
<<99, 97, 116>>
Reference modules
String, :binary
Implemented protocols
Collectable, IEx.Info, Inspect, JSON.Encoder, List.Chars, String.Chars
AI Elixir Mentor
Perfect! I see you have iex
working correctly. The i
function gives you information about any value - in this case showing that “cat” is a binary (You expected to see string - right? But Elixir calls it binary and you will understand over time why. Don’t delve into too much as it is not important - Just remember to map string to binary whenever you have to do something with string).
Now just type h and see what you get. You will see a lot of output and you have to scroll up a lot to see the first page. See whether you can find anything related to the previous command you executed in that first page.
Python Pro
Yes - I see the help for the i
command I used - it says i/1
is a function to introspect any value. A lot of jargons, I guess :). But I got it, but what is that suffix /1
and /2
I see quite a bit?
(39)> h
iex
IEx.Helpers
Welcome to Interactive Elixir. You are currently seeing the documentation for
IEx.Helpers which provides many helpers to make Elixir's shell more
the module joyful to work with.
This message was triggered by invoking the helper h(), usually referred to as
h/0 (since it expects 0 arguments).
You can use the h/1 function to invoke the documentation for any Elixir module
or function:
iex> h(Enum)
iex> h(Enum.map)
iex> h(Enum.reverse/1)
You can also use the i/1 function to introspect any value you have in the
shell:
iex> i("hello")
There are many other helpers available, here are some examples:
• b/1 - prints callbacks info and docs for a given module
• c/1 - compiles a file
• c/2 - compiles a file and writes bytecode to the given path
• cd/1 - changes the current directory
• clear/0 - clears the screen
• exports/1 - shows all exports (functions + macros) in a module
• flush/0 - flushes all messages sent to the shell
• h/0 - prints this help message
• h/1 - prints help for the given module, function or macro
• i/0 - prints information about the last value
• i/1 - prints information about the given term
AI Elixir Mentor
Great observation! The /1
and /2
notation is how Elixir indicates function arity - another jargon for the number of arguments a function takes. Just note this term arity
as you will see it used quite a lot in Elixir.
For example: - i/1
means the i
function that takes exactly 1 argument - h/1
means the h
function that takes exactly 1 argument - h/0
means the h
function that takes 0 arguments (which is what you just ran)
This is important in Elixir because unlike Python, you can have multiple functions with the same name but different number of arguments, and they’re considered completely different functions.
Python Pro
in python, the same function can’t be used again with a different number of arguments - right?
AI Elixir Mentor
That’s correct! In Python, you can’t have multiple functions with the same name but different numbers of arguments in the same scope.
In Python, if you define a function twice, the second definition overwrites the first:
def greet(name):
return f"Hello, {name}!"
def greet(name, time_of_day):
return f"Good {time_of_day}, {name}!"
# Now only the second definition exists
# greet("Alice") would raise a TypeError
Python handles variable arguments through default parameters, *args, and **kwargs:
def greet(name, time_of_day="day", *args, **kwargs):
# Handle different numbers of arguments in one function
pass
In Elixir, these are actually different functions, and the language will select the right one based on the number of arguments you provide. This is a fundamental difference that affects how you design your code.
But let us get too deep into those differences now - My main aim now is to familiarize you with the iex shell. So, let us get back to that.
Let me summarize - there are 2 ways to access the documentation 1. All the documentation of all packages in Elixir including the core packages are available in hexdocs.pm
. 2. IEx’s h
helper shows offline documentation for modules and functions that are part of Elixir itself or other libraries installed in your current project
Compared to basic python REPL or even the colorful ipython, iex is a lot more feature-rich. We will come back to this later on. But I want you to start paying attention to iex and not dismiss as just another command-line utility for the geeks.
Before, we close the topic on iex, Let’s try this out. Try typing:
h i/1
Then try:
h h
What do you notice about the output of these commands?
Python Pro
i
prints information about the data type of any given term.
And h h
prints the documentation for IEx.Helpers. Here is what I got for h h
.
(40)> h h
iex
def h()
Prints the documentation for IEx.Helpers.
defmacro h(term)
Prints the documentation for the given module or for the given function/arity
.
pair
## Examples
> h(Enum)
iex
It also accepts functions in the format function/arity and
.function/arity, for example:
module
> h(receive/1)
iex> h(Enum.all?/2)
iex> h(Enum.all?) iex
And interestingly, I already see the arity word here. It says function/arity pair
. But I also see the word macro
- what is that? I have heard of macro only in Excel, not in programming languagues like Python or JS.
AI Elixir Mentor
Good question! Macros in programming languages are quite different from Excel macros.
In Elixir (and many other languages), a macro is code that writes code. Think of it as a function that runs at compile time rather than runtime, and it can transform your code before it’s actually compiled.
Here’s a simple way to understand it:
- Regular functions: Take data as input, return data as output
- Macros: Take code as input, return code as output
In Python, you might have encountered decorators, which are somewhat similar in concept but much more limited than Elixir macros.
Macros are a powerful feature in Elixir that let you extend the language itself. They’re part of what makes Elixir’s syntax so clean and expressive. For example, the if
statement in Elixir is actually a macro, not a built-in language construct!
For now, just know that macros exist and are powerful, but we won’t need to create our own macros when getting started. We’ll use many built-in macros without even realizing it.
Let’s move on to something more simple as macros is an advanced topic that is outside the core 20% we want to focus.
Let us get started with the first concept - pattern matching. Do you remember how destructuring works in python?
1.4 Pattern Matching
Python Pro
Destructuring (also called unpacking) in Python allows you to assign elements from a list (or any other iterable, if I want to get more precise) to multiple variables in a single statement.
= [1, 2, 3]
numbers = numbers
a, b, c print(a, b, c) #> 1, 2, 3
In this example: The list numbers contains three elements. The statement a, b, c = numbers unpacks the list so that a gets the first value, b the second, and c the third.
Rule : The number of variables on the left must match the number of elements in the iterable on the right.
AI Elixir Mentor
Excellent! You’ve got a solid understanding of Python’s destructuring.
In Elixir, we have something similar but rather than calling =
as assignment operator
, Elixir calls it match operator
and when an Elixir pro looks at this line numbers = [1, 2, 3]
, he will say this is NOT an assignment, but pattern matching. It would make you wonder Hmm...Why a new name for something that is so commonly understood and used?
To help you understand the Elixir Pro talk, let us start translating each line from your python code into Elixir equivalent.
Now type this first line into iex:
> list = [1, 2, 3] iex
before you press Enter, think about what you expect to see. In python, if you type this into ipython
what would you see? Try that first and then do the same thing in iex and let me know what you see as the difference.
Python Pro
Since assignment is just a statement, I did not expect anything to be printed in the console and ipython did not print anything. But surprisingly, iex printed the list [1,2,3]
- why?
AI Elixir Mentor
Great observation! This is one of the first differences you’ll notice between Python and Elixir.
In Elixir, every expression returns a value, and IEx automatically prints that returned value. When you write list = [1, 2, 3]
, the match operator =
returns the value on its right side after performing the match.
This is actually quite convenient because:
- You can immediately see the result of your operations
- You can chain operations together, since each returns a value
- You get immediate feedback that your code worked as expected
To achieve the same effect, in Python, you have to type the variable again:
3]: numbers = [1,2,3]
In [
4]: numbers
In [4]: [1, 2, 3] Out[
Now, let’s try the destructuring part. In Elixir, type:
> a, b, c = numbers iex
What do you expect to happen? And what actually happens when you run it?
Python Pro
Hmm. that is mean - iex screamed at me in anger - here is what I see, but the error stacktrace is just 4 lines - that is nice :)
(2)> a, b, c = numbers
iex** (SyntaxError) invalid syntax found on iex:2:2:
error: syntax error before: ','
│2 │ a, b, c = numbers
^
│
│iex:2:2
└─ (iex 1.18.3) lib/iex/evaluator.ex:299: IEx.Evaluator.parse_eval_inspect/4
(iex 1.18.3) lib/iex/evaluator.ex:189: IEx.Evaluator.loop/1
(iex 1.18.3) lib/iex/evaluator.ex:34: IEx.Evaluator.init/5
(stdlib 6.2.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3
AI Elixir Mentor
So, you thought that you’re gonna learn by executing everything perfectly?!
Had I given you the proper syntax, I am sure you will have NOT noticed that there is a closing and opening bracket around a, b, c
. By making a mistake, now you learnt that things are different from python here. To me, this is the essence of learning by doing - you actively experiment by doing things to see how Elixir works, sometimes intentionally committing a mistake to see how Elixir reacts to that.
So you got to experience how Elixir reports error. Even though it is NOT clear why there is a syntax error before the comma, you can see that the stack trace and the overall error reporting is much shorter and sweeter compared to Python.
But before we see the right syntax to make this statement work, let us understand how this ACTUALLY works in python. Most people don’t know how it works. Once you know how it works, you will see the elegance behind the design of Elixir.
= [1, 2, 3]
numbers = numbers a, b, c
- Left side: a, b, c creates a tuple as if you have written like this:
(a, b, c)
- Right side: numbers is a list, but can be any iterable (tuple, string, etc.)
- Python unpacks: Takes the iterable and assigns each element to corresponding positions in the tuple pattern
So, python is creating a tuple, behind the scenes, and uses it to unpack. But Elixir’s philosophy is being explicit. Hence, it wants you to specify things explicitly - so, you have to use a list on the left side too. So, can you fix the issue yourself?
Python Pro
I went ahead and fixed the issue, and it was easy enough.
[a, b, c] = numbers
I really like the fact that Elixir is returning the value on the right-hand side. I can easily see the values assigned to a,b and c
without explicitly printing like in python.
Then I thought of trying on my own to actively figure out what should be the equivalent of the print statement and I just tried print(a,b,c)
and it reported to me - By readign the error, I understand that there is no print function which accepts 3 arguments - maybe, Elixir wants me to be explicit here. So, I tried just print(a)
and even that did not work - that is annoying.
iex(7)> print(a,b,c)
error: undefined function print/3 (there is no such import)
└─ iex:7
** (CompileError) cannot compile code (errors have been logged)
AI Elixir Mentor
Excellent! You fixed the pattern matching syntax perfectly - [a, b, c] = list
is exactly right. You’re absolutely right - Elixir wants you to be explicit with the square brackets [a, b, c]
to match the structure on both sides.
And I love that you’re experimenting on your own!
And you’re discovering another key difference: Elixir doesn’t have a print
function like Python. Instead, you use IO.puts
for printing. IO is the module in which puts function is defined - puts means put the string
AI is thinking of sending a message to Jose Valim (the creator of Elixir): Dear Jose,
IO.puts
(s for string) seems inconsistent in Elixir as strings are called binary - So, why NOTIO.putb
for putting the binary. And why not use the word
Ok, sorry I got lost in my thoughts. Try this:
IO.puts(a)
But notice how you can also just type the variable name and see its value, as in ipython:
a
What happens? And then see if you can figure out how to display all three values.
Python Pro
No I could not figure it out. I tried IO.puts(a,b)
and it reported an error about FunctionClause - that is weird - I was expecting it to say that print/2 is not found.
(8)> IO.puts(a, b)
iex** (FunctionClauseError) no function clause matching in IO.puts/2
The following arguments were given to IO.puts/2:
# 1
1
# 2
2
Attempted function clauses (showing 1 out of 1):
def puts(device, item) when is_atom(device) or is_pid(device)
(elixir 1.18.3) lib/io.ex:311: IO.puts/2
iex:8: (file)
AI Elixir Mentor
Great experimentation! You’ve discovered something important about Elixir’s error reporting.
The FunctionClauseError
tells us that IO.puts/2
exists (a function that takes 2 arguments), but your arguments don’t match what it expects. Function Clause is a key concept you need to understand as it falls under the core 20%. But we will come back to it later.
Looking at the error, it shows that IO.puts/2
expects the first argument to be a device (like :stdio
for standard output), not just any value.
So IO.puts
has two versions: - IO.puts/1
- prints to standard output - IO.puts/2
- prints to a specific device. So, you when you called this function/arity combination, Elixir was expecting a device as the first arg. But you passed it a number. But don’t worry about it for now - we will come back to this idea later.
Did you try to pass a list to IO.puts and see what it does?
Python Pro
It printed something weird - why?
iex(9)> IO.puts([a,b,c])
^!^"^#
:ok
and BTW, what was that :ok
that was on a new line. I noticed it when the value of a
was printed too.
iex> IO.puts(a)
1
:ok
AI Elixir Mentor
Great observations! You’ve hit two important Elixir concepts:
The :ok
: This is called an atom in Elixir - Atoms (starting with :
) are like symbols - constants that represent themselves. This is another key concept you need to understand as a lot of standard library functions return atoms to indicate success or failure. Here, IO.puts
is returning this atom to indicate that it has done its job successfully.
Remember how every expression in Elixir returns a value? IO.puts
does its job (printing), then returns the atom :ok
to indicate success. It’s like a Python function that returns None
, but here it is more explicit about success.
The weird output ^!^"^#
: When you pass a list of integers to IO.puts
, Elixir tries to interpret them as character codes. The numbers 1, 2, 3 correspond to ASCII control characters, which display as those strange symbols. This is Elixir being helpful (sometimes too helpful!) by assuming you want to print characters.
Here is the way to think about IO.puts
- use it when you want to print human-readable strings only. When you pass a list to IO.puts
, Elixir is thinking that the list contains the codepoints for each of the characters you want to print. In iex
, type i "cat"
now and see what it prints as the internal representation. You will see something like <<99, 97, 116>>
- this the delimiter that Elixir uses to denote the start and end of binary. It will also tell you the reason why it uses double-quotes:
This is a string: a UTF-8 encoded binary. It’s printed surrounded by “double quotes” because all UTF-8 encoded code points in it are printable.
Sidenote: if you don’t understand what UTF-8 means, and you really have a lot of time on your hand, then try reading this looooong but wonderfully accessible explanation of all the complexity behind UTF-8.
Now try IO.puts([99, 97, 116])
and see what gets printed? you got the cat
- right? So, that is how cat
is stored internally.
If you want to print a list or any other complex data structure, use IO.inspect. Just do a quick h IO.inspect
and see what it says. The documentation is wonderful as it gives very clear example usages. Now try to print the values of a,b,c
using IO.inspect and see whether you can succeed.
Python Pro
Yes I was able to succeeed.
iex(16)> IO.inspect([a,b,c])
[1, 2, 3]
[1, 2, 3]
I guess that the first line is the actual printing and the 2nd line is the same value returned by IO.inspect - but why does it do it?
AI Elixir Mentor
Exactly right! You’ve figured out the pattern.
IO.inspect
is designed to be used in pipelines and debugging, so it prints the value AND returns it unchanged. This lets you “peek” at values in the middle of a computation without disrupting the flow. But we have not really discussed the concept of piping yet in Elixir.
Let us get back to printing again - to print the values of a,b,c
like in python, you should use string interpolation.
IO.puts("#{a} #{b} #{c}")
And this is very similar to python f-strings, though python has more than 1 way to do string interpolation, Elixir has only one way. Pay attention to the extra pound symbol for each interpolated variable, compared to the python syntax. Try typing it by hand to get used to this as you will use it a lot.
# f-strings (Python 3.6+)
print(f"{a} {b} {c}")
Python Pro
Yes I tried it and it works
iex(16)> IO.inspect([a,b,c])
[1, 2, 3]
[1, 2, 3]
iex(17)> IO.puts("#{a} #{b} #{c}")
1 2 3
:ok
iex(18)> IO.puts(a)
1
:ok
when I tried IO.puts(a) and you said that puts means put the string, but a is NOT a string. It is an integer and you said that Elixir is explicit :)
AI Elixir Mentor
Excellent catch! You’ve spotted an inconsistency in my explanation. You’re absolutely right to question this.
IO.puts
is actually more flexible than I initially described. It can handle different data types by converting them to strings automatically. When you pass an integer like a
(which is 1), Elixir converts it to the string “1” before printing.
This is one of those cases where Elixir is being helpful rather than strictly explicit. The function is designed to “do what you mean” for common printing tasks.
If Elixir were truly strict about only accepting strings, you’d have to write:
IO.puts(Integer.to_string(a))
But that would be quite verbose for such a common operation.
So you’ve discovered an important nuance: Elixir balances explicitness with practicality. It’s explicit about data structures (like requiring [a, b, c]
instead of a, b, c
), but more forgiving with common operations like printing.
Python Pro
Ok - we have covered a lot of ground and my head is spinning. Can we summarize?
AI Elixir Mentor
Absolutely! Let’s summarize what you’ve learned so far:
1.5 Key Concepts Covered
1. Pattern Matching vs Assignment - Elixir uses =
as a “match operator”, not assignment - Must be explicit with structure: [a, b, c] = list
(not a, b, c = list
) - Always returns the value on the right side
2. IEx Interactive Shell - Every expression returns a value (unlike Python REPL) - h
for help, i
for inspecting data types - Function arity notation: function_name/number_of_arguments
3. Printing and Output - IO.puts
for human-readable output (converts types automatically) - IO.inspect
for debugging (prints AND returns the value) - String interpolation: "#{variable}"
(similar to Python f-strings, but pay attention to the extra weight of the pound symbol)
4. Key Differences from Python - Every expression returns a value - Atoms like :ok
indicate status
Python Pro
that is great - please show me the python and Elixir code that we have walked through side-by-side to go over it once more.
AI Elixir Mentor
Here’s the side-by-side comparison of what we’ve covered:
1.6 Creating and Pattern Matching Lists
Python:
= [1, 2, 3] # Assignment, no output
numbers = numbers # Destructuring (implicit tuple creation) a, b, c
Elixir:
= [1, 2, 3] # Pattern matching, returns [1, 2, 3]
list [a, b, c] = list # Pattern matching, explicit structure
1.7 Printing Values
Python:
print(a) # Prints: 1
print(f"{a} {b} {c}") # Prints: 1 2 3
print([a, b, c]) # Prints: [1, 2, 3]
Elixir:
IO.puts(a) # Prints: 1, returns: :ok
IO.puts("#{a} #{b} #{c}") # Prints: 1 2 3, returns: :ok
IO.inspect([a, b, c]) # Prints: [1, 2, 3], returns: [1, 2, 3]
Notice how Elixir always shows you what each expression returns, while Python’s assignment statements return nothing.
Python Pro
Great - now, give me a little exercise to put what I have learnt to use. Maybe, I can try pattern matching a list of words. Give me the python code for me to translate into Elixir.
AI Elixir Mentor
Perfect! Here’s a Python exercise for you to translate:
Python code to translate:
= ["apple", "banana", "cherry"]
fruits = fruits
first, second, third print(f"I like {first} the most!")
Python Pro
Yes, I succeeded and here is my code
= ["apple", "banana", "cherry"]
fruits [first, second, third] = fruits
IO.puts("I like #{first} the most!")
AI Elixir Mentor
Excellent work! You’ve translated it perfectly. Your Elixir code shows you understand:
✅ Pattern matching with explicit list structure [first, second, third]
✅ String interpolation with #{first}
✅ Using IO.puts
for output
Now let’s test your understanding with a slightly trickier question:
How will you translate the following python code into Elixir:
print('All my fruits:', fruits)
#> All my fruits: ['apple', 'banana', 'cherry']
Hint: type h inspect
and see what it says.
Python Pro
Haha…That was not a tough question at all. Here is my Elixir code:
IO.puts("All my fruits: #{inspect(fruits)}")
But what is the difference between IO.inspect and plain inspect? I am confused.
AI Elixir Mentor
Great job! You nailed it perfectly.
The difference is subtle but important:
inspect/1
- A function that converts any data structure to a string representation (but doesn’t print anything)IO.inspect/1
- Prints the data structure AND returns it unchanged
So when you use inspect(fruits)
inside string interpolation, you’re converting the list to a string first, then that string gets interpolated into your message.
Mental model:
inspect
= convert to string (like Python’s str() or repr())IO.inspect
= print and return (like Python’s print() but returns the value)
Python Pro
Ok I understand that - but we started this discussion on pattern matching saying this is a match operator and not an assignment operator. So, how does assignment work in Elixir?
AI Elixir Mentor
Excellent question! This gets to the heart of why Elixir calls =
as the match operator
rather than assignment
.
Let’s start with our familiar numbers list.
We started with:
= [1, 2, 3]
numbers [a, b, c] = numbers
Now, what if we wanted to match just the first element? Try this:
[first] = [1]
This pattern matches a list with exactly one element, binding first
to 1
.
Now, here’s the key insight: when you write x = 5
, this is really just pattern matching with the simplest possible pattern - a single variable! It’s like matching a “list” with one element, but without the brackets.
So “assignment” in Elixir is actually just pattern matching where: - Left side: a single variable pattern - Right side: a single value
In Elixir, there’s no traditional “assignment” like in Python. Everything is pattern matching, but it can look like assignment in simple cases.
When you write x = 5
, What’s really happening is: “Does the pattern x
match the value 5
?” Since x
has not been declared before, Elixir will match the two sides by binding x
to 5
. If x already has a value, Elixir will still match the value by rebinding x
to 5
. So, in a sense, this is similar to assignment and reassignment in python.
But here’s where it gets interesting. Try this in iex:
= 5 # x is now bound to 5
x 5 = x # What do you think happens here?
Python Pro
It just returned the right side value - which is 5, as expected. But I was expecting it to throw an error as assignment flows from right to left in almost all languages I have worked, starting from C to Java to Python and even JS.
AI Elixir Mentor
Exactly! You’ve discovered the fundamental difference between assignment and pattern matching.
In assignment-based languages (Python, Java, etc.), 5 = x
would be a syntax error because you can’t assign to a literal value.
But in Elixir, 5 = x
means: “Does the pattern 5
match the value stored in x
?” Since x
contains 5
, the match succeeds and returns 5
.
This is why Elixir calls it the “match operator” - it’s checking if both sides can be made to match, not flowing data from right to left.
Try this experiment to see the difference:
= 5
x 6 = x
What do you think will happen now? And why?
Python Pro
I get a match error - it makes sense as 6 is not equal to 5
iex(35)> 6=x
** (MatchError) no match of right hand side value: 5
(stdlib 6.2.2) erl_eval.erl:667: :erl_eval.expr/6
iex:35: (file)
AI Elixir Mentor
Perfect! You’ve grasped the core concept of pattern matching.
The error no match of right hand side value: 5
tells us exactly what happened: Elixir tried to match the pattern 6
with the value 5
, and they don’t match, so it failed.
This is fundamentally different from assignment because: - Assignment: Always succeeds (overwrites the variable) - Pattern matching: Can fail if the patterns don’t match
This makes Elixir code much more robust - you can use pattern matching to ensure your data has the structure you expect, and the program will crash early if it doesn’t, rather than silently continuing with wrong data.
Now you understand why =
is called the match operator! It’s not moving data from right to left - it’s checking if both sides can be made to match.
Now let us take a detour to understand the real meaning of =
in Elixir.
1.8 Detour - Real meaning of =
Assignment Operator
Here is an excerpt from Dave Thomas’ book on Elixir:
> “Joe Armstrong, Erlang’s creator, compares the equals sign in Erlang to that used in algebra. When you write the equation x = a + 1, you are not assigning the value of a + 1 to x. Instead you’re simply asserting that the expressions x and a + 1 have the same value. If you know the value of x, you can work out the value of a, and vice versa. > > > His point is that you had to unlearn the algebraic meaning of = when you first came across assignment in imperative programming languages. Now’s the time to un-unlearn it.”
when I had read this, I did NOT really understand what Joe Armstrong was saying as I have almost forgotten the real meaning of equals in algebra in the course of my programming career. So, I turned to the newly baked Claude 4 and asked it to explain what this statement means and the whole explanation below, is Claude has given me (with a little bit of nudging, of course :).
1.8.1 Mathematical Equality (What You Learned First)
x = a + 1
This equation works both ways: - If a = 5
, then x = 6
- If x = 6
, then a = 5
- It’s a statement of equality - both sides have the same value
1.8.2 Programming Assignment (What You Had to Learn)
= a + 1 # Python x
This only works one direction: - Takes value of a
, adds 1, stores result in x
- You cannot write a + 1 = x
(SyntaxError!) - It’s an action: “compute and store”
1.8.3 Elixir Pattern Matching (Back to Mathematics!)
= a + 1 # If a is bound, x gets bound to a + 1
x + 1 = x # If x is bound, assert that x equals a + 1 a
This works both directions like algebra!
1.9 The “Unlearning” Journey
- Childhood: You learned
x = 5 + 1
means “x equals 6” - Programming school: You unlearned this -
=
now means “assignment” - Elixir: Time to un-unlearn -
=
goes back to meaning “equals”
1.10 Side-by-Side Comparison
Context | Expression | Meaning | Bidirectional? |
---|---|---|---|
Algebra | x = a + 1 |
“x equals a plus 1” | ✅ Yes |
Python | x = a + 1 |
“assign a plus 1 to x” | ❌ No |
Elixir | x = a + 1 |
“x matches a plus 1” | ✅ Yes |
1.11 Why This Matters
In Python, this fails:
6 = 5 + 1 # SyntaxError: can't assign to literal
In Elixir, this works:
6 = 5 + 1 # Success: 6 matches 5 + 1
The insight: Elixir’s =
is bidirectional equality (like math), not unidirectional assignment (like most programming languages).
1.12 Addressing a Common Question
Python Pro: “But we still compute right-to-left, don’t we?”
You’re absolutely right! In practice, we often do:
= 5 + 1 # We calculate 5 + 1 = 6, then bind x to 6 x
This looks just like assignment. But the real magic becomes apparent when x is already bound - that’s where Elixir becomes truly bidirectional:
1.12.1 The Real Magic: Asserting Equality
In Elixir, ^
Pin operator is used to refer to the existing value of variables - these variable are referred to as already-bound
.
This works in Elixir (but fails in Python):
= 6 # x is bound to 6 now
x 6 = x # Success! Assert "6 equals x"
6 = 5 + 1 # Success! Assert "6 equals 5 + 1"
# With pin operator for already-bound variables:
= 5
a ^x = ^a + 1 # Assert "a(5) + 1 equals x(6)" ✓
^a + 1 = ^x # This also works ✓
In Python:
= 6
x 6 = x # SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='?
= a + 1 # This will assign 6 to x and NOT match
x + 1 = x # SyntaxError: cannot assign to expression here. Maybe you meant '==' instead of '='? a
1.12.2 The Key Insight
While computation often flows right-to-left, Elixir’s =
allows bidirectional assertions - you can assert equality in any direction, just like mathematical equations.
Joe’s point: The capability for bidirectional matching is what makes it algebraic, even when we don’t always use it that way. It’s the potential that matters.
1.13 Key Takeaway
Stop thinking “=
means assign to” and start thinking “=
means equals” - just like in mathematics. While we often compute right-to-left, the ability to assert equality in any direction is what makes pattern matching fundamentally different from assignment.
Python Pro
ok - I enjoyed the detour on the meaning of =
that I have forgotten over the years. Ok, what do we get by having this bidirectional equality in Elixir, that I don’t get in python?
AI Elixir Mentor
Great question! The bidirectional equality gives you several powerful capabilities that Python doesn’t have:
1. Data Validation You can assert that your data has the expected structure and values:
Elixir:
# Ensure a function returns exactly what you expect
{:ok, result} = some_function() # # Crashes if not {:ok, value} for example, if function returns {:error, reason}
In Python, you’d need explicit if-statements to check this.
Python:
= some_function()
result if result[0] != "ok":
raise ValueError("Expected success tuple")
= result[1] actual_result
2. Destructuring with Constraints You can extract values while simultaneously validating them:
Elixir:
# Extract coordinates, but only if z is exactly 0
{x, y, 0} = get_coordinates() # Crashes if z ≠ 0
Python:
= get_coordinates()
coords = coords
x, y, z if z != 0:
raise ValueError("Expected z to be 0")
So, this results in lot less code compared to Python and much easier to understand as the error-handling is delegated to Elixir - we work with the matches and if there is no match, Elixir will raise an error.
Python Pro
that is nice - for the first time, I am hearing something concrete from you - I have read a lot of material on pattern matching, but never really understood that the essence here is the bidirectional equality and the joy it gives by enabling us to write concise code.
So, what else should I know about pattern matching?
AI Elixir Mentor
Excellent! I’m glad that clicked for you. Let’s explore a few more powerful pattern matching features:
1.14 Partial Matching with the “Don’t Care” Variable
Sometimes you only care about some parts of the data:
Elixir:
[first, _, third] = ["apple", "banana", "cherry"]
# first = "apple", third = "cherry"
# The underscore _ means "I don't care about this value"
Try this in iex and see what happens. What do you think _
will be bound to?
Python Pro
When I try to print _
in python, it prints banana
. But Elixir got angry with me and gave me severe warnings to behave properly!
iex(35)> [first, _, third] = ["apple", "banana", "cherry"]
["apple", "banana", "cherry"]
iex(36)> _
error: invalid use of _. _ can only be used inside patterns to ignore values and cannot be used in expressions. Make sure you are inside a pattern or change it accordingly
└─ iex:36
** (CompileError) cannot compile code (errors have been logged)
AI Elixir Mentor
Perfect observation! You’ve discovered an important difference between Python and Elixir.
In Python, _
is just a regular variable name (by convention used for “throwaway” values), so it gets the value “banana” and you can print it.
In Elixir, _
is a special language construct that means “match anything but don’t bind it to a variable.” It’s not a variable you can reference later - it’s truly a “don’t care” placeholder.
This is Elixir being more explicit again! If you don’t care about a value, you really can’t access it later. This prevents bugs where you accidentally use a value you intended to ignore.
If you want to ignore a value but still be able to reference it, you can use a named variable starting with underscore:
[first, _middle, third] = ["apple", "banana", "cherry"]
# This works - returns "banana" _middle
Try this and see what happens. Elixir will give you a warning but still work.
Python Pro
Yes - true to what you said, Elixir politely informed me about the mismatch between my intention and then actual code, but it still printed banana
iex(37)> _middle # This works - returns "banana"
warning: the underscored variable "_middle" is used after being set. A leading underscore indicates that the value of the variable should be ignored. If this is intended please rename the variable to remove the underscore
└─ iex:37
"banana"
AI Elixir Mentor
That is great. Now let us look at another oft-used pattern.
1.15 Head and Tail Matching
In python, if you want to extract a list into 2 parts - head variable containing the first element and the tail variable containing a list with the rest of the elements, the unpacking operator *
is handy.
Python:
*tail = [1, 2, 3, 4]
head, # head = 1, tail = [2, 3, 4]
The same idea works, but the syntax is a bit different - pay attention to the space around |
- this symbol is not attached to the tail variable in Elixir as *
symbol was attached in python.
Elixir:
[head | tail] = [1, 2, 3, 4]
# head = 1, tail = [2, 3, 4]
The corner cases work exactly like python - First try these statements in python and see what you get. Then translate these 2 python statements into Elixir syntax (remember Elixir is explicit) and see whether the behaviour is similar.
*tail = [1, 2] # would tail be a list in python and in elixir?
head,
*tail = [1] # what would tail be in python and in elixir?
head,
*tail = [] # would this work in python and in elixir? head,
Python Pro
Yes I tried those 3 statements one by one in ipython and here is what I got:
In [15]: head, *tail = [1, 2]
In [16]: head
Out[16]: 1
In [17]: tail
Out[17]: [2]
In [18]: head, *tail = [1]
In [19]: head
Out[19]: 1
In [20]: tail
Out[20]: []
In [21]: head, *tail = []
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[21], line 1
----> 1 head, *tail = []
ValueError: not enough values to unpack (expected at least 1, got 0)
Then I translated them into Elixir and I got a similar behaviour.
iex(39)> [ head | tail ] = [1, 2]
[1, 2]
iex(40)> head
1
iex(41)> tail
[2]
iex(42)> [ head | tail ] = [1]
[1]
iex(43)> head
1
iex(44)> tail
[]
iex(45)> [ head | tail ] = []
** (MatchError) no match of right hand side value: []
(stdlib 6.2.2) erl_eval.erl:667: :erl_eval.expr/6
iex:45: (file)
AI Elixir Mentor
Excellent! You’ve discovered that Python and Elixir handle head/tail decomposition very similarly:
✅ Both extract the first element as head
✅ Both put remaining elements in tail
as a list (even if empty)
✅ Both fail when trying to extract from an empty list
The key difference is in error handling: - Python: Raises ValueError
with a descriptive message - Elixir: Raises MatchError
- pattern matching failed
This head/tail pattern is extremely common in functional programming. You’ll see [head | tail]
everywhere in Elixir code for processing lists recursively.
1.16 Multiple Elements
You can also extract multiple elements from the head:
[first, second | rest] = [1, 2, 3, 4, 5]
# first = 1, second = 2, rest = [3, 4, 5]
Try this and see what happens when you have exactly 2 elements: [first, second | rest] = [1, 2]
What do you think rest
will be?
Python Pro
As expected rest
is an empty list.
I love the flexibility of star operator in python - can we do something like this in Elixir?
*first, last = [1, 2, 3, 4] # I want the tail alone and the rest in a list
*middle, last = [1, 2, 3, 4] # I want the first and last alone and the rest in a list first,
AI Elixir Mentor
Great question! Unfortunately, Elixir’s |
operator is more limited than Python’s *
operator.
In Elixir, you can only use |
to separate the beginning of a list from the rest. You cannot: - Extract from the end: *first, last
❌ - Extract from the middle: first, *middle, last
❌
So these Python patterns don’t have direct Elixir equivalents:
Python (works):
*first, last = [1, 2, 3, 4] # first=[1,2,3], last=4
*middle, last = [1, 2, 3, 4] # first=1, middle=[2,3], last=4 first,
Elixir (doesn’t work):
[first | last] = [1, 2, 3, 4] # This is head|tail, not *first,last
[first, middle | last] = [1, 2, 3, 4] # This is NOT first,*middle,last
In Elixir, you’d need to use other functions like List.last/1
or Enum.take/2
for these patterns. But the good news is that the head|tail pattern [head | tail]
covers most common use cases in functional programming.
This is one area where Python’s unpacking is more flexible than Elixir’s pattern matching.
Python Pro
Haha. You have finally hit upon a place where Elixir is less powerful than Python. Tell me more - you were always bragging about Elixir’s superiority. Now I guess, you will make up some valid-sounding reason for why extracting the first part or the middle part is NOT that imporant.
AI Elixir Mentor
Haha, you caught me! You’re absolutely right - I have been highlighting Elixir’s strengths, and now you’ve found a legitimate area where Python is more flexible.
I could try to rationalize this by saying “functional programming typically processes lists from head to tail, so you rarely need to extract from the end…” but that would be exactly the kind of excuse-making you’re calling out! 😄
The honest truth is: Python’s unpacking is more powerful and flexible than Elixir’s pattern matching for lists. Period.
Elixir’s [head | tail]
pattern is optimized for the most common functional programming use case (recursive list processing), but it’s definitely more limited than Python’s *
operator.
This is a good reminder that no language is perfect at everything. Elixir makes trade-offs - it gains other powerful pattern matching features (like matching on data structure shapes, which we haven’t covered yet), but loses some of Python’s unpacking flexibility.
You’ve discovered an important principle: always question the hype around any technology. Every language has strengths and weaknesses, and it’s good to keep a critical eye!
So what do you think - should we explore some areas where Elixir’s pattern matching is more powerful than Python, or do you want to keep poking holes in my Elixir enthusiasm? 😉
Python Pro
It is OK - I just want to poke you once to bring you to terra firma (as you were in cloud nine all the time about Elixir). Anyway, I am excited about Elixir as well, so - no more poking at you. Are we done with pattern matching - if yes, please summarise what we have covered? if not, tell me what we are missing.
AI Elixir Mentor
Fair enough! You’ve successfully brought me back to earth. 😄
We’ve covered the core fundamentals of pattern matching that you’ll use 80% of the time:
1.17 What We’ve Covered ✅
1. Basic Concepts - =
as match operator (bidirectional equality) - Pattern matching vs assignment - Explicit structure matching: [a, b, c] = list
2. Practical Patterns - Variables: x = 5
- Lists: [first, second, third] = list
- Ignoring values: [first, _, third] = list
- Head/tail: [head | tail] = list
- Multiple head elements: [first, second | rest] = list
- Pin operator ^
for matching existing variables
3. Key Benefits - Data validation (crashes on mismatch) - Concise destructuring with constraints - Less error-checking code needed
1.18 What We Haven’t Covered (Advanced Topics)
- Pattern matching on tuples and maps (Elixir’s key-value structures)
- Guards (adding conditions to patterns)
- Pattern matching in function definitions
But honestly? What we’ve covered gives you the foundation for 80% of pattern matching use cases. The advanced topics can wait until you’re more comfortable with Elixir.
Ready to move on to our second core concept: immutability?
1.19 Immutability
Python Pro
yes my dear Elixir mentor.
AI Elixir Mentor
Excellent! Let’s dive into immutability - another core concept that makes Elixir different from Python.
1.20 Quick Question First
In Python, what happens when you do this:
= [1, 2, 3]
numbers 4)
numbers.append(print(numbers) # What gets printed?
And what happens to any other variables that were pointing to the same list?
= numbers
backup 5)
numbers.append(print(backup) # What gets printed here?
Try this in Python first, then tell me what you observe. This will help us understand why immutability matters.
Python Pro
backup is also modified to [1, 2, 3, 4, 5]. And that is expected as lists are references in python like arrays are pointers in C and C++. So, no big deal.
AI Elixir Mentor
Exactly! You understand the issue perfectly. In Python, backup
and numbers
point to the same list object in memory, so modifying one affects the other.
This can lead to subtle bugs:
def process_data(data_list):
"processed") # Oops! Modified the original
data_list.append(return data_list
= [1, 2, 3]
original = process_data(original)
result print(original) # [1, 2, 3, "processed"] - Surprise!
1.21 Elixir’s Solution: Everything is Immutable
In Elixir, you cannot modify existing data. Period.
Try this in iex:
= [1, 2, 3] numbers
Now, how would you “add” 4 to this list? There’s no append
method. Try typing numbers.
and see what tab completion suggests (if any).
What do you think happens when you try to “modify” a list in Elixir?
Python Pro
when I press tab after typing numbers.
i don’t seem to get anything, may be tab completion is not working
AI Elixir Mentor
That’s actually the point! There are no methods on the list itself because lists are immutable - you can’t modify them directly.
Instead, Elixir provides functions that create new lists based on existing ones. Try this:
= [1, 2, 3]
numbers = [4 | numbers] new_numbers
What do you get? And then check what numbers
contains - has it changed?
Alternatively, you can try:
= numbers ++ [5] new_numbers
The key insight: Instead of modifying existing data, Elixir creates new data structures. This means numbers
will always contain [1, 2, 3]
no matter what you do with new_numbers
.
Python Pro
Here is what I got, when I tried your suggestion, but there is something curious in the output of ++
operator. It shows that tail operator |
, rather than comma to separate the new element whereas the comma is used when I used the tail operator |
- is Elixir playing it funny?
iex(49)> new_numbers = [4 | numbers]
[4, 1, 2, 3]
iex(50)> numbers
[1, 2, 3]
iex(51)> new_numbers = numbers ++ 5
[1, 2, 3 | 5]
iex(52)> new_numbers
[1, 2, 3 | 5]
iex(53)> numbers
[1, 2, 3]
AI Elixir Mentor
Great observation! You’ve discovered something important about how Elixir displays lists.
The issue is with this line:
= numbers ++ 5 new_numbers
The ++
operator expects two lists, but you gave it a list and a number. So Elixir created what’s called an “improper list” - a list that doesn’t end with an empty list []
.
That’s why it displays as [1, 2, 3 | 5]
- Elixir is showing you the internal structure where the tail is 5
(a number) instead of []
(empty list).
Try this instead:
= numbers ++ [5] new_numbers
Notice the square brackets around 5
? Now you’re concatenating two proper lists: [1, 2, 3]
and [5]
.
Summary of list operations: - [new_element | existing_list]
- prepends to front - existing_list ++ [new_element]
- appends to end
- existing_list ++ other_list
- concatenates two lists
Try the corrected version and see how it displays now!
Python Pro
Yes that looks good now.
AI Elixir Mentor
Perfect! Now you see the difference:
numbers ++ [5]
creates[1, 2, 3, 5]
(proper list)numbers ++ 5
created[1, 2, 3 | 5]
(improper list)
1.22 The Key Point About Immutability
Notice that in all these operations, the original numbers
list never changed. It’s still [1, 2, 3]
. This is immutability in action.
Benefits: 1. No surprise mutations - numbers
will always be [1, 2, 3]
2. Safe sharing - You can pass numbers
to any function without worrying it will be modified 3. Easier debugging - Data doesn’t change unexpectedly
The trade-off: - Creating new data structures uses more memory (but Elixir optimizes this behind the scenes)
Let’s test your understanding: If you have a function that needs to “modify” a list, what does it actually do in Elixir? And how would you use the result?
Python Pro
I guess, it would make a copy first, then do the modification and return the copy. I think this idea of making a copy first and using that to modify is used by Pandas quite heavily as we could chain multiple operations on a dataframe. Am I right?
AI Elixir Mentor
You’re thinking in the right direction, but there’s a subtle difference!
Pandas approach: Makes copies and allows method chaining
0).groupby('column').sum() # Each method returns a new DataFrame df.dropna().fillna(
Elixir approach: Functions return new data structures, but you need to explicitly capture the result
In Elixir, functions don’t modify - they transform. So if you want to apply multiple transformations, you’d do:
# Step by step
= [1, 2, 3]
numbers = [0 | numbers] # [0, 1, 2, 3]
step1 = step1 ++ [4] # [0, 1, 2, 3, 4]
step2 = [99 | step2] # [99, 0, 1, 2, 3, 4] final
But this gets verbose. That’s where our third core concept comes in: piping! It lets you chain operations elegantly, just like Pandas.
We’ll cover that next, but first - try creating a list that starts with [1, 2, 3]
, adds 4
to the end, then adds 0
to the beginning. What do you get?
Python Pro
I got [0, 1, 2, 3, 4].
AI Elixir Mentor
Perfect! You successfully chained the operations:
- Started with
[1, 2, 3]
- Added
4
to the end:[1, 2, 3, 4]
- Added
0
to the beginning:[0, 1, 2, 3, 4]
Now you can see how immutability works in practice - each step created a new list without modifying the previous ones.
1.23 Quick Summary: Immutability
✅ No mutations - data never changes after creation
✅ Safe sharing - no surprise side effects
✅ Functions transform - they return new data instead of modifying existing data
The downside is that chaining operations can get verbose with intermediate variables. But that’s exactly what our third concept solves!
Ready to learn about piping - Elixir’s elegant solution for chaining operations?
Python Pro
But before we start piping, tell me how Elixir manages this whole mess of all the variables left behind by each operation and the ton of memory it would consume?
AI Elixir Mentor
Excellent question! This is a crucial concern when moving from mutable to immutable languages.
1.24 Elixir’s Memory Magic
1. Structural Sharing Elixir doesn’t actually copy the entire data structure. It shares unchanged parts:
= [1, 2, 3]
original = [0 | original] # [0, 1, 2, 3] new_list
Behind the scenes: - new_list
creates a new “head” node pointing to 0
- The tail points to the same memory as original
- Only the new head node uses extra memory!
2. Garbage Collection When variables go out of scope, Elixir’s garbage collector automatically reclaims unused memory:
def process_data() do
= [1, 2, 3]
step1 = [0 | step1] # step1 might be collected if not used elsewhere
step2 = step2 ++ [4] # step2 might be collected
step3 # Only step3 survives
step3 end
3. BEAM VM Optimizations The Erlang VM (BEAM) that runs Elixir has decades of optimization for exactly this pattern.
The Result: In practice, immutable operations in Elixir are surprisingly memory-efficient and often faster than you’d expect!
This is why functional languages can be both safe AND performant.
Python Pro
Ok we have covered 2 topics - pattern matching and immutability. Please summarise both and tell me what is next.
AI Elixir Mentor
1.25 Summary So Far
1. Pattern Matching ✅ - =
is match operator (bidirectional equality), not assignment - Explicit structure: [a, b, c] = list
- Head/tail decomposition: [head | tail] = list
- Data validation: crashes on mismatch - Benefits: Concise code, built-in validation
2. Immutability ✅ - No mutations - data never changes after creation - Functions return new data instead of modifying existing - Safe sharing - no surprise side effects - Memory efficient through structural sharing and garbage collection
Before we look at piping, I want you to look at how loops are handled in Elixir. Just to get you a bit excited! - there is no traditional for loop in Elixir :). Have you heard of that?
Python Pro
Hmm. That is wild - I thought that conditionals and looping are the bread and butter of programming.
AI Elixir Mentor
You’re absolutely right - conditionals and looping ARE fundamental! But Elixir takes a different approach to looping that’s actually more powerful once you get used to it.
Instead of traditional for
loops, Elixir uses functions that operate on collections. Let me show you with a familiar example:
Python:
= [1, 2, 3, 4]
numbers for num in numbers:
print(num * 2)
Elixir equivalent:
= [1, 2, 3, 4]
numbers Enum.each(numbers, fn num -> IO.puts(num * 2) end)
But this Elixir code introduces two new concepts we have not yet covered: 1. Enum module and its utility functions for working with collections like lists, tuples, etc. 2. Anonymous functions
But before we cover these 2 new ideas, let us take a Detour to really understand the different paradigms that are available in python to do a simple loop to print the values in a list, one by one.
1.26 Detour - Different paradigms to loop over a list
When working with collections, looping over items (iteration) is one of the most common operations. You constantly need to: - Process each item (validate, transform, display) - Find specific items (search, filter) - Aggregate data (sum, count, analyze)
Python’s approach to iteration has evolved significantly, inheriting ideas from different programming paradigms:
1.27 Evolution of Python’s Iteration Mental Models
1. C-Style Indexing (Inherited from Traditional Languages)
= ["apple", "banana", "cherry"]
fruits for i in range(len(fruits)):
print(f"{i}: {fruits[i]}")
Mental Model: “Manually control the counter, access by index”
From: C, Java, early programming languages
Advantages: Explicit control, familiar to C programmers
Drawbacks: Verbose, managing index bounds manually can sometimes lead to hard-to-find bugs
2. Direct Iteration (Idiomatic Python Looping)
for fruit in fruits:
print(fruit) # But we lost the index!
Mental Model: “Iterate over objects directly, not indices”
From: Python’s object-oriented nature, iterator protocol
Advantages: Cleaner, safer, more Pythonic
Drawbacks: Lost access to index when needed
3. Enumerate (Pythonic Best Practice)
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}")
Mental Model: “Get both item and position naturally”
From: Python’s tuple unpacking + built-in functions
Advantages: Clean syntax, provides both index and value when needed, Pythonic, no manual index management, works with any iterable
Drawbacks: Still allows side effects inside the loop, can modify external state, potential for bugs when loop body becomes complex
4. Functional Style (Borrowed from Functional Programming)
= lambda x: print(f"{x[0]}: {x[1]}")
print_fruit list(map(print_fruit, enumerate(fruits)))
Mental Model: “Transform data, then apply functions”
From: Functional programming languages (Lisp, Haskell)
Advantages: Separates data transformation from processing, encourages pure functions, more composable, easier to reason about data flow
Drawbacks: Less readable for Python developers unfamiliar with functional style, list(map(...))
is verbose, lambda syntax can be cryptic for complex operations
Each evolution solved real problems while building on Python’s growing capabilities. Notice how Python gradually moved from “manual control” to “declarative intent.”
Python Pro
I really like this detour. I am able to see how Python allows for all these different possibilities. How does Elixir compare? Does it only allow me to do 4. Functional style
only?
The challenge I have with this kind of a functional code is that people write even the lambda inside map - hence it results in lot of mental gymnastics to really understand what is going on and where the brackets for each operation start and stop.
And even though generators are great from a conceptual idea perspective, they make it to hard to work in a REPL environment doing interactive coding as I have to often call list to see the results. I wish there is a better way in Python - how are these challenges tackled in Elixir?
AI Elixir Mentor
AI Elixir Mentor
Excellent observations! You’ve hit on some real pain points with functional programming in Python. Elixir addresses these challenges elegantly:
Let me use the exact same example listed under 4. Functional Style
to show how Elixir solves both challenges:
1.28 Challenge 1: Bracket Hell and Mental Gymnastics
Python (hard to read - nested brackets):
= ["apple", "banana", "cherry"]
fruits list(map(lambda x: print(f"{x[0]}: {x[1]}"), enumerate(fruits)))
Elixir (reads left to right):
= ["apple", "banana", "cherry"]
fruits
fruits|> Enum.with_index() # Like enumerate()
|> Enum.each(fn {fruit, index} -> # fn is like lambda - indicates an anonymous function, but it can span multiple lines
IO.puts("#{index}: #{fruit}")
end)
Don’t worry about each step now - just get an overall idea of how readable the piping syntax is compared to the bracket syntax. We will cover the Enum utility functions as well as anonymous functions.
No nested brackets! You read from top to bottom: “Take fruits, add indices, then print each one.”
1.29 Challenge 2: REPL Unfriendly (No list()
Wrapper Needed)
Python (need list() to see results):
= map(lambda x: print(f"{x[0]}: {x[1]}"), enumerate(fruits)) # generator prints memory address :)
result list(result) # Actually see the output
Elixir (immediate results in iex):
fruits|> Enum.with_index() # Returns actual list immediately: [{"apple", 0}, {"banana", 1}, {"cherry", 2}]
Please don’t try to execute these examples yet in iex - I just want to give you an idea of how the different mental models for looping in Python compares to the single model in Elixir. Though I told you that there is no For loop, there is actually a for construct in Elixir, but we will tuck it away for now and come back to it once the timing for it is perfect.
So, how do you feel about the functional style in Elixir?
Python Pro
Not having to do the list conversion each time is a great timesaver for me. But I really like the following simple idiomatic python way of going through each item inside a for loop.
for fruit in fruits:
print(fruit)
To me, the idiomatic python code is so easy-to-read compared to the functional alternative. Why is this not supported in Elixir? Is that not functional? Why do I have to do this map thing. That always seems a little bit counterintuitive to me.
AI Elixir Mentor
You’re absolutely right! That idiomatic Python for loop is beautifully readable and intuitive. Your frustration is completely valid! - See I also came from Python to Elixir. So I still love certain aspects of Python :)
The reason goes back to immutability. Traditional for loops are designed around mutation:
1.30 3 Core Functional Patterns
Python Pro
Ok. That is a simple yet very clear and concrete explanation of why mutation inside a for loop can be dangerous. So, tell me if I want to make the switch to thinking more functionally, what should I do?
AI Elixir Mentor
Great question! Here’s a practical roadmap to shift your thinking from imperative to functional:
1.31 Mental Model Shift: From “Do Things” to “Transform Things”
Instead of thinking: “Loop through and modify each item”
Think: “Transform this collection into a new collection”
1.32 The 3 Core Functional Patterns
1. Map - Transform each item
“Make all fruits uppercase”
2. Filter - Keep only some items
“Keep only fruits with more than 5 letters”
3. Reduce - Combine all items into one result
“Count total letters in all fruits”
That’s it! Every loop you’ve ever written falls into one of these three patterns.
Python Pro
Wow - that is a very clear and concise way of thinking about it.
Let us go through a few examples that illustrate the different types of operations we do, under each of these 3 core functional patterns - map, filter and reduce.
AI Elixir Mentor
Perfect! Let’s go through concrete examples that show how common operations fit these patterns - Please skim the examples to get an overall ideas - we will work through each of these cases one by one and see how to translate them into Elixir.
1.33 1. MAP - Transform Each Item
“I want to change every item in some way”
Python examples:
# Make all uppercase
for fruit in fruits]
[fruit.upper()
# Add tax to prices
* 1.08 for price in prices]
[price
# Get string lengths
len(word) for word in words] [
The pattern: Input list → Same number of items → Different values
1.34 2. FILTER - Keep Only Some Items
“I want to keep only items that meet a condition”
Python examples:
# Only long words
for word in words if len(word) > 5]
[word
# Only positive numbers
for num in numbers if num > 0]
[num
# Only fruits starting with `a`
for fruit in fruits if fruit.startswith("a")] [fruit
The pattern: Input list → Fewer (or same) items → Same values
1.35 3. REDUCE - Combine All Into One Result
“I want to combine all items into a single answer”
Python examples:
# Sum all numbers
sum(numbers)
# Find maximum
max(numbers)
# Join into string
", ".join(fruits)
The pattern: Input list → Single result
1.36 Core Functional Pattern 1 - Map
Python Pro
Let us start with problem 1.1 under Map - Make all uppercase.
= ["apple", "banana", "cherry"]
fruits for fruit in fruits]
[fruit.upper() # or the functional style
list(map(lambda x: x.upper(), fruits))
How do I transform this into Elixir? Please do not give me the answer - if you have NOT covered all the ingredients to solve this problem, then explain them first.
I would like to learn by trying it myself and going through the struggle before peeking at the solution - research proves that this is the best way to learn for the long-term as these struggles create hooks in our brain to receive the answers and remember them for the long-term - if I read through your solution, it might feel good in the short-term, but I am unlikely to remember it in the long-term.
AI Elixir Mentor
Excellent approach! You’re absolutely right about learning through struggle - it creates much stronger memory pathways.
You’ll need three things we haven’t covered yet:
1.37 Ingredient 1: Anonymous Functions
In Python, you’d use a lambda. In Elixir, you use fn
:
Python:
lambda x: x.upper()
Elixir:
fn fruit -> String.upcase(fruit) end
The syntax is: fn parameter -> body end
Though I have shown you a single line anonymous function, it is not a requirement in Elixir. I just want to keep things similar to reduce the mental burden on you.
1.38 Ingredient 2: The Enum Module
Elixir’s Enum
module contains all the functions for working with collections (lists, etc.). It’s like Python’s built-in functions (map
, filter
) but organized in a module.
Try this in iex to explore:
Enum h
You’ll see a lot of information - scroll up to find the starting point.
Another way is to look up the same information in hexdocs: Enum — Elixir v1.18.4
Check this out and compare what you see in iex and at hexdocs - are they the same? hexdocs has a lot more information than iex. And did you notice that there is even a cheatsheet - for now, don’t go there as the examples there use a map which we have not yet covered.
So, you have 2 choices to lookup docs - for small and quick things, you can use iex, but for long ones like this, it is better to use a browser. You can also use Cmd/Ctrl-K
to search for items quickly.
To figure out what function in Enum to use, search for the first pattern name - map
and see what you get. Also see the same docs in iex using h Enum.map
. Don’t worry about the spec and any other thing you don’t understand - just focus on the examples and see whether you understand them. Copy and paste them into iex to understand how they work.
1.39 Ingredient 3: String Functions
The last piece of the puzzle is to figure out a function to transform from lowercase to uppercase. Lookup docs String.upcase
function in iex using h String.upcase
.
Now try to piece these together! You have: - A list: ["apple", "banana", "cherry"]
- An Enum function that transforms each element - An anonymous function that uppercases a string
And here is the code you have translate:
= ["apple", "banana", "cherry"]
fruits list(map(lambda x: x.upper(), fruits))
Python Pro
yes I got it working
= ["apple", "banana", "cherry"]
fruits Enum.map(fruits, fn x -> String.upcase(x) end)
AI Elixir Mentor
Excellent work! You successfully pieced together all three ingredients:
✅ Enum.map
for transforming each element
✅ Anonymous function fn x -> String.upcase(x) end
✅ String.upcase
for the transformation
Your Elixir code is a direct translation of the Python functional style:
Python:
list(map(lambda x: x.upper(), fruits))
Your Elixir:
Enum.map(fruits, fn x -> String.upcase(x) end)
Notice how similar they are! The main differences: - list(map(...))
becomes Enum.map(...)
- lambda x:
becomes fn x ->
- x.upper()
becomes String.upcase(x)
- end
instead of just closing parentheses
Now let’s see if you can tackle the next one. Here’s problem 1.2:
Python:
= [10.0, 25.50, 8.75]
prices # intentionally I am NOT giving you the functional equivalent in python to help you flex your mental muscles :)
* 1.08 for price in prices] # Add 8% tax [price
Try translating this to Elixir. You already have all the ingredients you need!
Python Pro
yes I got it.
= [10.0, 25.50, 8.75]
prices Enum.map(prices, fn x -> x * 1.08 end)
AI Elixir Mentor
Perfect! You’ve got the pattern down. Notice how the anonymous function fn x -> x * 1.08 end
directly mirrors the Python lambda logic.
You’re seeing that once you understand the basic structure of Enum.map(collection, fn item -> transformation end)
, you can handle any transformation.
Let’s try one more map example before moving to filter. Here’s problem 1.3:
Python:
= ["cat", "elephant", "dog", "butterfly"]
words len(word) for word in words] # Get string lengths [
This one requires you to find the right function to get string length. Try looking up string length functions in Elixir.
Python Pro
I nailed this one too.
= ["cat", "elephant", "dog", "butterfly"]
words Enum.map(words, fn x -> String.length(x) end)
AI Elixir Mentor
Fantastic! You’re really getting the hang of this pattern. You correctly found String.length/1
and applied it in the anonymous function.
You’ve now mastered the MAP pattern:
Transform each item in a collection
Get back the same number of items
Use
Enum.map(collection, fn item -> transformation end)
Before we move on to the next pattern FILTER, I want you to give you a few detour to help you understand a few things about Elixir - some of them are easy and some of them might feel strange at first - but you will see them in Phoenix often, so I want you to be fully prepared.
1.40 Detour - How Functions are Organized in Elixir vs Python
The Problem: Converting a String to Uppercase
Let’s just return to the simple problem we handled in our first task - to convert “hello world” to “HELLO WORLD”. This simple task reveals a fundamental difference in how Python and Elixir organize code.
1.41 Python’s Object-Oriented Approach
= "hello world"
text = text.upper()
result print(result) # "HELLO WORLD"
How it works: - The string "hello world"
is an object - The upper()
method belongs to the string object - We call the method on the object: text.upper()
- Fortunately, the String function return a new string without modifying the original, just like the functional way, - but many python functions mutate the data held in the object e.g. list.append(“no new lists please”) - The object transforms itself
This is the Object-Oriented way: “Hey string, uppercase yourself!”
1.42 Elixir’s Functional Approach
= "hello world"
text = String.upcase(text)
result IO.puts(result) # "HELLO WORLD"
How it works: - The string "hello world"
is just data - The upcase
function belongs to the String
module - We pass the data to the function: String.upcase(text)
- The function creates new data without changing the original
This is the Functional way: “Hey String module, create an uppercase version of this data!”
1.43 The Mental Model Difference
1.43.1 Python: Methods Belong to Objects
# Methods are attached to the data
= "hello world"
text # Method called ON the string
text.upper() # Method called ON the string
text.split() "l", "x")
text.replace(
= [1, 2, 3]
numbers 4) # Method called ON the list numbers.append(
1.43.2 Elixir: Functions Belong to Modules
# Functions live in modules, data is passed TO them
= "hello world"
text String.upcase(text) # Function takes string as argument
String.split(text) # Function takes string as argument
String.replace(text, "l", "x") # Function takes string as argument
= [1, 2, 3]
numbers List.insert_at(numbers, -1, 4) # Function takes list as argument
1.44 Why This Difference Matters
Python’s way: Data carries behavior with it - "hello".upper()
- the string knows how to uppercase itself - Each data type has its own methods
Elixir’s way: Behavior is separate from data - String.upcase("hello")
- the String module knows how to work with strings - Data is just data, functions transform it
1.45 Getting Help
Python: Ask the object what it can do
help(str) # Shows string methods
dir("hello") # Lists available methods for the string object "hello"
Elixir: Ask the module what functions it has
String.upcase # Shows function documentation
h String # Shows all String module functions h
This mental shift from “objects with methods” to “modules with functions” is fundamental to thinking functionally in Elixir.
Python Pro
Hmmm. That is interesting - but why did Elixir use upcase
instead of uppercase
- I searched for upper
in hexdocs, but could not really figure out the function and I had to go through the entire list of functions to find it. Why change something that is well-established?
AI Elixir Mentor
Great question! This is actually a common frustration when switching between languages. The naming differences come from Elixir’s heritage and design philosophy:
1.46 The Heritage Factor
Elixir is built on Erlang, which has been around since the 1980s. Many of Elixir’s naming conventions come from Erlang’s established patterns:
- Erlang used
upcase
/downcase
- Ruby also uses
upcase
/downcase
- Jose Valim (Elixir’s creator) came from Ruby, so he kept familiar patterns
1.47 Different Language Communities, Different Conventions
Python tradition: upper()
, lower()
Ruby/Erlang tradition: upcase()
, downcase()
JavaScript tradition: toUpperCase()
, toLowerCase()
None is “more correct” - they’re just different community conventions that evolved independently.
1.48 The Search Strategy
You’re right that searching can be tricky! Here are better strategies:
- Search for the concept, not the exact word: Search “case” instead of “upper”
- Use the module overview: Look at
h String
first to see all available functions - Think in terms of the target language: When in Elixir-land, think “upcase” not “upper”
It’s like learning that “elevator” (US) vs “lift” (UK) - same concept, different words. You just have to adapt to the local vocabulary!
The good news: once you learn a few modules, the patterns become predictable.
Python Pro
Ah! I never realized that Ruby and Erlang had this kind of a convention. It’s surprising how often we equate familiarity with correctness. Thanks for helping me confront my confirmation bias. So, what is the next detour?
AI Elixir Mentor
Exactly! That’s a profound insight - familiarity often masquerades as correctness. It’s one of the biggest hurdles when learning new languages or paradigms.
Next few detours are about the variations of anonymous functions you have written as you will see them quite a lot - But before we tackle the more tricky aspects of the anon functions, you need to understand 2 things well - Everything is an Expression in Elixir and Why Parantheses are Optional in Elixir.
2 Detour - Everything is an Expression in Elixir
One of the most fundamental differences between Elixir and Python is how they handle code execution. Understanding this difference is crucial to make the transition to an Elixir pro.
2.1 The Big Difference
Python has both statements and expressions: - Statements do things but don’t return values (if
, for
, def
, etc.) - Expressions calculate and return values (2 + 3
, len("hello")
, etc.)
Elixir has only expressions: - Everything returns a value - Everything can be used wherever a value is expected
2.2 What This Means in Practice
2.2.1 Example 1: Basic Assignment and Function Calls
Python:
# This works - using expressions
= [1, 2, 3]
numbers = len(numbers) # len() is an expression, returns 3 result
Elixir:
# This also works - everything is an expression
= [1, 2, 3]
numbers = length(numbers) # length() is an expression, returns 3 result
2.2.2 Example 2: Using Conditional Logic
Python:
# This doesn't work - can't use if statement as expression
# result = if True: 5 else: 3 # SyntaxError!
# You need the ternary operator (which IS an expression)
= 5 if True else 3 # This works result
Elixir:
# This works perfectly - if is an expression that returns a value
= if true do
result 5
else
3
end
# result is 5
2.2.3 Example 3: Assignment Inside Function Calls
Python:
# Assignment is a statement, doesn't return a value
= 10
x # print(y = x + 5) # SyntaxError! Assignment doesn't return anything
Elixir:
# Assignment returns the assigned value
= 10
x IO.puts(y = x + 5) # This works! Prints 15, and y is also set to 15
2.2.4 Example 4: Complex Conditionals
Python:
# if is a statement - it does something but doesn't return a value
= 10
x if x > 5:
= "big number"
message else:
= "small number"
message
print(message) # "big number"
Elixir:
# if is an expression - it returns a value
= 10
x = if x > 5 do
message "big number"
else
"small number"
end
IO.puts(message) # "big number"
# You can even nest if expressions inside other expressions
IO.puts(if x > 5 do
"big number"
else
"small number"
end)
2.2.5 Example 5: Function Definitions
Python:
# def is a statement - creates a function but doesn't return it
def add(x, y):
return x + y
# Functions are objects, but def doesn't return the function
# print(def add(x, y): return x + y) # SyntaxError!
# To create and use a function in one expression, you need lambda
= (lambda x, y: x + y)(3, 4) # Returns 7 result
Elixir:
# def is also used to create named functions
def add(x, y) do
+ y
x end
# In Elixir, you can't use def inside expressions either
# IO.puts(def add(x, y), do: x + y) # This won't work as expected!
# To create and use a function in one expression, you use fn
= (fn x, y -> x + y end).(3, 4) # Returns 7 result
2.3 Pattern Matching: Also an Expression
Python:
# Assignment doesn't return a value
# You can't do: print(x, y = (1, 2)) # SyntaxError!
# You have to separate assignment from usage
= (1, 2)
x, y print(x, y)
Elixir:
# Pattern matching returns the matched value
IO.puts({x, y} = {1, 2}) # This works! Prints {1, 2}, and sets x=1, y=2
# You can use pattern matching in complex expressions
IO.puts(case {:ok, "hello"} do
{:ok, message} -> "Success: #{message}"
{:error, reason} -> "Failed: #{reason}"
end)
# Prints: "Success: hello"
2.4 Why This Matters for Data Transformation
Python:
# You need to break operations into statements
= [1, 2, 3, 4, 5]
numbers = [x * 2 for x in numbers]
doubled = [x for x in doubled if x > 4]
filtered = sum(filtered)
result # result is 18
Elixir:
# You can chain and compose operations naturally
= [1, 2, 3, 4, 5]
result |> Enum.map(fn x -> x * 2 end)
|> Enum.filter(fn x -> x > 4 end)
|> Enum.sum()
# result is 18
# Each step returns a value that feeds into the next step
2.5 The Mental Model
Python: “Do this, then do that, then do the other thing” - Mix of statements (do things) and expressions (return values) - More verbose when chaining operations
Elixir: “Calculate this value by combining these smaller values” - Everything returns a value - Natural composition and chaining - Functions are just expressions that return values
This expression-oriented thinking is what makes Elixir’s functional programming style so elegant and composable. When we later explore anonymous functions and the capture operator, you’ll see how this “everything returns a value” principle makes the syntax feel natural and consistent.
Python Pro
That is beautiful - almost artistic. So, let us see why parantheses are optional. I am wondering how that is even possible.
AI Elixir Mentor
Great question! The optional parentheses in Elixir can seem magical at first, but there’s elegant logic behind it.
Read the detour and let me know if you have any questions.
2.6 Detour - Why Parentheses are Optional in Elixir
One of the first things you’ll notice when reading Elixir code is that parentheses around function arguments are often missing. Coming from languages like Python where parentheses are required, this can be confusing. Let’s understand why Elixir allows this and when you actually need them.
2.7 The Foundation: Everything is an Expression
As we covered in our previous discussion about expressions, Elixir treats everything as an expression that returns a value. This design philosophy extends to function calls - they’re just expressions that happen to take arguments and return values.
Since function calls are expressions, Elixir’s parser can often figure out what you mean without requiring explicit parentheses.
2.8 Python vs Elixir: Function Call Syntax
2.8.1 Python: Parentheses Always Required
# In Python, you MUST use parentheses for function calls
len([1, 2, 3]) # Required
print("hello") # Required
str.upper("hello") # Required
# Without parentheses, you get the function object, not a call
len # Returns: <built-in function len>
print # Returns: <built-in function print>
2.8.2 Elixir: Parentheses Often Optional
# In Elixir, parentheses are often optional
[1, 2, 3] # Works fine
length IO.puts "hello" # Works fine
String.upcase "hello" # Works fine
# With parentheses (also valid)
([1, 2, 3]) # Also works
lengthIO.puts("hello") # Also works
String.upcase("hello") # Also works
2.9 When Parentheses Are Optional
Parentheses can be omitted in most straightforward function calls:
# Single argument
IO.puts "Hello World"
String.length "elixir"
Integer.parse "42"
# Multiple arguments
String.replace "hello world", "world", "elixir"
Enum.map [1, 2, 3], fn x -> x * 2 end
Math.pow 2, 3
We have NOT covered piping in detail, but just skim it to get an idea - this is intended as a starter to whet your appetite; full-course will be served in due time.
# Chained function calls (pipe operator)
"hello world"
|> String.upcase
|> String.reverse
|> IO.puts
2.10 When Parentheses Are Required
There are several situations where you MUST use parentheses:
2.10.1 1. Zero-Argument Functions
# Zero arguments - parentheses required
System.system_time() # Must have parentheses
:rand.uniform() # Must have parentheses - the atom style module refers to Erlang modules. Not imporant for now, but just take a note!
# Without parentheses, you're referencing the function, not calling it
# System.system_time # What is your intention here? Referencing or calling - not clear!
2.10.2 2. Nested Function Calls
# When nesting function calls, inner calls need parentheses for clarity
String.length(String.upcase("hello"))
# Without parentheses, this becomes ambiguous:
# String.length String.upcase "hello" # Confusing - which function gets what?
2.11 Real Examples from Our Previous Writeups
Let’s look at examples from our anonymous functions writeup to see this in practice:
2.11.1 Example 1: String Operations
# From our fruits example - both styles work
= ["apple", "banana", "cherry"]
fruits
# Without parentheses (readable for simple cases)
Enum.map fruits, fn x -> String.upcase x end
# With parentheses (clearer, generally better for complex cases, not for toy ones like this)
Enum.map(fruits, fn x -> String.upcase(x) end)
2.11.2 Example 2: Math Operations
# From our prices example
= [10.0, 25.50, 8.75]
prices
# Without parentheses
Enum.map prices, fn x -> x * 1.08 end
# With parentheses (recommended for clarity)
Enum.map(prices, fn x -> x * 1.08 end)
2.11.3 Example 3: Nested Function Calls
# From our words example
= ["cat", "elephant", "dog", "butterfly"]
words
# This needs parentheses for the inner function call
Enum.map(words, fn x -> String.length(x) end)
2.12 The Mental Model: When to Use Parentheses
Think of parentheses as clarity markers:
2.12.1 Use parentheses when:
- Zero arguments: Always required
- Nested calls: Required for inner functions
- Complex expressions: When it makes the code clearer
2.12.2 Skip parentheses when:
- Simple, single function calls:
IO.puts "hello"
- Pipe operations:
data |> transform |> output
- The meaning is completely clear:
String.upcase "hello"
Elixir’s optional parentheses support the language’s philosophy:
- Readability: Code can look more like natural language
- Flexibility: You can choose the style that makes your code clearest
- Pipe operator: Supports the elegant
|>
chaining syntax - Expression-oriented: Everything is an expression, so the syntax stays consistent
This flexibility, combined with Elixir’s “everything is an expression” design, creates a language that’s both powerful and readable. When we explore anonymous functions and the capture operator next, you’ll see how this optional parentheses feature makes functional programming patterns feel more natural.
Python Pro
That is nice - yes, the piping syntax looks inviting and it is one of thing that everyone seems to praise about Elixir. But what comes next in our coverage of anonymous functions?
AI Elixir Mentor
Great! Now that you understand expressions and optional parentheses, let’s explore the different ways to write anonymous functions in Elixir. You’ve been using the basic fn
syntax, but there are several variations that make code more concise. Let us start with the shorter syntax which you will see a lot in Phoenix code.
2.13 Detour - Elixir Anonymous Functions - Shorter Syntax
When you’re transforming data in Elixir, you often write simple anonymous functions that just take one input and apply one function to it. Elixir has a shorter way to write these.
2.14 The Problem: Repetitive Anonymous Functions
Look at the three examples you have code under the first functional pattern - Map:
# Example 1: Convert fruits to uppercase
= ["apple", "banana", "cherry"]
fruits Enum.map(fruits, fn x -> String.upcase(x) end)
# Result: ["APPLE", "BANANA", "CHERRY"]
# Example 2: Add tax to prices
= [10.0, 25.50, 8.75]
prices Enum.map(prices, fn x -> x * 1.08 end)
# Result: [10.8, 27.54, 9.45]
# Example 3: Get word lengths
= ["cat", "elephant", "dog", "butterfly"]
words Enum.map(words, fn x -> String.length(x) end)
# Result: [3, 8, 3, 9]
Notice the pattern: fn x -> some_function(x) end
appears often!
2.15 The Solution: The Capture Operator &
Elixir provides a special operator called the capture operator (&
) that creates anonymous functions with shorter syntax:
# Long way vs Short way
# Example 1: String.upcase
Enum.map(fruits, fn x -> String.upcase(x) end)
Enum.map(fruits, &String.upcase/1) # Shorter!
# Example 2: Math operation
Enum.map(prices, fn x -> x * 1.08 end)
Enum.map(prices, &(&1 * 1.08)) # Shorter!
# Example 3: String.length
Enum.map(words, fn x -> String.length(x) end)
Enum.map(words, &String.length/1) # Shorter!
2.16 What is the Capture Operator?
The &
symbol is called the capture operator. It “captures” functions and expressions to create anonymous functions quickly.
Think of it as saying: “Capture this and turn it into a function I can pass around.”
2.17 Two Types of Short Syntax
2.17.1 Type 1: Capturing Existing Functions (&Module.function/arity
)
When you want to “capture” an existing function to use it as an anonymous function:
# Pattern: fn x -> Module.function(x) end
# Shortcut: &Module.function/1
&String.upcase/1 # Same as: fn x -> String.upcase(x) end
&String.length/1 # Same as: fn x -> String.length(x) end
2.17.2 Type 2: Capturing Expressions (&(&1 + something)
)
This is where the capture operator gets more interesting - you can “capture” entire expressions and turn them into functions automatically.
The Basic Pattern:
elixir
# Instead of writing this long form:
fn x -> x + something end
# You can write this shortcut:
&(&1 + something)
What’s happening here?
- The outer
&
tells Elixir “make this into a function” &1
represents the first argument that will be passed to the function- Everything else is the expression you want to perform on that argument
# Examples
&(&1 * 1.08) # Same as: fn x -> x * 1.08 end
&(&1 + 5) # Same as: fn x -> x + 5 end
&(&1 + &2) # Same as: fn x, y -> x + y end
2.18 Function/Arity Combo
I hope you still remember function/arity combo we discussed a while back. So, it is NOT just a concept to understand, it is a way to communicate with the Elixir compiler.
The /n
tells Elixir how many arguments the function takes:
String.upcase/1
- takes 1 argumentString.replace/3
- takes 3 argumentsEnum.map/2
- takes 2 arguments
2.19 Comparing with Python
Let’s see how this compares to Python’s approach:
Python - Using existing functions in map:
# Python hides the function creation
= ["apple", "banana", "cherry"]
fruits list(map(str.upper, fruits)) # A bit harder to read, but module name gives it away that a function is being used...
= ["cat", "elephant", "dog", "butterfly"]
words list(map(len, words)) # hard to know what len is - is it a variable or a function?
Elixir - Explicit capture operator:
# Elixir makes it clear you're creating a function
= ["apple", "banana", "cherry"]
fruits Enum.map(fruits, &String.upcase/1) # The & says "I'm making a function"
= ["cat", "elephant", "dog", "butterfly"]
words Enum.map(words, &String.length/1) # Crystal clear what's happening
The capture operator makes it explicit that you’re creating an anonymous function, while Python’s syntax can be confusing - you might not realize str.upper
is being turned into a function behind the scenes.
2.20 The Mental Model
The capture operator &
is like saying “capture this and make it into a function”:
&String.upcase/1
→ “capture the upcase function so I can pass it around”&(&1 * 2)
→ “capture this expression and make it a function that doubles its input”
You’re literally capturing existing functions or expressions to create new anonymous functions!
This shorter syntax keeps your code clean when doing simple transformations!
Python Pro
Hmm. That reminds of the C days - pointers and addresses, etc. Anyway, I do agree that the syntax is shorter and compared to reading Python, it is more explicit. I am not sure whether I like it or not - I will let you know after I have used it for a while.
AI Elixir Mentor
That’s a very mature approach! You’re absolutely right to reserve judgment until you’ve used it in practice. The C pointer analogy is interesting - there is some similarity in that you’re “pointing to” or “referencing” functions.
Your instinct to wait and see is spot-on. Some developers love the conciseness of &String.upcase/1
, while others prefer the explicitness of fn x -> String.upcase(x) end
. Both are perfectly valid Elixir, and you’ll develop your own preference over time.
The good news is that understanding both syntaxes means you can read any Elixir code you encounter, regardless of which style the author preferred.
For now, feel free to stick with the fn x -> ... end
syntax in your own code if it feels more comfortable. You can always experiment with the capture operator later when you’re more settled with Elixir’s other concepts.
Ready for the next detour? Our next detour is a bit involved - so, get your coffee, if needed. Though it might feel like an advanced topic at this stage, you will see this very often in the wild and I want you to be clear about the subtle differences between Elixir and Python with respect to anonymous functions and how they are used.
2.21 Detour - The Dot Syntax for calling Anonymous Functions
In Elixir, there’s a special syntax you need to use when calling anonymous functions that are stored in variables. This might seem strange at first, but once you understand it, it becomes second nature.
2.22 The Rule: Anonymous Functions in Variables Need a Dot
When you have an anonymous function stored in a variable, you must use a dot (.
) before the parentheses to call it.
# This is the syntax for functions stored in variables:
= fn x -> x * 2 end
my_function .(arguments)
my_function# ↑ Notice the dot here!
Important: You only need the dot when the function is stored in a variable. When you write anonymous functions inline (like in Enum.map
), no dot is needed:
# ✅ Inline anonymous function - no dot needed
Enum.map([1, 2, 3], fn x -> x * 2 end)
# ✅ Function stored in variable - dot required when calling
= fn x -> x * 2 end
double .(5) # Notice the dot
double
# ✅ Using the stored function with Enum.map - still no dot in the map call
Enum.map([1, 2, 3], double) # We're passing the function, not calling it
2.23 Why the Dot?
The dot syntax exists because parentheses are optional in Elixir. This creates a fundamental ambiguity that doesn’t exist in Python.
Here’s where the problem becomes clear - imagine this function:
def get_tax_calculator do
= fn price -> price * 1.08 end
add_tax
# Without the dot rule, what does this line mean?
# Am I returning the function or calling it with no args?
add_tax end
In Python, parentheses make the intent crystal clear:
def get_tax_calculator():
def add_tax(price):
return price * 1.08
return add_tax # Clearly returning the function (no parentheses)
# return add_tax() # Would clearly be calling it (has parentheses)
But Elixir allows both of these to mean “call the function”:
# These are equivalent in Elixir for named functions:
String.upcase("hello") # With parentheses
String.upcase "hello" # Without parentheses - both call the function!
So for anonymous functions stored in variables, Elixir needs the dot to distinguish:
def get_tax_calculator do
= fn price -> price * 1.08 end
add_tax
# Return the function (no dot)
add_tax end
def calculate_price(base_price) do
= fn price -> price * 1.08 end
add_tax
.(base_price) # Call the function (dot + argument)
add_taxend
The dot prevents ambiguity between “use this function” vs “call this function” in a language where parentheses are optional.
2.24 Examples: Simple Case (Single Parameter, No Extra Processing)
Let’s start with the simplest case where your stored function takes exactly one parameter and needs no additional processing.
2.24.1 Example 1: Uppercasing Strings
Elixir:
# Inline version (no dot needed)
= ["apple", "banana", "cherry"]
fruits Enum.map(fruits, fn x -> String.upcase(x) end)
# Result: ["APPLE", "BANANA", "CHERRY"]
# Stored function version
= fn x -> String.upcase(x) end
uppercase_fn Enum.map(fruits, uppercase_fn) # Pass the function directly (no dot)
# Result: ["APPLE", "BANANA", "CHERRY"]
Python:
# Inline version
= ["apple", "banana", "cherry"]
fruits list(map(lambda x: x.upper(), fruits))
# Result: ["APPLE", "BANANA", "CHERRY"]
# Stored function version
def uppercase_fn(x):
return x.upper()
list(map(uppercase_fn, fruits)) # Pass the function directly (no parentheses)
# Result: ["APPLE", "BANANA", "CHERRY"]
2.24.2 Example 2: Adding Tax to Prices
Elixir:
# Inline version
= [10.0, 25.50, 8.75]
prices Enum.map(prices, fn x -> x * 1.08 end)
# Result: [10.8, 27.54, 9.45]
# Stored function version
= fn x -> x * 1.08 end
add_tax Enum.map(prices, add_tax) # Pass the function directly
# Result: [10.8, 27.54, 9.45]
Python:
# Inline version
= [10.0, 25.50, 8.75]
prices list(map(lambda x: x * 1.08, prices))
# Result: [10.8, 27.54, 9.45]
# Stored function version
def add_tax(x):
return x * 1.08
list(map(add_tax, prices)) # Pass the function directly
# Result: [10.8, 27.54, 9.45]
Key Point: For simple cases, both languages work similarly - you can pass the stored function directly without calling it.
2.25 Examples: Complex Case (Additional Processing Needed)
Sometimes you need to do more than just call your stored function - maybe add extra processing or combine it with other operations.
2.25.1 Example: Uppercasing + Adding Exclamation
Elixir:
= fn x -> String.upcase(x) end
uppercase_fn = ["apple", "banana", "cherry"]
fruits
# Need to call the function AND add "!" - requires the dot syntax
Enum.map(fruits, &(uppercase_fn.(&1) <> "!"))
# Result: ["APPLE!", "BANANA!", "CHERRY!"]
Python:
def uppercase_fn(x):
return x.upper()
= ["apple", "banana", "cherry"]
fruits
# Need to call the function AND add "!" - requires a lambda
list(map(lambda x: uppercase_fn(x) + "!", fruits))
# Result: ["APPLE!", "BANANA!", "CHERRY!"]
2.25.2 Example: Tax Calculation + Rounding
Elixir:
= fn x -> x * 1.08 end
add_tax = [10.0, 25.50, 8.75]
prices
# Call the function AND round the result
Enum.map(prices, &Float.round(add_tax.(&1), 2))
# Result: [10.8, 27.54, 9.45]
Python:
def add_tax(x):
return x * 1.08
= [10.0, 25.50, 8.75]
prices
# Call the function AND round the result
list(map(lambda x: round(add_tax(x), 2), prices))
# Result: [10.8, 27.54, 9.45]
Key Point: When you need additional processing, both languages require you to wrap the function call in a new function (capture syntax in Elixir, lambda in Python).
2.26 Examples: Multiple Parameters
When your stored function takes multiple parameters, but map
only provides one, you need to supply the extra arguments.
2.26.1 Example: Tax Rate + Fee Calculation
Elixir:
# Function that takes 3 parameters
= fn price, tax_rate, fee -> price * (1 + tax_rate) + fee end
calculate_total = [10.0, 25.50, 8.75]
prices
# map only gives us one arg (price), so we provide the other two
Enum.map(prices, &calculate_total.(&1, 0.08, 2.0))
# Result: [12.8, 29.54, 11.45]
Python:
# Function that takes 3 parameters
def calculate_total(price, tax_rate, fee):
return price * (1 + tax_rate) + fee
= [10.0, 25.50, 8.75]
prices
# map only gives us one arg (price), so we provide the other two via lambda
list(map(lambda price: calculate_total(price, 0.08, 2.0), prices))
# Result: [12.8, 29.54, 11.45]
Key Point: Both languages handle multiple parameters the same way - you must create a new function that calls your stored function with the extra arguments provided.
2.27 Common Mistakes
= fn x -> x * 2 end
my_function
# ❌ Wrong - missing the dot when calling directly
(5)
my_function
# ✅ Correct - with the dot when calling directly
.(5)
my_function
# ✅ Correct - no dot when passing to another function
Enum.map([1, 2, 3], my_function)
2.28 Remember
- Inline anonymous functions: No dot needed →
Enum.map(list, fn x -> x * 2 end)
- Stored anonymous functions when calling directly: Use a dot →
my_function.(args)
- Stored anonymous functions when passing to other functions: No dot →
Enum.map(list, my_function)
- Named functions: No dot →
String.upcase(args)
- The dot is only for calling stored anonymous functions - not for inline functions or for named functions or when passing functions around
This syntax might feel unusual coming from other languages, but it’s one of Elixir’s ways of making code more explicit and preventing common mistakes!
Python Pro
wow - that is a lot to digest - but I am happy you used the same 3 examples as in the previous detour - that reduces my mental strain so that I can focus only on what is new.
AI Elixir Mentor
Exactly! I’m glad you noticed that intentional choice. Using the same examples lets you focus purely on the new concept (the dot syntax) without having to juggle new problem domains at the same time.
This is a key learning principle - when introducing complex concepts, keep everything else familiar so your brain can dedicate all its processing power to the new idea.
The dot syntax for calling stored anonymous functions is definitely one of those “Elixir quirks” that feels strange at first but becomes second nature with practice. The key thing to remember is:
- Calling directly:
my_function.(args)
← needs the dot - Passing around:
Enum.map(list, my_function)
← no dot
You’ll encounter this pattern frequently in real Elixir code, so it’s worth understanding now even if it feels a bit heavy.
Ready to move on from these detours and get back to our functional patterns? We still have FILTER and REDUCE to explore!
2.29 Core Functional Pattern 2 - Filter
Python Pro
That was a lot of detours - But before we tackle the next pattern - can you do 2 summaries - one overall summary that summarizes all that we have covered so far. Then the next summary about the 3 core functional patterns and what we have covered there.
AI Elixir Mentor
2.30 Overall Summary: Everything We’ve Covered So Far
2.30.1 Core Concepts Mastered ✅
1. Pattern Matching - =
as match operator (bidirectional equality, not assignment) - Explicit structure matching: [a, b, c] = list
- Head/tail decomposition: [head | tail] = list
- Data validation through pattern matching - Pin operator ^
for matching existing variables
2. Immutability - No mutations - data never changes after creation - Functions return new data instead of modifying existing - Safe sharing - no surprise side effects - Memory efficiency through structural sharing
3. IEx and Documentation - Function arity notation: function_name/number_of_arguments
- h
for help, i
for inspecting data types - Every expression returns a value
4. Key Language Features - Everything is an expression (vs Python’s statements + expressions) - Optional parentheses (with clear rules about when required) - Modules organize functions (vs Python’s object methods) - String interpolation: "#{variable}"
5. Anonymous Functions - Basic syntax: fn x -> transformation end
- Capture operator: &String.upcase/1
and &(&1 * 2)
- Dot syntax for calling stored functions: my_function.(args)
2.31 Functional Patterns Summary
2.31.1 The 3 Core Patterns
- MAP - Transform each item (same number out as in)
- FILTER - Keep only some items (fewer or same number out)
- REDUCE - Combine all into one result (single value out)
2.31.2 What We’ve Covered
MAP Pattern ✅ - Enum.map(collection, transformation_function)
- Examples mastered: - Uppercasing strings: Enum.map(fruits, fn x -> String.upcase(x) end)
- Adding tax: Enum.map(prices, fn x -> x * 1.08 end)
- String lengths: Enum.map(words, fn x -> String.length(x) end)
FILTER Pattern ⏳ NEXT UP - Keep only items that meet a condition - Examples to explore: filtering by length, positive numbers, etc.
REDUCE Pattern ⏳ COMING SOON - Combine all items into single result - Examples to explore: sum, max, count, join strings
Ready to tackle FILTER?
Python Pro
that is a wonderful summary - let us start filtering now.
AI Elixir Mentor
Perfect! Let’s dive into the FILTER pattern.
First I want to give you an overview of the 3 examples we will work through to help you practice Filtering in Elixir.
Remember the mental model: “I want to keep only items that meet a condition”
The pattern: Input list → Fewer (or same) items → Same values
2.32 Filter Pattern - 3 Examples to Master
Example 2.1: Filter by string length
= ["cat", "elephant", "dog", "butterfly"]
words # Only long words
for word in words if len(word) > 5]
[word # Result: ["elephant", "butterfly"]
Example 2.2: Filter by numeric condition
= [1, -2, 3, -4, 5, 0]
numbers # Only positive numbers
for num in numbers if num > 0]
[num # Result: [1, 3, 5]
Example 2.3: Filter by string pattern
= ["apple", "banana", "cherry", "apricot"]
fruits # Only fruits starting with "a"
for fruit in fruits if fruit.startswith("a")]
[fruit # Result: ["apple", "apricot"]
Now that you have seen what is coming, Ready to tackle Example 2.1?
2.33 Filter 2.1 : Only long words
Let’s start with a simple example. Here’s the Python code to translate:
Python:
= ["cat", "elephant", "dog", "butterfly"]
words for word in words if len(word) > 5] # Keep only long words
[word # Result: ["elephant", "butterfly"]
You’ll need to find the right function in the Enum
module. Try looking up “filter” in the Enum documentation (h Enum
and search for filter-related functions).
Once you find the right function, try translating this Python code to Elixir. You already know how to: - Write anonymous functions: fn word -> ... end
- Get string length: String.length(word)
- Compare with >
Give it a try!
Python Pro
I got it
= ["cat", "elephant", "dog", "butterfly"]
words Enum.filter(words, fn x -> String.length(x) > 5 end)
AI Elixir Mentor
Excellent! You nailed it perfectly. You correctly identified:
✅ Enum.filter
as the right function
✅ Anonymous function structure: fn x -> condition end
✅ String.length(x) > 5
as the condition
✅ The condition returns true
/false
to decide what to keep
Your result should be ["elephant", "butterfly"]
- exactly the words longer than 5 characters.
Notice the pattern: Enum.filter(collection, fn item -> boolean_condition end)
The function you pass to filter
must return true
(keep the item) or false
(discard it).
Ready for Example 2.2? This one involves numbers instead of strings:
Python:
= [1, -2, 3, -4, 5, 0]
numbers for num in numbers if num > 0] # Keep only positive numbers
[num # Result: [1, 3, 5]
This should be even simpler than the string example. Give it a shot!
Python Pro
Yes done - easy, peasy
= [1, -2, 3, -4, 5, 0]
numbers Enum.filter(numbers, fn x -> x > 0 end)
AI Elixir Mentor
Perfect! You’re getting the hang of this pattern. Much simpler indeed - no need for any module functions, just a direct comparison x > 0
.
Your result should be [1, 3, 5]
- all the positive numbers.
Now let’s try the final filter example, which introduces string pattern matching:
Python:
= ["apple", "banana", "cherry", "apricot"]
fruits for fruit in fruits if fruit.startswith("a")] # Keep only fruits starting with "a"
[fruit # Result: ["apple", "apricot"]
For this one, you’ll need to find the Elixir equivalent of Python’s startswith()
method. Try looking in the String
module documentation (h String
) for functions related to checking the beginning of strings.
Give it a try!
Python Pro
Filter examples are very easy and I am really liking the repetition I get from the slightly varied tasks that help me to recall the new syntax multiple times.
= ["apple", "banana", "cherry", "apricot"]
fruits Enum.filter(fruits, fn x -> String.starts_with?(x, "a") end)
AI Elixir Mentor
Excellent! You found String.starts_with?/2
perfectly. Notice how Elixir uses the ?
suffix to indicate functions that return true
/false
- this is a helpful convention you’ll see throughout Elixir.
Your result should be ["apple", "apricot"]
- exactly the fruits starting with “a”.
I’m glad you’re enjoying the repetitive practice with variations! That’s exactly the learning approach we discussed - building muscle memory through spaced repetition with slight changes. You’re now seeing the same core pattern:
Enum.filter(collection, fn item -> boolean_condition end)
But with different conditions: - String length: String.length(x) > 5
- Numeric comparison: x > 0
- String pattern: String.starts_with?(x, "a")
2.34 Filter Pattern Summary ✅
You’ve mastered filtering! The pattern is always: - Input: A collection - Function: Returns true
(keep) or false
(discard) - Output: Same values, but fewer items
Ready to tackle the final pattern: REDUCE? This one combines all items into a single result - it’s the most powerful of the three patterns!
2.35 Core Functional Pattern 3 - Reduce
Python Pro
yes - let us start REDUCING…
AI Elixir Mentor
Excellent! Let’s dive into the REDUCE pattern - the most powerful of the three!
Remember the mental model: “I want to combine all items into a single result”
The pattern: Input list → Single result
2.36 Reduce Pattern - 3 Examples to Master
Example 3.1: Sum all numbers
= [1, 2, 3, 4, 5]
numbers sum(numbers) # Combine into total
# Result: 15
Example 3.2: Find maximum
= [3, 1, 4, 1, 5, 9, 2]
numbers max(numbers) # Combine into largest value
# Result: 9
Example 3.3: Join strings
= ["Hello", "beautiful", "world"]
words ", ".join(words) # Combine into single string
# Result: "Hello, beautiful, world"
2.37 Reduce 3.1: Sum all numbers
Let’s start with the classic example:
Python:
= [1, 2, 3, 4, 5]
numbers sum(numbers) # Result: 15
For this, you’ll need to find the right function in the Enum
module. Try looking for “sum” or “reduce” functions (h Enum
).
The reduce pattern is a bit different from map and filter - it builds up a result by combining each item with what came before.
Give it a try!
Python Pro
rather than just showing only sum, can you also show the variant using reduce in python also - otherwise, this exercise is not challenging at all
AI Elixir Mentor
Great point! You’re absolutely right - just using sum()
doesn’t teach you the reduce pattern. Let me show you both approaches in Python:
Python - Easy way (built-in function):
= [1, 2, 3, 4, 5]
numbers sum(numbers) # Result: 15
Python - Reduce way (shows the pattern):
from functools import reduce
= [1, 2, 3, 4, 5]
numbers reduce(lambda acc, x: acc + x, numbers, 0) # Result: 15
The reduce pattern works like this: - Start with an initial value (accumulator): 0
- For each item in the list: combine accumulator + current_item
- The accumulator builds up: 0 + 1 = 1
, then 1 + 2 = 3
, then 3 + 3 = 6
, etc.
Now translate both versions to Elixir: 1. Find the Elixir equivalent of Python’s sum()
2. Find the Elixir equivalent of Python’s reduce()
and write the explicit version
This will help you understand both the convenient shortcut and the underlying pattern!
Python Pro
Thanks. I got it - I almost cheated as I saw the code for summing as the first example when I did h Enum.reduce
(42)> numbers = [1, 2, 3, 4, 5]
iex[1, 2, 3, 4, 5]
(43)> Enum.sum(numbers)
iex15
(44)> Enum.reduce(numbers, fn x, acc -> x + acc end)
iex15
AI Elixir Mentor
Perfect! You found both approaches:
✅ Enum.sum(numbers)
- the convenient shortcut
✅ Enum.reduce(numbers, fn x, acc -> x + acc end)
- the explicit pattern
Great observation about “almost cheating” by seeing the example in the docs - that’s exactly how real programming works! Reading documentation and examples is a core skill.
Now, Let’s tackle Example 3.2 - finding the maximum value.
Python - Easy way (built-in function):
= [3, 1, 4, 1, 5, 9, 2]
numbers max(numbers) # Result: 9
Python - Reduce way (shows the pattern):
from functools import reduce
= [3, 1, 4, 1, 5, 9, 2]
numbers reduce(lambda acc, x: max(acc, x), numbers) # Result: 9
# Or equivalently: reduce(lambda acc, x: acc if acc > x else x, numbers)
The reduce pattern here: - Start with first element as accumulator: 3
- For each item: keep the larger of accumulator
vs current_item
- Builds up: max(3,1)=3
, then max(3,4)=4
, then max(4,1)=4
, then max(4,5)=5
, etc.
Now translate both versions to Elixir: 1. Find the Elixir equivalent of Python’s max()
2. Write the explicit reduce version
Try it out!
Python Pro
Hmmm. I am stuck as I don’t know how to do if-else inside that anon function block
(45)> Enum.max(numbers)
iex5
(46)> Enum.reduce(numbers, fn x, acc -> if x > acc then x else acc end)
iex** (SyntaxError) invalid syntax found on iex:46:53:
error: syntax error before: 'else'
(46)> Enum.reduce(numbers, fn x, acc -> if x > acc do x else acc end)
iex** (MismatchedDelimiterError) mismatched delimiter found on iex:46:64:
error: unexpected token: )
AI Elixir Mentor
Good catch! You’re running into Elixir’s if
syntax inside anonymous functions. The issue is that Elixir’s if
statement needs do...end
blocks, but that doesn’t work cleanly inside the compact fn...end
syntax.
Here are two solutions:
Solution 1: Use max/2
function (simplest)
Enum.reduce(numbers, fn x, acc -> max(x, acc) end)
Solution 2: Multi-line anonymous function (if you want the explicit if/else)
Enum.reduce(numbers, fn x, acc ->
if x > acc do
xelse
accend
end)
The first solution is much cleaner and more idiomatic. Elixir has a built-in max/2
function that takes two arguments and returns the larger one - perfect for reduce operations!
Try the first solution - it should work perfectly and give you 9
as the result.
Python Pro
Great. I was thinking that there might be a max in Elixir. Then I thought that it would be inside Enum module - so, how I do write Enum.max by calling Enum.max inside it :) - So, I lost that line of thought. So, I was able to make it work - but what is this curious max function which does not seem to follow the Elixir convention of Module.function?
Enum.reduce(numbers, fn x, acc -> max(x, acc) end)
AI Elixir Mentor
Excellent observation! You’ve discovered something important about Elixir’s design.
The max/2
function you used is actually part of Elixir’s Kernel module. The Kernel module contains the most fundamental functions that are so commonly used they’re automatically imported into every Elixir program.
So when you write max(x, acc)
, you’re actually calling Kernel.max(x, acc)
, but Elixir imports it automatically so you don’t need the module prefix.
Other examples of auto-imported Kernel functions:
# These are all actually Kernel.function_name
([1, 2, 3]) # Really: Kernel.length/1
length(-5) # Really: Kernel.abs/1
abs(3.7) # Really: Kernel.round/1
round(5, 3) # Really: Kernel.min/2 min
You can verify this by trying:
Kernel.max(5, 3) # This works too!
Why the distinction? - Enum.max/1
- finds the maximum value in a collection - Kernel.max/2
- finds the maximum between two values
So Enum.max([3, 1, 4, 1, 5, 9, 2])
and Kernel.max(5, 3)
serve different purposes, which is why both exist!
This is why your reduce solution works perfectly - you’re using the two-argument version inside the reduction.
Python Pro
that is very nice and very interesting - So, there is direct equivalent for max and len functions in python here in Elixir. Great let us move on to the last reduce example.
AI Elixir Mentor
Exactly! Elixir does provide direct equivalents for many common Python functions, just organized differently:
- Python
len()
→ Elixirlength()
(auto-imported from Kernel) - Python
max()
→ Elixirmax()
(auto-imported from Kernel) - Python
abs()
→ Elixirabs()
(auto-imported from Kernel)
Now let’s tackle the final reduce example - joining strings!
Python - Easy way (built-in method):
= ["Hello", "beautiful", "world"]
words ", ".join(words) # Result: "Hello, beautiful, world"
Python - Reduce way (shows the pattern):
from functools import reduce
= ["Hello", "beautiful", "world"]
words reduce(lambda acc, x: acc + ", " + x, words) # Result: "Hello, beautiful, world"
The reduce pattern here: - Start with first element as accumulator: "Hello"
- For each remaining item: combine accumulator + ", " + current_item
- Builds up: "Hello" + ", " + "beautiful" = "Hello, beautiful"
, then "Hello, beautiful" + ", " + "world" = "Hello, beautiful, world"
Now translate both versions to Elixir: 1. Find the Elixir equivalent of Python’s ", ".join()
2. Write the explicit reduce version using string concatenation
Try it out!
Python Pro
I translated the easy version. But I need some clues as +
does not seem to work in Elixir for string concat - Hmmm. python was so much more intuitive.
(47)> words = ["Hello", "beautiful", "world"]
iex["Hello", "beautiful", "world"]
(51)> Enum.join(words, ", ")
iex"Hello, beautiful, world"
(52)> Enum.reduce(words, fn x, acc -> acc + x end)
iex** (ArithmeticError) bad argument in arithmetic expression: "Hello" + "beautiful"
:erlang.+("Hello", "beautiful")
iex:52: (file)
iex:52: (file)
AI Elixir Mentor
Great job finding Enum.join(words, ", ")
!
You’re right that +
doesn’t work for string concatenation in Elixir - that’s reserved for arithmetic. For strings, Elixir uses the <>
operator:
"Hello" <> " " <> "world" # Result: "Hello world"
Python Pro
Yes it is working for me.
(52)> Enum.reduce(words, fn x, acc -> acc <> x end)
iex"Hellobeautifulworld"
(53)> Enum.reduce(words, fn x, acc -> acc <> "," <> x end)
iex"Hello,beautiful,world"
(54)> Enum.reduce(words, fn x, acc -> acc <> ", " <> x end)
iex"Hello, beautiful, world"
AI Elixir Mentor
Perfect! You figured out the string concatenation operator <>
and got it working.
I can see your thought process in those three attempts: 1. acc <> x
→ "Hellobeautifulworld"
(no separator) 2. acc <> "," <> x
→ "Hello,beautiful,world"
(comma but no space) 3. acc <> ", " <> x
→ "Hello, beautiful, world"
(perfect!)
2.38 Reduce Pattern Summary ✅
You’ve now mastered all three examples:
Convenient shortcuts: - Enum.sum(numbers)
- Enum.max(numbers)
- Enum.join(words, ", ")
Explicit reduce patterns: - Enum.reduce(numbers, fn x, acc -> x + acc end)
- Enum.reduce(numbers, fn x, acc -> max(x, acc) end)
- Enum.reduce(words, fn x, acc -> acc <> ", " <> x end)
2.39 🎉 All Three Functional Patterns Complete!
You’ve mastered: - MAP: Transform each item → Enum.map
- FILTER: Keep only some items → Enum.filter
- REDUCE: Combine all into one result → Enum.reduce
These three patterns handle 90% of all data processing tasks you’ll encounter. Ready to put it all together with piping?
Python Pro
That is great - let us summarize the 3 core functional patterns first and make sure that I understand them well by giving me 3 exercises which combines these ideas together.
AI Elixir Mentor
2.40 Summary: The 3 Core Functional Patterns
2.40.1 MAP - Transform Each Item
Pattern: Same number of items out as in, but transformed Syntax: Enum.map(collection, fn item -> transformation end)
Examples: - Uppercase strings: Enum.map(words, fn x -> String.upcase(x) end)
- Add tax: Enum.map(prices, fn x -> x * 1.08 end)
- Get lengths: Enum.map(words, fn x -> String.length(x) end)
2.40.2 FILTER - Keep Only Some Items
Pattern: Fewer (or same) items out, but same values Syntax: Enum.filter(collection, fn item -> boolean_condition end)
Examples: - Long words: Enum.filter(words, fn x -> String.length(x) > 5 end)
- Positive numbers: Enum.filter(numbers, fn x -> x > 0 end)
- Starts with “a”: Enum.filter(fruits, fn x -> String.starts_with?(x, "a") end)
2.40.3 REDUCE - Combine All Into One Result
Pattern: Single result from entire collection Syntax: Enum.reduce(collection, fn item, accumulator -> combination end)
Examples: - Sum: Enum.reduce(numbers, fn x, acc -> x + acc end)
or Enum.sum(numbers)
- Max: Enum.reduce(numbers, fn x, acc -> max(x, acc) end)
or Enum.max(numbers)
- Join: Enum.reduce(words, fn x, acc -> acc <> ", " <> x end)
or Enum.join(words, ", ")
2.41 3 Exercises combining map, filter and reduce
Let us start with a straightforward exercise.
Exercise 1: Word Processing Pipeline
= ["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
words # 1. Convert all to lowercase
# 2. Keep only words with more than 4 letters
# 3. Count how many words remain
# Expected result: 4
# they are ["apple", "banana", "cherry", "elderberry"]
Python Pro
yes it was easy
(56)> words = ["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
iex["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
(57)> words = Enum.map(words, fn x -> String.downcase(x) end)
iex["apple", "banana", "cherry", "date", "elderberry"]
(58)> words = Enum.filter(words, fn x -> String.length(x) > 4 end)
iex["apple", "banana", "cherry", "elderberry"]
(59)> length(words)
iex4
AI Elixir Mentor
Excellent work! You correctly applied all three patterns in sequence:
✅ MAP: String.downcase(x)
to convert to lowercase
✅ FILTER: String.length(x) > 4
to keep long words
✅ REDUCE: length(words)
to count the results
Your step-by-step approach with intermediate variables is perfect for learning. You can see exactly what happens at each stage: 1. Mixed case → all lowercase 2. 5 words → 4 words (filtered out “date”) 3. List → count of 4
Exercise 2: Number Crunching Pipeline
= [1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
numbers # 1. Keep only positive numbers
# 2. Square each number
# 3. Sum all the squares
# Expected result: 248
So the expected result is 248. Give this one a try!
Python Pro
I got it - I really like the simple touch of Elixir - it returns the right-hand side after pattern-matching which helps me to immediately see the output in a REPL session like this - I love this.
(61)> numbers = [1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
iex[1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
(61)> numbers = Enum.filter(numbers, fn x -> x > 0 end)
iex[1, 3, 5, 7, 8, 10]
(62)> numbers = Enum.map(numbers, fn x -> x * x end)
iex[1, 9, 25, 49, 64, 100]
(63)> Enum.sum(numbers)
iex248
AI Elixir Mentor
Perfect! You nailed it exactly right. I love your observation about Elixir returning the right-hand side value - it really does make REPL-driven development so much more pleasant compared to Python where you often have to print or re-type variable names to see results.
Your pipeline was flawless: ✅ FILTER: x > 0
kept [1, 3, 5, 7, 8, 10]
✅ MAP: x * x
squared them to [1, 9, 25, 49, 64, 100]
✅ REDUCE: Enum.sum
gave you 248
Final Exercise 3: String Analysis Pipeline
= ["Hello world", "Elixir is fun", "I love programming", "Code every day"]
sentences # 1. Split each sentence into words (hint: String.split)
# 2. Flatten the result into one big list of words
# 3. Filter out words shorter than 4 characters
# 4. Count the remaining words
# Expected result: 7
This one introduces a new challenge - you’ll need to look up String.split/1
and figure out how to flatten a list of lists into a single list. Try exploring the List
module for a flatten function!
Python Pro
Yes - it felt a bit uneasy as I was not sure how to flatten the list of lists. But I got it - I was able to find List.flatten/1
function.
(67)> sentences = Enum.map(sentences, fn x -> String.split(x) end)
iex[
["Hello", "world"],
["Elixir", "is", "fun"],
["I", "love", "programming"],
["Code", "every", "day"]
]
(73)> words = Enum.filter(words, fn x -> String.length(x) >= 4 end)
iex["Hello", "world", "Elixir", "love", "programming", "Code", "every"]
(74)> length(words)
iex7
AI Elixir Mentor
Great work! You successfully found List.flatten/1
and applied the pipeline. I can see you got 7 words instead of the expected 6, but let me check your logic…
Looking at your final filtered list: ["Hello", "world", "Elixir", "love", "programming", "Code", "every"]
You used >= 4
(4 or more characters), since exercise asked for filtering out words “shorter than 4 characters”, which means keeping words with 4 or more characters - so your logic is actually correct!
Your pipeline is perfect:
✅ MAP: String.split(x)
to split sentences into word lists
✅ FLATTEN: List.flatten()
to combine into single word list
✅ FILTER: String.length(x) >= 4
to keep long words
✅ COUNT: length()
to get final count of 7
Excellent problem-solving finding the flatten function! Ready to learn how to make these pipelines even more elegant with the pipe operator?
3 Piping in Elixir - Joy of reading from left to right
3.1 The Problem: Nested Function Hell
Imagine you’re making a sandwich. Here’s how you’d naturally describe the process:
- Get the bread
- Toast the bread
- Add cheese
- Add lettuce
- Add mayo
But in most programming languages, you’d have to write it backwards or create a mess of nested functions.
3.2 Python’s Approach (Reading Inside-Out)
Python with nested functions:
def make_sandwich():
return add_mayo(add_lettuce(
add_cheese(toast_bread(get_bread()))))
# This reads inside-out: confusing!
# You have to start from get_bread() and work your way out
Python with intermediate variables:
def make_sandwich():
= get_bread()
bread = toast_bread(bread)
toasted = add_cheese(toasted)
with_cheese = add_lettuce(with_cheese)
with_lettuce = add_mayo(with_lettuce)
final return final
# Better, but lots of temporary variables
3.3 Elixir’s Solution: The Pipe Operator |>
Elixir with piping:
def make_sandwich do
()
get_bread|> toast_bread()
|> add_cheese()
|> add_lettuce()
|> add_mayo()
end
This reads exactly like your recipe instructions - top to bottom, step by step!
3.4 How the Pipe Operator Works
The pipe operator |>
takes the result from the left side and passes it as the first argument to the function on the right side.
These are equivalent:
# With pipe
|> String.upcase()
name
# Without pipe
String.upcase(name)
Longer chain:
# With pipe
" hello world "
|> String.trim()
|> String.upcase()
|> String.reverse()
# Without pipe (nested mess)
String.reverse(String.upcase(String.trim(" hello world ")))
# Step by step what happens:
# Step 1: " hello world " |> String.trim() → "hello world"
# Step 2: "hello world" |> String.upcase() → "HELLO WORLD"
# Step 3: "HELLO WORLD" |> String.reverse() → "DLROW OLLEH"
3.5 Real Example: Processing a Name
Let’s say you want to clean up and format a user’s name input.
Python approach:
def clean_name(raw_name):
return raw_name.strip().title()
= " alice johnson "
name = clean_name(name)
result print(result) # "Alice Johnson"
Elixir with piping:
def clean_name(raw_name) do
raw_name|> String.trim()
|> String.capitalize()
end
= " alice johnson "
name = clean_name(name)
result IO.puts(result) # "Alice Johnson"
3.6 More Complex Example: Building a Greeting
Python (multiple steps):
def create_greeting(name, title):
= name.strip().title()
cleaned = f"{title} {cleaned}"
formal return f"Welcome, {formal}!"
= create_greeting(" bob smith ", "Dr.")
greeting print(greeting) # "Welcome, Dr. Bob Smith!"
Elixir (reads like a recipe): then/2
takes the piped value and passes it as an argument to an anonymous function. The syntax is:
|> then(fn x -> # do something with x end) value
Now here is the pipeline version of create_greeting:
def create_greeting(name, title) do
name|> String.trim() # Remove whitespace
|> String.capitalize() # Capitalize properly
|> then(fn clean_name -> "#{title} #{clean_name}" end) # Add title
|> then(fn formal -> "Welcome, #{formal}!" end) # Add welcome
end
= create_greeting(" bob smith ", "Dr.")
greeting IO.puts(greeting) # "Welcome, Dr. Bob Smith!"
3.7 The Magic: Debugging Made Easy
Want to see what’s happening at each step? Just add |> IO.inspect()
:
" alice johnson "
|> IO.inspect() # Prints: " alice johnson "
|> String.trim()
|> IO.inspect(label: "After trim") # Prints: After trim: "alice johnson"
|> String.capitalize()
|> IO.inspect(label: "Final") # Prints: Final: "Alice Johnson"
This is impossible to do cleanly with nested functions!
3.8 Why Piping is Elegant
Mental Model: Assembly Line
- Each function is a station on an assembly line
- Data flows left to right, top to bottom
- Each step transforms the data and passes it to the next step
- Easy to add, remove, or reorder steps
Benefits:
- Readable: Reads like step-by-step instructions
- Debuggable: Easy to inspect data at any point
- Modular: Easy to add/remove transformations
- No Nesting: No confusing inside-out reading
- Natural Flow: Matches how you think about the problem
3.9 Key Takeaway
The pipe operator |>
transforms your code from a puzzle of nested function calls into a clear sequence of transformations.
Instead of reading inside-out, you read top-to-bottom, just like following a recipe or assembly line instructions.
Remember: data |> function()
is the same as function(data)
- the pipe just makes the data flow more obvious and readable.
Python Pro
That is a wonderful detour. I guess that these 3 exercises with all these intermediate variables will become more elegant with the pipe operator.
AI Elixir Mentor
Exactly! You’ve hit the nail on the head. All those intermediate variables (words = Enum.map(words, ...)
, words = Enum.filter(words, ...)
) are exactly what the pipe operator eliminates.
Let’s transform your Exercise 1 solution to see the magic:
Your current approach (with intermediate variables):
= ["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
words = Enum.map(words, fn x -> String.downcase(x) end)
words = Enum.filter(words, fn x -> String.length(x) > 4 end)
words (words) length
With the pipe operator:
["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
|> Enum.map(fn x -> String.downcase(x) end)
|> Enum.filter(fn x -> String.length(x) > 4 end)
|> length()
The |>
operator takes the result from the left side and passes it as the first argument to the function on the right side.
Try this transformation yourself with Exercise 1.
Python Pro
this is beautiful. I just copied and pasted your code into iex and iex somehow intuitively picked up the result of the previous expression and passed it to pipe
(81)> ["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
iex["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
(82)> |> Enum.map(fn x -> String.downcase(x) end)
iex["apple", "banana", "cherry", "date", "elderberry"]
(83)> |> Enum.filter(fn x -> String.length(x) > 4 end)
iex["apple", "banana", "cherry", "elderberry"]
(84)> |> length()
iex4
AI Elixir Mentor
Wow! That’s incredible - you’ve discovered one of the most delightful features of IEx!
Yes, IEx is smart enough to automatically pipe the result of the previous expression when you start a new line with |>
. This makes interactive development absolutely magical - you can build up complex transformations step by step, seeing the result at each stage.
This is exactly how experienced Elixir developers work: 1. Start with some data 2. Pipe it through one transformation 3. See the result, make sure it’s what you expect 4. Add the next pipe 5. Keep building the pipeline incrementally
You can also write it all as one expression:
["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
|> Enum.map(fn x -> String.downcase(x) end)
|> Enum.filter(fn x -> String.length(x) > 4 end)
|> length()
But the interactive approach you just discovered is often better for development because you can verify each step!
Try converting your Exercise 2 solution using pipes now. You’ll love how it reads like a recipe: “Take numbers, then filter positives, then square them, then sum them up.”
Python Pro
can you please recall the code I used for exercise 2?
AI Elixir Mentor
Of course! Here’s your Exercise 2 solution:
= [1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
numbers = Enum.filter(numbers, fn x -> x > 0 end)
numbers = Enum.map(numbers, fn x -> x * x end)
numbers Enum.sum(numbers)
Now try converting this to use the pipe operator! Start with the initial list and pipe it through each transformation step by step.
Python Pro
Great - this definitely feels better and easier. I loved the flexibility to drop the parantheses for Enum.sum.
(85)> [1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
iex[1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
(86)> |> Enum.filter(fn x -> x > 0 end)
iex[1, 3, 5, 7, 8, 10]
(87)> |> Enum.map(fn x -> x * x end)
iex[1, 9, 25, 49, 64, 100]
(88)> |> Enum.sum
iex248
AI Elixir Mentor
Perfect! You’ve really got the hang of this. I love how you noticed the flexibility with parentheses - |> Enum.sum
is so clean and readable.
The pipeline now reads like natural language: - “Take these numbers” - “Filter to keep positive ones” - “Map each to its square” - “Sum them all up”
This is exactly why Elixir developers love the pipe operator - it transforms nested function calls into a clear, step-by-step recipe.
Now try Exercise 3 with pipes! That one had 4 steps:
Your original solution was:
= ["Hello world", "Elixir is fun", "I love programming", "Code every day"]
sentences = Enum.map(sentences, fn x -> String.split(x) end)
sentences = List.flatten(sentences)
words = Enum.filter(words, fn x -> String.length(x) >= 4 end)
words (words) length
Can you convert this to a beautiful pipeline?
Python Pro
Yes I have done it, But I tried to use the shorter syntax for anon functions, but one place, I got an error which perplexed me - take a look.
(89)> ["Hello world", "Elixir is fun", "I love programming", "Code every day"]
iex["Hello world", "Elixir is fun", "I love programming", "Code every day"]
(90)> |> Enum.map(&String.split/2)
iex** (BadArityError) &String.split/2 with arity 2 called with 1 argument ("Hello world")
(elixir 1.18.3) lib/enum.ex:1714: Enum."-map/2-lists^map/1-1-"/2
iex:90: (file)
(90)> |> Enum.map(&String.split/1)
iex[
["Hello", "world"],
["Elixir", "is", "fun"],
["I", "love", "programming"],
["Code", "every", "day"]
]
(91)> |> List.flatten
iex["Hello", "world", "Elixir", "is", "fun", "I", "love", "programming", "Code",
"every", "day"]
(92)> |> Enum.filter(&(String.length(&1) >=4))
iex["Hello", "world", "Elixir", "love", "programming", "Code", "every"]
(93)> |> length
iex7
AI Elixir Mentor
Great work! You successfully converted it to a beautiful pipeline. Your error discovery is actually a fantastic learning moment!
3.10 The String.split Arity Issue
You discovered something important about String.split
:
String.split/1
- splits on whitespace (what you wanted)String.split/2
- splits on a specific pattern (takes string + pattern)
When you used &String.split/2
, Elixir expected you to provide 2 arguments, but Enum.map
only provides 1 (the string from the list). That’s why you got the “called with 1 argument” error.
3.11 The Capture Operator Complexity
Notice how your filter line got a bit messy:
|> Enum.filter(&(String.length(&1) >= 4))
This is where the capture operator starts to feel clunky. Compare:
# Capture operator (harder to read)
|> Enum.filter(&(String.length(&1) >= 4))
# Regular anonymous function (clearer)
|> Enum.filter(fn x -> String.length(x) >= 4 end)
For simple function calls like &String.split/1
, the capture operator is great. But for complex expressions, the regular fn
syntax is often more readable.
Your final pipeline is beautiful - it reads like a story from top to bottom!
Python Pro
ok great - can you please summarize pipe operator and is there anything else to know about piping syntax in Elixir?
AI Elixir Mentor
3.12 Pipe Operator Summary
3.12.1 Basic Concept
The pipe operator |>
takes the result from the left and passes it as the first argument to the function on the right.
|> function(args) # Same as: function(data, args) data
3.12.2 Key Benefits
✅ Readable: Top-to-bottom flow like a recipe
✅ Debuggable: Easy to inspect at each step with |> IO.inspect()
✅ Modular: Easy to add/remove/reorder steps
✅ No nesting: Eliminates confusing inside-out reading
✅ REPL-friendly: IEx auto-pipes when you start with |>
3.12.3 When to Use Pipes vs Regular Syntax
# Good for pipes - simple transformations
|> String.upcase() |> String.reverse()
data
# Better without pipes - single function call
String.upcase(data)
# Good for pipes - multiple steps
|> Enum.filter(&(&1 > 0)) |> Enum.sum() numbers
3.13 Advanced Piping Features
3.13.1 1. The then/2
Function
When you need to use the piped value in a complex expression:
name|> String.trim()
|> then(fn clean_name -> "Hello, #{clean_name}!" end)
3.13.2 2. Debugging with IO.inspect
data|> transform_step_1()
|> IO.inspect(label: "After step 1") # Prints and passes through
|> transform_step_2()
|> IO.inspect() # Quick peek
3.13.3 3. Parentheses Rules in Pipes
# Optional parentheses (clean)
|> String.upcase |> String.reverse
data
# Required for zero-argument functions
|> String.trim() |> System.system_time()
data
# Required for clarity with multiple arguments
|> String.replace("old", "new") data
That covers the essential piping knowledge! The pipe operator is one of Elixir’s most beloved features.
Python Pro
Ok, I think that we have covered 3 key ideas that we started with - pattern matching, immutability and piping - can you please summarise?
AI Elixir Mentor
3.14 Summary: The 3 Core Elixir Concepts
3.14.1 1. Pattern Matching ✅
Concept: =
is a match operator (bidirectional equality), not assignment
[a, b, c] = [1, 2, 3] # Explicit structure matching
[head | tail] = [1, 2, 3] # Head/tail decomposition
6 = 5 + 1 # Bidirectional - works both ways!
Benefits: Data validation, concise destructuring, less error-checking code
3.14.2 2. Immutability ✅
Concept: Data never changes after creation - functions return new data
= [1, 2, 3]
list = [0 | list] # Creates new list, original unchanged
new_list # list is still [1, 2, 3]
Benefits: No surprise mutations, safe sharing, easier debugging, memory-efficient through structural sharing
3.14.3 3. Piping ✅
Concept: |>
passes result as first argument to next function
data|> transform_step_1()
|> transform_step_2()
|> final_result()
Benefits: Readable top-to-bottom flow, easy debugging, modular transformations
3.15 The 3 Functional Patterns ✅
- MAP: Transform each item →
Enum.map
- FILTER: Keep only some items →
Enum.filter
- REDUCE: Combine all into one →
Enum.reduce
3.16 How They Work Together
["Apple", "BANANA", "cherry"]
|> Enum.map(&String.downcase/1) # Transform each
|> Enum.filter(&(String.length(&1) > 4)) # Keep some
|> length() # Combine to count
These three concepts form the foundation of thinking functionally in Elixir!
Python Pro
ok what comes next?
AI Elixir Mentor
Great question! You’ve built a solid foundation in Elixir’s core concepts. Here are the natural next steps:
3.17 Immediate Next Steps
1. Data Structures Beyond Lists - Tuples: {:ok, result}
- fixed-size, fast access - Maps: %{name: "Alice", age: 30}
- key-value pairs (like Python dicts) - Keyword Lists: [name: "Alice", age: 30]
- ordered key-value pairs
2. Control Flow - case statements with pattern matching - cond for multiple conditions - with for happy path programming
3. Modules and Functions - Defining your own modules - Named functions vs anonymous functions - Function clauses and guards
3.18 Phoenix-Focused Path
Since your goal is to evaluate Phoenix vs Rails, you could also jump more directly into Phoenix fundamentals:
1. Basic Phoenix App - Controllers, Views, Templates - Routing - LiveView basics
2. Database Integration - Ecto (Elixir’s database library) - Schemas and migrations
What feels more appealing to you right now: - A. Continue building Elixir language skills with data structures and control flow? - B. Jump into a simple Phoenix application to see the web framework in action? - C. Something else you’re curious about?
Your learning style and timeline will help determine the best path!
Python Pro
i want to do a phoeix-focused path, but let us not jump into phoenix just yet. My aim is to generate a to-do list app or blog app at the end of the book and I should be able to read through the code and understand most of the things. So, what topics should be covered next?
AI Elixir Mentor
Perfect! Building toward a Phoenix to-do or blog app is an excellent goal. Let me map out the essential Elixir concepts you’ll need to understand Phoenix code comfortably.
3.19 Essential Elixir for Phoenix Development
3.19.1 Phase 1: Data Structures (Next)
You’ll see these everywhere in Phoenix: - Maps: %{title: "Learn Elixir", completed: false}
- your data models - Tuples: {:ok, user}
vs {:error, reason}
- Phoenix’s return patterns - Keyword Lists: [class: "btn", id: "submit"]
- HTML attributes
3.19.2 Phase 2: Pattern Matching in Practice
- case statements - handling different outcomes
- with statements - chaining operations that might fail
- Function clauses - multiple function definitions with different patterns
3.19.3 Phase 3: Modules and Functions
- Creating your own modules
- Public vs private functions
- How Phoenix organizes code into modules
3.19.4 Phase 4: Error Handling
{:ok, result}
vs{:error, reason}
pattern- How to handle failures gracefully
3.19.5 Phase 5: Basic Phoenix Concepts
- Understanding MVC in Phoenix
- How requests flow through the application
- Templates and data binding
This sequence will prepare you to read and understand Phoenix code naturally. Each concept builds on what you already know.
Ready to start with Maps and Tuples? These are the data structures you’ll use constantly in Phoenix apps.
3.20 Maps
Python Pro
yes let us cover maps and see how they compare with python.
AI Elixir Mentor
4 Maps in Elixir vs Python
Maps are key-value data structures that store associations between unique keys and their values. Python uses dictionaries while Elixir uses maps - here’s how their syntax compares for common operations.
4.1 Creating Maps
Python:
= {"name": "Alice", "age": 30, "city": "New York"}
person = {} empty_dict
Elixir:
= %{"name" => "Alice", "age" => 30, "city" => "New York"}
person
# Though the above syntax is the default, Elixir provides a syntactic sugar which looks very close to python dictionary.
# but you will discover that the space after the colon is required, unlike python :)
= %{name: "Alice", age: 30, city: "New York"} # atom keys - cleaner syntax
atom_map
# you might question the need for the `%` before the curly braces, Elixir uses curly braces alone for tuples. So, maps need that extra symbol `%`
= %{}
empty_map
# These are identical:
{name: "Alice", age: 30} # Shorthand syntax
%# this is converted to the following syntax - coming from python or JS, the above syntax is easier
{:name => "Alice", :age => 30} # Explicit atom syntax %
4.2 Accessing Values
Python:
= person["name"] # Raises KeyError if key doesn't exist
name = person.get("age", 0) # Returns default value if missing age
Elixir:
# Returns nil if key doesn't exist. Thank God - no exceptions when keys are not found. It is a graceful nil.
= person["name"]
name = Map.get(person, "age", 0) # Returns default value if missing
age
# Atom keys enable dot notation:
= atom_map.name # Raises KeyError if key doesn't exist
name = atom_map[:name] # Returns nil if key doesn't exist
name
# Unlike Python where KeyError is always raised, Elixir gives you choice:
# - Use dot notation when you expect the key to exist (fails fast)
# - Use bracket notation when the key might be missing (returns nil)
4.3 Adding/Updating Values
Python:
"email"] = "alice@example.com" # Add new key
person["age"] = 31 # Update existing key person[
Elixir:
# Returns new map (immutable!)
= Map.put(person, "email", "alice@example.com") # Adds or updates key
updated_person = %{person | "age" => 31} # Update syntax - key must exist or raises KeyError
updated_person
# Different behaviors for adding vs updating:
# Map.put/3 - always works (add or update)
# | syntax - only for updating existing keys (safer, fails if key missing)
4.4 Checking for Keys
Python:
if "name" in person:
print(person["name"])
Elixir:
if Map.has_key?(person, "name") do
IO.puts(person["name"])
end
4.5 Removing Keys
Python:
del person["city"] # Modifies in place
"age", None) # Returns value, with default person.pop(
Elixir:
= Map.delete(person, "city") # Returns new map
updated_person {value, updated_person} = Map.pop(person, "age", nil) # Returns {value, new_map}
4.6 Getting All Keys/Values
Python:
= list(person.keys())
keys = list(person.values())
values = list(person.items()) # [(key, value), ...] items
Elixir:
= Map.keys(person)
keys = Map.values(person)
values # No direct equivalent to items() - use Enum.to_list/1 if needed
4.7 Merging Maps
Python:
= {"theme": "dark", "notifications": True}
defaults = {"theme": "light", }
user_prefs = {**defaults, **user_prefs} # user_prefs overrides defaults settings
Elixir:
= %{"theme" => "dark", "notifications" => true}
defaults = %{"theme" => "light", }
user_prefs = Map.merge(defaults, user_prefs) # same behaviour as python settings
4.8 Pattern Matching (Elixir Exclusive)
Python (traditional approach):
def greet_person(person):
if person.get("name"):
return f"Hello, {person['name']}!"
return "Hello, stranger!"
Elixir (with pattern matching):
def greet_person(%{"name" => name}), do: "Hello, #{name}!"
def greet_person(_), do: "Hello, stranger!"
4.9 Key Differences
- Syntax: Python uses
{}
, Elixir uses%{}
- Mutability: Python dictionaries are mutable (modify in place), Elixir maps are immutable (return new maps)
- Atom vs String Keys: Elixir atom keys (
%{key: value}
) enable dot notation access and cleaner syntax, while string keys (%{"key" => value}
) are better for external data
4.10 Atom Keys vs String Keys
Use atom keys when: - Working with internal application data structures - You know the keys at compile time - You want cleaner, more readable code with dot notation
Use string keys when: - Receiving data from external sources (JSON, APIs) - Keys are dynamic or come from user input - Interfacing with external systems
Python Pro
that is great - so, you have used an example where pattern matching is done on function args - that is intriguing. I think you have not covered this before.
AI Elixir Mentor
You’re absolutely right! I introduced pattern matching in function arguments without explaining it first - that’s a sneak peek of a powerful Elixir feature we haven’t covered yet.
In that example:
def greet_person(%{"name" => name}), do: "Hello, #{name}!"
def greet_person(_), do: "Hello, stranger!"
What’s happening is: - First function clause: Only matches if the argument is a map with a “name” key, and extracts that value into the variable name
- Second function clause: Matches anything else (the _
means “I don’t care what this is”)
This is function clause pattern matching - you can define multiple versions of the same function that handle different patterns of input. Elixir automatically chooses the right one based on what you pass in.
But you’re right - we should cover this properly! This is part of “Pattern Matching in Practice” from our roadmap.
For now, let’s stick with basic maps and get comfortable with the syntax. Try the 10 exercises that will help you firmly plant the differences in syntax in Elixir for maps.
4.10.1 Elixir Map Practice Exercises
Practice your map manipulation skills with these graded exercises
Welcome to your Elixir map practice session! These exercises are designed to help you build confidence working with maps and the Enum module. Start with the easier problems and work your way up to more challenging ones.
4.11 🎯 Quick Reference
As you work through these exercises, remember:
Enum.map/2
- Transform each elementEnum.filter/2
- Select elements that match a condition
Enum.reduce/3
- Combine all elements into a single value
4.11.1 Common Patterns You’ll Use:
# Access a field
.name # or employee[:name]
employee
# Create a new map
{name: employee.name, salary: employee.salary}
%
# Update an existing map
{employee | salary: new_salary}
%
# Chain operations
|> filter(...) |> map(...) |> reduce(...) data
4.12 Setup Data
Copy this employee data into your Elixir session - you’ll use it for all exercises:
= [
employees {name: "Alice", department: "Engineering", salary: 85000, experience: 5},
%{name: "Bob", department: "Marketing", salary: 65000, experience: 3},
%{name: "Carol", department: "Engineering", salary: 95000, experience: 8},
%{name: "Dave", department: "Sales", salary: 70000, experience: 4},
%{name: "Frank", department: "Marketing", salary: 82000, experience: 6},
%{name: "Eve", department: "Engineering", salary: 78000, experience: 2},
%]
4.12.1 Exercise 1: Your First Map Transformation
Goal: Get comfortable with Enum.map/2
Create a new list that contains only the names of all employees.
What you should get:
["Alice", "Bob", "Carol", "Dave", "Frank", "Eve"]
Try it yourself first! Then check your answer below.
💡 Hint: Use Enum.map/2
and access the :name
field from each employee map.
✅ Solution:
Enum.map(employees, fn employee -> employee.name end)
# Or using the shorthand:
Enum.map(employees, & &1.name)
# piping
|> Enum.map(fn x -> x.name end) employees
4.12.2 Exercise 2: Simple Filtering
Goal: Learn basic filtering with Enum.filter/2
Find all employees who work in the “Engineering” department.
What you should get:
[
{name: "Alice", department: "Engineering", salary: 85000, experience: 5},
%{name: "Carol", department: "Engineering", salary: 95000, experience: 8},
%{name: "Eve", department: "Engineering", salary: 78000, experience: 2}
%]
💡 Hint: Use Enum.filter/2
and check if the department equals “Engineering”.
✅ Solution:
Enum.filter(employees, fn employee ->
.department == "Engineering"
employeeend)
# Or using the shorthand:
Enum.filter(employees, &(&1.department == "Engineering"))
4.12.3 Exercise 3: Basic Math with Reduce
Goal: Learn to aggregate data with Enum.reduce/3
Calculate how many total years of experience all employees have combined.
What you should get: 28
💡 Hint: Start with 0 and add each employee’s experience to your accumulator.
✅ Solution:
Enum.reduce(employees, 0, fn employee, total_experience ->
+ employee.experience
total_experience end)
4.12.4 Exercise 4: Creating New Maps
Goal: Practice creating new map structures
Create a list of maps that shows each employee’s name and their salary increased by 10%.
What you should get:
[
{name: "Alice", new_salary: 93500.0},
%{name: "Bob", new_salary: 71500.0},
%# ... and so on
]
💡 Hint: Use Enum.map/2
to create completely new maps with just the name and calculated salary.
✅ Solution:
Enum.map(employees, fn employee ->
{name: employee.name, new_salary: employee.salary * 1.1}
%end)
4.12.5 Exercise 5: Combining Filter and Map
Goal: Chain operations together
Get the names of all employees who earn more than $75,000, and sort them alphabetically.
What you should get: ["Alice", "Carol", "Eve", "Frank"]
💡 Hint: You’ll need three steps: filter by salary, map to get names, then sort.
✅ Solution:
employees|> Enum.filter(&(&1.salary > 75000))
|> Enum.map(& &1.name)
|> Enum.sort()
4.13 Tuples
Python Pro
That is great - what is next?
AI Elixir Mentor
Excellent! Now that you’ve practiced maps, let’s move on to Tuples - the other essential data structure you’ll see constantly in Phoenix.
4.14 Tuples in Elixir vs Python
Tuples are ordered collections of elements, but they serve different purposes in each language.
4.15 Creating and Using Tuples
Python:
# Creating tuples
= (3, 4)
point = ("Alice", 30, "Engineer")
person = ()
empty_tuple = (42,) # Note the comma!
single_item
# Accessing elements (by index)
= point[0] # 3
x = point[1] # 4 y
Elixir:
# Creating tuples
= {3, 4}
point = {"Alice", 30, "Engineer"}
person = {}
empty_tuple = {42} # No comma needed
single_item
# Accessing elements (by pattern matching most of the time, not index)
{x, y} = point # x = 3, y = 4
{name, age, job} = person # name = "Alice", age = 30, job = "Engineer"
# if you really want index acces, then you still have it
(person, 0 ) #> "Alice" elem
4.16 Key Difference: Usage Philosophy
Python tuples are often used like immutable lists:
= [(1, 2), (3, 4), (5, 6)]
coordinates for x, y in coordinates:
print(f"Point: {x}, {y}")
Elixir tuples are used for fixed-size, structured data with meaning. That is why accessing the elements using index is not supported. The expectation is that there is a structured list of items in a certain order inside the tuple and you will use pattern matching to extract it.
# Return values with status
{:ok, user} = create_user("Alice")
{:error, reason} = create_user("")
# Coordinates
{x, y} = {10, 20}
# RGB colors
{r, g, b} = {255, 128, 0}
4.17 The Phoenix Pattern: Status Tuples
This is the pattern you’ll see everywhere in Phoenix:
# Success case
{:ok, user} = User.create(%{name: "Alice", email: "alice@example.com"})
# Error case
{:error, changeset} = User.create(%{name: "", email: "invalid"})
Try creating a few tuples in iex and pattern matching them:
# Try these in iex:
= {:ok, "Success!"}
result {status, message} = result
= {:error, "Something went wrong"}
error {:error, reason} = error
What do you get when you run these? Examine the values of message and reason and see whether they match your expectation.
Python Pro
Yes they work as expected. Can we do some exercises using tuples?
AI Elixir Mentor
4.18 🎯 Multiple Function Clauses
Before we dive into the exercises, let’s learn a powerful Elixir feature: multiple function clauses in anonymous functions. This is especially useful when working with tuples that follow patterns like :ok/:error
.
4.18.1 The Problem with Simple Conditions
You’ve learned to filter lists using simple conditions:
# Simple filtering - works great for basic conditions
Enum.filter([1, 2, 3, 4, 5], fn x -> x > 3 end)
# Result: [4, 5]
But with tuples like {:ok, data}
and {:error, message}
, checking conditions gets awkward:
# Awkward way to filter for :ok tuples
Enum.filter(api_responses, fn response ->
(response, 0) == :ok
elemend)
4.18.2 The Elegant Solution: Multiple Function Clauses
Instead, Elixir lets you write multiple clauses in your anonymous function, each handling a different pattern:
Enum.filter(api_responses, fn
{:ok, _} -> true # If it matches {:ok, anything}, return true
{:error, _} -> false # If it matches {:error, anything}, return false
end)
4.18.3 How It Works
- Pattern Matching: Each clause tries to match the input against its pattern
- First Match Wins: Elixir uses the first clause that matches
- Automatic Destructuring: The
_
means “match anything but don’t store it”
4.18.4 More Examples
# Extract data based on tuple type
Enum.map(responses, fn
{:ok, data} -> data # Extract the data part
{:error, msg} -> "Error: #{msg}" # Transform error to string
end)
# Count different types
Enum.reduce(responses, %{good: 0, bad: 0}, fn
{:ok, _}, acc -> %{acc | good: acc.good + 1}
{:error, _}, acc -> %{acc | bad: acc.bad + 1}
end)
4.18.5 Why This is Powerful
- Readable: Each case is clearly separate
- Safe: Elixir ensures all patterns are handled
- Flexible: Easy to add new tuple types later
Now let’s practice this technique!
4.19 Setup Data
Copy this data into your Elixir session:
# API responses (simulating web service calls)
= [
api_responses {:ok, %{id: 1, name: "Alice", email: "alice@example.com"}},
{:error, "Invalid email format"},
{:ok, %{id: 2, name: "Bob", email: "bob@example.com"}},
{:error, "User already exists"},
{:ok, %{id: 3, name: "Carol", email: "carol@example.com"}}
]
4.20 Exercise 1: Filter Successful Results
Goal: Work with tuple pattern matching
From the api_responses
list, get only the successful responses (those that start with :ok
).
What you should get:
[
{:ok, %{id: 1, name: "Alice", email: "alice@example.com"}},
{:ok, %{id: 2, name: "Bob", email: "bob@example.com"}},
{:ok, %{id: 3, name: "Carol", email: "carol@example.com"}}
]
💡 Hint: Use Enum.filter/2
and pattern match on the first element of each tuple.
✅ Solution:
Enum.filter(api_responses, fn
{:ok, _} -> true
{:error, _} -> false
end)
4.21 Exercise 2: Extract User Data
Goal: Combine filtering and mapping
From the api_responses
, get only the user data (maps) from successful responses.
What you should get:
[
{id: 1, name: "Alice", email: "alice@example.com"},
%{id: 2, name: "Bob", email: "bob@example.com"},
%{id: 3, name: "Carol", email: "carol@example.com"}
%]
💡 Hint: Filter for :ok
responses, then map to extract just the data part.
✅ Solution:
api_responses|> Enum.filter(fn {:ok, _} -> true; {:error, _} -> false end)
|> Enum.map(fn {:ok, data} -> data end)
4.22 Exercise 3: Count Success vs Error
Goal: Analyze tuple patterns with reduce
Count how many successful vs error responses you have in api_responses
.
What you should get: %{success: 3, errors: 2}
💡 Hint: Use Enum.reduce/3
with pattern matching in the function clauses.
✅ Solution:
Enum.reduce(api_responses, %{success: 0, errors: 0}, fn
{:ok, _}, acc -> %{acc | success: acc.success + 1}
{:error, _}, acc -> %{acc | errors: acc.errors + 1}
end)
4.23 Exercise 4: Collect Error Messages
Goal: Extract specific data from error tuples
Get all the error messages from failed responses in api_responses
.
What you should get: ["Invalid email format", "User already exists"]
💡 Hint: Filter for :error
tuples, then map to extract the messages.
✅ Solution:
api_responses|> Enum.filter(fn {:error, _} -> true; {:ok, _} -> false end)
|> Enum.map(fn {:error, message} -> message end)
4.24 Exercise 5: Transform to Status Report
Goal: Create a comprehensive summary
Transform the api_responses
into a status report that shows total requests, successful user IDs, and error messages.
What you should get:
{
%total_requests: 5,
successful_user_ids: [3, 2, 1],
error_messages: ["User already exists", "Invalid email format"]
}
💡 Hint: Use Enum.reduce/3
with pattern matching to build up the report map.
✅ Solution:
Enum.reduce(api_responses, %{total_requests: 0, successful_user_ids: [], error_messages: []}, fn
{:ok, user}, acc ->
{acc |
%total_requests: acc.total_requests + 1,
successful_user_ids: [user.id | acc.successful_user_ids]
}
{:error, message}, acc ->
{acc |
%total_requests: acc.total_requests + 1,
error_messages: [message | acc.error_messages]
}
end)
4.25 🎉 Key Takeaways
After completing these exercises, you should understand:
- Multiple Function Clauses: How to write
fn pattern1 -> result1; pattern2 -> result2 end
- Pattern Matching: How
{:ok, _}
matches any 2-tuple starting with:ok
- Real-World Patterns: How Phoenix/Elixir commonly uses
{:ok, data}
and{:error, reason}
tuples - Destructuring: How to extract data directly in the pattern:
{:ok, user}
gives you access touser
This pattern matching approach makes your code more readable, safer, and more “Elixir-like” than using conditional statements!
4.26 Keyword Lists
Python Pro
that is great - let us cover keyword lists.
AI Elixir Mentor
5 Keyword Lists in Elixir vs Python
Keyword lists are a special type of list in Elixir that store key-value pairs. They’re similar to Python’s dictionaries but with important differences.
5.1 Creating Keyword Lists
Python (dictionaries):
# Creating dictionaries
= {"host": "localhost", "port": 3000, "ssl": True}
config = {"class": "btn", "id": "submit", "disabled": True} html_attrs
Elixir (keyword lists):
# Creating keyword lists - two syntaxes
= [host: "localhost", port: 3000, ssl: true]
config = [{:host, "localhost"}, {:port, 3000}, {:ssl, true}] # Explicit syntax
config
= [class: "btn", id: "submit", disabled: true] html_attrs
5.2 Accessing Values
Python:
= config["host"]
host = config.get("port", 8080) # With default port
Elixir:
= config[:host] # Returns "localhost"
host = Keyword.get(config, :port, 8080) # With default port
5.3 Key Differences from Maps
Maps (unique keys):
= %{name: "Alice", name: "Bob"} # Second "name" overwrites first
map # Result: %{name: "Bob"}
Keyword Lists (duplicate keys allowed):
= [name: "Alice", age: 30, name: "Bob"] # Duplicates OK!
keyword_list # Result: [name: "Alice", age: 30, name: "Bob"]
# First match wins when accessing
[:name] # Returns "Alice" (first occurrence) keyword_list
5.4 When to Use Keyword Lists vs Maps
Use Keyword Lists when: - Function options/configuration - HTML attributes (can have duplicates) - Order matters - You need duplicate keys
Use Maps when: - Data modeling - Unique keys required - Fast key lookups needed
5.5 Phoenix Examples
You’ll see keyword lists everywhere in Phoenix for configuration and options:
Function options:
# Phoenix route helpers
(conn, :show, user, [format: :json, locale: "en"])
user_path
# Ecto queries
User |> where([u], u.active == true) |> limit(10)
HTML attributes:
# Phoenix templates
(form, :name, [class: "form-control", placeholder: "Enter name"]) text_input
Configuration:
# Phoenix configuration
:my_app, MyApp.Repo,
config username: "postgres",
password: "secret",
database: "my_app_dev",
hostname: "localhost"
Try these examples in iex:
# Create a keyword list for HTML attributes
= [class: "btn btn-primary", id: "submit", disabled: false]
attrs
# Access values
[:class]
attrsKeyword.get(attrs, :type, "button") # With default
# Add more attributes (duplicates allowed)
= [class: "btn-large"] ++ attrs new_attrs
What do you get when you run these?
Python Pro
yes they work as expected - please give me 3 meaningful exercises.
AI Elixir Mentor
6 Keyword List Practice Exercises
Practice working with keyword lists and Phoenix-style configurations
6.1 Setup Data
Copy this data into your Elixir session:
# Phoenix-style form configurations
= [
form_configs [field: :name, type: :text, class: "form-control", required: true],
[field: :email, type: :email, class: "form-control", placeholder: "Enter email"],
[field: :age, type: :number, class: "form-control", min: 18, max: 100],
[field: :submit, type: :submit, class: "btn btn-primary", value: "Save User"]
]
6.2 Exercise 1: Extract Form Field Names
Goal: Work with keyword list access
Extract all the field names from the form_configs
.
What you should get: [:name, :email, :age, :submit]
💡 Hint: Use Enum.map/2
and access the :field
key from each keyword list.
✅ Solution:
Enum.map(form_configs, &(&1[:field]))
# Or more explicitly:
Enum.map(form_configs, fn config -> config[:field] end)
6.3 Exercise 2: Filter Input Fields Only
Goal: Filter based on keyword list values
Get only the form configurations that are input fields (not submit buttons). Filter out any field where type
is :submit
.
What you should get:
[
[field: :name, type: :text, class: "form-control", required: true],
[field: :email, type: :email, class: "form-control", placeholder: "Enter email"],
[field: :age, type: :number, class: "form-control", min: 18, max: 100]
]
💡 Hint: Use Enum.filter/2
and check if the :type
is not equal to :submit
.
✅ Solution:
Enum.filter(form_configs, fn config ->
[:type] != :submit
configend)
6.4 Exercise 3: Build HTML Attributes String
Goal: Transform keyword lists into strings
For each form configuration, create a string that could be used as HTML attributes. Include type
, class
, and any other attributes (but skip field
).
What you should get:
[
"type=\"text\" class=\"form-control\" required=\"true\"",
"type=\"email\" class=\"form-control\" placeholder=\"Enter email\"",
"type=\"number\" class=\"form-control\" min=\"18\" max=\"100\"",
"type=\"submit\" class=\"btn btn-primary\" value=\"Save User\""
]
💡 Hint: Use Enum.map/2
to transform each config. Filter out the :field
key, then build a string from the remaining key-value pairs.
✅ Solution:
Enum.map(form_configs, fn config ->
config|> Keyword.delete(:field) # Remove the field key
|> Enum.map(fn {key, value} -> "#{key}=\"#{value}\"" end)
|> Enum.join(" ")
end)
These exercises show you the practical Phoenix patterns you’ll encounter: extracting field information, filtering configurations, and building HTML attributes from keyword lists!
Python Pro
that is great. I am happy to see examples relevant to phoenix. Is that all with the data structures or do we have more?
AI Elixir Mentor
Great question! We’ve covered the essential three data structures you’ll use 90% of the time in Phoenix:
✅ Lists - [1, 2, 3]
(collections, sequences)
✅ Maps - %{key: value}
(data modeling, JSON)
✅ Tuples - {:ok, result}
(status returns, fixed structures)
✅ Keyword Lists - [key: value]
(options, HTML attributes)
6.5 Other Data Structures (Less Common)
There are a few more, but they’re used in specific situations:
Structs - Like maps but with predefined fields (you’ll see these in Phoenix for schemas)
User{name: "Alice", email: "alice@example.com"} %
Binaries/Strings - We’ve been using these, but there’s more depth
<<"hello">> # Binary representation
Ranges - For sequences
1..10 # Range from 1 to 10
6.6 My Recommendation
Let’s move on to control flow next! You have solid data structure knowledge for Phoenix. The patterns you’ll need most are:
- case statements (handling those
{:ok, result}
tuples) - with statements (chaining operations that might fail)
- Function clauses (multiple function definitions with pattern matching)
These will help you understand Phoenix’s error handling and request processing patterns.
Ready to tackle case
statements? They’re perfect for handling the tuple patterns you just practiced!
6.7 Structs
Python Pro
let us cover Structs and then wrap up data structures.
AI Elixir Mentor
7 Structs in Elixir
Structs are like maps with superpowers - they have predefined fields and belong to a specific module. Think of them as “typed maps” that give you more structure and safety.
7.1 Structs vs Maps vs Python Classes
Python classes:
class User:
def __init__(self, name, email, age=None):
self.name = name
self.email = email
self.age = age
= User("Alice", "alice@example.com", 30)
user print(user.name) # "Alice"
Elixir maps (what we’ve been using):
= %{name: "Alice", email: "alice@example.com", age: 30}
user .name # "Alice" user
Elixir structs:
# First, define the struct
defmodule User do
defstruct [:name, :email, :age]
end
# Then create instances
= %User{name: "Alice", email: "alice@example.com", age: 30}
user .name # "Alice" user
7.2 Defining Structs
defmodule BlogPost do
defstruct [:title, :content, :author, published: false, views: 0]
# ↑ required fields ↑ fields with defaults
end
7.3 Creating and Using Structs
# Create a new struct
= %BlogPost{
post title: "Learning Elixir",
content: "Elixir is amazing!",
author: "Alice"
}
# published: false, views: 0 (defaults)
# Access fields (same as maps)
.title # "Learning Elixir"
post.published # false
post
# Update fields (same as maps)
= %{post | published: true, views: 100} published_post
7.4 Key Advantages of Structs
1. Field Validation:
# This works (all required fields provided)
User{name: "Alice", email: "alice@example.com"}
%
# This fails at compile time (missing required field)
User{name: "Alice"} # Error: missing required key :email %
2. Pattern Matching:
def greet_user(%User{name: name}), do: "Hello, #{name}!"
def greet_user(_), do: "Hello, stranger!"
# Only matches User structs, not regular maps
(%User{name: "Alice"}) # "Hello, Alice!"
greet_user(%{name: "Bob"}) # "Hello, stranger!" greet_user
3. Clear Data Contracts:
# You know exactly what fields a User has
# No surprises like with regular maps
7.5 Phoenix Examples
In Phoenix, you’ll see structs everywhere:
User Schema:
defmodule MyApp.User do
use Ecto.Schema
"users" do
schema :name, :string
field :email, :string
field :age, :integer
field
()
timestampsend
end
# Creates a struct like:
# %MyApp.User{id: nil, name: nil, email: nil, age: nil, ...}
Changeset (for form validation):
= %Ecto.Changeset{
changeset data: %User{},
changes: %{name: "Alice"},
errors: [],
valid?: true
}
7.6 Try It Yourself
Try these in iex:
# Define a simple struct
defmodule Task do
defstruct [:title, :description, completed: false, priority: :medium]
end
# Create some tasks
= %Task{title: "Learn Elixir", description: "Study structs"}
task1 = %Task{title: "Build Phoenix app", description: "Create a todo app", priority: :high}
task2
# Update a task
= %{task1 | completed: true} completed_task
What do you get when you run these?
Python Pro
they work nicely. But what is that message when I tried to execute defmodule. By the way, what is defmodule? You have not yet covered it, I guess.
warning: redefining module Task (current version loaded from /opt/homebrew/Cellar/elixir/1.18.3/lib/elixir/lib/elixir/ebin/Elixir.Task.beam)
iex:121: Task (module)
└─
{:module, Task,
<<70, 79, 82, 49, 0, 0, 9, 244, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 247,
0, 0, 0, 22, 11, 69, 108, 105, 120, 105, 114, 46, 84, 97, 115, 107, 8, 95,
95, 105, 110, 102, 111, 95, 95, 10, 97, ...>>,
Task{title: nil, description: nil, completed: false, priority: :medium}} %
AI Elixir Mentor
Great observation! You’ve discovered two important things:
7.7 1. The Warning Message
The warning tells you that Task
is already a built-in Elixir module! Elixir has a Task
module for running concurrent processes, so when you defined your own Task
module, you overwrote it.
This is like accidentally naming a Python class list
or dict
- it works, but shadows the built-in.
Better naming:
defmodule TodoItem do # Better name!
defstruct [:title, :description, completed: false, priority: :medium]
end
7.8 2. What is defmodule
?
You’re absolutely right - I haven’t covered modules yet! I jumped ahead because structs require modules.
Quick explanation: - defmodule
creates a new module (like a namespace/container for functions) - Modules organize related functions together - defstruct
inside a module creates a struct type
Think of it like:
# Python
class TodoItem: # Class definition
def __init__(self, title, description, completed=False):
self.title = title
# etc.
# Elixir
defmodule TodoItem do # Module definition
defstruct [:title, :description, completed: false] # Struct definition
end
7.9 The Return Value
That long binary output <<70, 79, 82...>>
is the compiled module bytecode - just ignore it! The important part is the struct at the end showing the default values.
Let’s use a safe name:
defmodule BlogPost do
defstruct [:title, :content, published: false]
end
= %BlogPost{title: "Hello", content: "World"} post
Try this version - no warnings!
Python Pro
yes they work nicely.
AI Elixir Mentor
Perfect! Now you have a solid understanding of all the essential data structures for Phoenix development:
7.10 Data Structures Summary ✅
Lists - [1, 2, 3]
- Collections and sequences
Maps - %{key: value}
- General data modeling
Tuples - {:ok, result}
- Status returns and fixed structures
Keyword Lists - [key: value]
- Options and configurations
Structs - %User{name: "Alice"}
- Typed data with validation
You now understand: - When to use each data structure - How they compare to Python equivalents - The Phoenix patterns you’ll see in real applications - Pattern matching with all of them
7.11 Ready for Control Flow!
Now let’s move on to control flow - specifically the patterns that handle all those {:ok, result}
and {:error, reason}
tuples you’ve been practicing with.
Next up: case
statements - the elegant way to handle different outcomes in Elixir. This is where pattern matching really shines and where you’ll see the Phoenix request/response patterns come together.
Ready to learn how to handle success and error cases like a pro?
7.12 Control flow
Python Pro
please do a high-level summary of control flows and then we can go into a little more detail, one by one.
AI Elixir Mentor
8 Control Flow in Elixir - High-Level Overview
Control flow in Elixir is fundamentally different from Python because it’s built around pattern matching and immutability rather than traditional imperative constructs.
8.1 Python vs Elixir Philosophy
Python (Imperative):
# Tell the computer HOW to do things step by step
if condition:
do_this()
modify_variable()else:
do_that() modify_other_variable()
Elixir (Functional/Declarative):
# Tell the computer WHAT you want based on data patterns
case data do
{:ok, result} -> handle_success(result)
{:error, reason} -> handle_error(reason)
end
8.2 The 4 Main Control Flow Constructs
8.2.1 1. case
- Pattern Matching Control
Purpose: Handle different data patterns
Python equivalent: Complex if/elif chains + type checking
case api_call() do
{:ok, user} -> "Welcome #{user.name}!"
{:error, :not_found} -> "User not found"
{:error, reason} -> "Error: #{reason}"
end
8.2.2 2. cond
- Condition-Based Control
Purpose: Multiple conditions (like if/elif)
Python equivalent: if/elif/else chains
cond do
< 13 -> "child"
age < 20 -> "teenager"
age < 65 -> "adult"
age true -> "senior"
end
8.2.3 3. with
- Happy Path Programming
Purpose: Chain operations that might fail
Python equivalent: Nested try/catch or early returns
{:ok, user} <- get_user(id),
with {:ok, account} <- get_account(user),
{:ok, balance} <- check_balance(account) do
{:ok, balance}
else
-> error
error end
8.2.4 4. if/unless
- Simple Conditions
Purpose: Basic true/false decisions
Python equivalent: Simple if statements
if user.admin? do
"Admin access granted"
else
"Regular user"
end
8.3 Key Differences from Python
Python | Elixir | Why Different? |
---|---|---|
if/elif/else |
case with patterns |
Pattern matching is more powerful |
try/except |
case with tuples |
Errors are data, not exceptions |
Multiple returns | Single expression result | Everything is an expression |
Mutation in branches | New data in branches | Immutability |
8.4 Phoenix Patterns You’ll See
Handling web requests:
case authenticate_user(token) do
{:ok, user} -> render_dashboard(user)
{:error, :invalid_token} -> redirect_to_login()
{:error, :expired} -> show_refresh_page()
end
Database operations:
{:ok, user} <- Users.create(params),
with {:ok, _email} <- send_welcome_email(user) do
{:ok, user}
else
{:error, changeset} -> {:error, changeset}
-> {:error, "Failed to create user"}
error end
8.5 Learning Order
case
- Most important, handles the{:ok, result}
patternswith
- Essential for Phoenix request pipelines
cond
- Simple but usefulif/unless
- You already know this concept
Ready to start with case
statements? They’re the heart of Elixir’s error handling!
Python Pro
yes - let us start with case statements.
AI Elixir Mentor
9 Case Statements in Elixir
Case statements are Elixir’s most powerful control flow construct. They use pattern matching to decide what to do based on the shape and content of your data.
9.1 Basic Syntax
case expression do
-> result1
pattern1 -> result2
pattern2 -> result3
pattern3 end
9.2 Simple Example vs Python
Python (if/elif/else):
def handle_response(status_code):
if status_code == 200:
return "Success"
elif status_code == 404:
return "Not found"
elif status_code == 500:
return "Server error"
else:
return f"Unknown status: {status_code}"
Elixir (case):
def handle_response(status_code) do
case status_code do
200 -> "Success"
404 -> "Not found"
500 -> "Server error"
-> "Unknown status: #{code}"
code end
end
9.3 Pattern Matching Power
This is where case statements become much more powerful than Python’s if/elif:
Matching on data structure:
case api_response do
{:ok, %{name: name, age: age}} ->
"User: #{name}, Age: #{age}"
{:error, "not_found"} ->
"User doesn't exist"
{:error, reason} ->
"Error occurred: #{reason}"
->
_ "Unexpected response format"
end
Python equivalent (much more verbose):
if (isinstance(api_response, tuple) and
len(api_response) == 2 and
0] == "ok"):
api_response[= api_response[1]
user_data if isinstance(user_data, dict) and "name" in user_data:
return f"User: {user_data['name']}, Age: {user_data.get('age')}"
elif (isinstance(api_response, tuple) and
len(api_response) == 2 and
0] == "error"):
api_response[if api_response[1] == "not_found":
return "User doesn't exist"
else:
return f"Error occurred: {api_response[1]}"
else:
return "Unexpected response format"
9.4 Phoenix Examples
Handling user authentication:
case authenticate_user(token) do
{:ok, %User{admin: true} = user} ->
(user)
render_admin_dashboard
{:ok, user} ->
(user)
render_user_dashboard
{:error, :invalid_token} ->
()
redirect_to_login
{:error, :expired} ->
()
show_token_expired_pageend
9.5 Try It Yourself
Try these examples in iex:
# Simple number matching
= 25
age case age do
when x < 13 -> "child"
x when x < 20 -> "teenager"
x when x < 65 -> "adult"
x -> "senior"
_ end
# Tuple pattern matching
= {:ok, "Success!"}
result case result do
{:ok, message} -> "Great: #{message}"
{:error, reason} -> "Failed: #{reason}"
end
What do you get when you run these?