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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Keep it minimal - focus on the 20% of the language that will the reader 80% returns on his time invested
  7. 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.

  1. Pattern matching
  2. Immutability by default
  3. 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.

iex(38)> i "cat"
Term
  "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?

iex(39)> h

                                  IEx.Helpers

Welcome to Interactive Elixir. You are currently seeing the documentation for
the module IEx.Helpers which provides many helpers to make Elixir's shell more
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.

iex(40)> h h

                                    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

    iex> h(Enum)

It also accepts functions in the format function/arity and
module.function/arity, for example:

    iex> h(receive/1)
    iex> h(Enum.all?/2)
    iex> h(Enum.all?)

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.

numbers = [1, 2, 3]
a, b, c = numbers
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:

iex> list = [1, 2, 3]

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:

  1. You can immediately see the result of your operations
  2. You can chain operations together, since each returns a value
  3. You get immediate feedback that your code worked as expected

To achieve the same effect, in Python, you have to type the variable again:

In [3]: numbers = [1,2,3]

In [4]: numbers
Out[4]: [1, 2, 3]

Now, let’s try the destructuring part. In Elixir, type:

iex> a, b, c = numbers

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 :)

iex(2)> a, b, c = numbers
** (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.

numbers = [1, 2, 3]
a, b, c = numbers
  1. Left side: a, b, c creates a tuple as if you have written like this: (a, b, c)
  2. Right side: numbers is a list, but can be any iterable (tuple, string, etc.)
  3. 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 NOT IO.putb for putting the binary. And why not use the word print as it is simple enough for everyone to understand?

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.

iex(8)> IO.puts(a, b)
** (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.

The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) – Joel on Software

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:

numbers = [1, 2, 3]        # Assignment, no output
a, b, c = numbers          # Destructuring (implicit tuple creation)

Elixir:

list = [1, 2, 3]           # Pattern matching, returns [1, 2, 3]
[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:

fruits = ["apple", "banana", "cherry"]
first, second, third = fruits
print(f"I like {first} the most!")

Python Pro

Yes, I succeeded and here is my code

fruits = ["apple", "banana", "cherry"]
[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:

numbers = [1, 2, 3]
[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:

x = 5    # x is now bound to 5
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:

x = 5
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)

x = a + 1  # Python

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!)

x = a + 1  # If a is bound, x gets bound to a + 1
a + 1 = x  # If x is bound, assert that x equals a + 1

This works both directions like algebra!

1.9 The “Unlearning” Journey

  1. Childhood: You learned x = 5 + 1 means “x equals 6”
  2. Programming school: You unlearned this - = now means “assignment”
  3. 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:

x = 5 + 1  # We calculate 5 + 1 = 6, then bind x to 6

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):

x = 6        # x is bound to 6 now
6 = x        # Success! Assert "6 equals x"
6 = 5 + 1    # Success! Assert "6 equals 5 + 1"

# With pin operator for already-bound variables:

a = 5
^x = ^a + 1    # Assert "a(5) + 1 equals x(6)" ✓
^a + 1 = ^x    # This also works ✓

In Python:

x = 6
6 = x        # SyntaxError: cannot assign to literal here. Maybe you meant '==' instead of '='?
x = a + 1    # This will assign 6 to x and NOT match
a + 1 = x    # SyntaxError: cannot assign to expression here. Maybe you meant '==' instead of '='?

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:

result = some_function()
if result[0] != "ok":
    raise ValueError("Expected success tuple")
actual_result = result[1]

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:

coords = get_coordinates()
x, y, z = coords
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"]
_middle  # This works - returns "banana"

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:


head, *tail = [1, 2, 3, 4]
# 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.

head, *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?

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
first, *middle, last = [1, 2, 3, 4] # I want the first and last alone and the rest in a list

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
first, *middle, last = [1, 2, 3, 4] # first=1, middle=[2,3], last=4

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:

numbers = [1, 2, 3]
numbers.append(4)
print(numbers)  # What gets printed?

And what happens to any other variables that were pointing to the same list?

backup = numbers
numbers.append(5)
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):
    data_list.append("processed")  # Oops! Modified the original
    return data_list

original = [1, 2, 3]
result = process_data(original)
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:

numbers = [1, 2, 3]

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:

numbers = [1, 2, 3]
new_numbers = [4 | numbers]

What do you get? And then check what numbers contains - has it changed?

Alternatively, you can try:

new_numbers = numbers ++ [5]

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:

new_numbers = numbers ++ 5

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:

new_numbers = numbers ++ [5]

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

df.dropna().fillna(0).groupby('column').sum()  # Each method returns a new DataFrame

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
numbers = [1, 2, 3]
step1 = [0 | numbers]        # [0, 1, 2, 3]
step2 = step1 ++ [4]         # [0, 1, 2, 3, 4]
final = [99 | step2]         # [99, 0, 1, 2, 3, 4]

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:

  1. Started with [1, 2, 3]
  2. Added 4 to the end: [1, 2, 3, 4]
  3. 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:

original = [1, 2, 3]
new_list = [0 | original]  # [0, 1, 2, 3]

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
  step1 = [1, 2, 3]
  step2 = [0 | step1]     # step1 might be collected if not used elsewhere
  step3 = step2 ++ [4]    # step2 might be collected
  step3                   # Only step3 survives
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:

numbers = [1, 2, 3, 4]
for num in numbers:
    print(num * 2)

Elixir equivalent:

numbers = [1, 2, 3, 4]
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)

fruits = ["apple", "banana", "cherry"]
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)

print_fruit = lambda x: print(f"{x[0]}: {x[1]}")
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):

fruits = ["apple", "banana", "cherry"]
list(map(lambda x: print(f"{x[0]}: {x[1]}"), enumerate(fruits)))

Elixir (reads left to right):

fruits = ["apple", "banana", "cherry"]

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):

result = map(lambda x: print(f"{x[0]}: {x[1]}"), enumerate(fruits))  # generator prints memory address :)
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.29.1 The Real Problem: Side Effects and Shared State

Python (mutation inside loop possible):

fruits = ["apple", "banana", "cherry"]

for i, fruit in enumerate(fruits):
    fruits[i] = fruit.upper() # Mutating the original list!

print(fruits) # ['APPLE', 'BANANA', 'CHERRY']

Why this is dangerous:

original_fruits = ["apple", "banana", "cherry"]
backup_fruits = original_fruits # Both point to same list!

for i, fruit in enumerate(original_fruits):
    original_fruits[i] = fruit.upper() # Mutating original

print(backup_fruits) # ['APPLE', 'BANANA', 'CHERRY'] - Surprise! Backup changed too

Elixir prevents this entirely:


fruits = ["apple", "banana", "cherry"]

# You literally CANNOT mutate fruits
# No fruits[i] = new_value syntax exists

# You must create a new list:
uppercased_fruits = 
    Enum.map(fruits, 
        fn fruit -> String.upcase(fruit) end)

# fruits is still ["apple", "banana", "cherry"]

So the “no for loop” design forces you away from dangerous mutation patterns and toward safer transformation patterns.

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
[fruit.upper() for fruit in fruits]

# Add tax to prices  
[price * 1.08 for price in prices]

# 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
[word for word in words if len(word) > 5]

# Only positive numbers
[num for num in numbers if num > 0]

# Only fruits starting with `a`
[fruit for fruit in fruits if fruit.startswith("a")]

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.

fruits = ["apple", "banana", "cherry"]
[fruit.upper() for fruit in fruits]
# 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:

h Enum

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:

fruits = ["apple", "banana", "cherry"]
list(map(lambda x: x.upper(), fruits))

Python Pro

yes I got it working

fruits = ["apple", "banana", "cherry"]
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:

prices = [10.0, 25.50, 8.75]
# intentionally I am NOT giving you the functional equivalent in python to help you flex your mental muscles :)
[price * 1.08 for price in prices]  # Add 8% tax

Try translating this to Elixir. You already have all the ingredients you need!

Python Pro

yes I got it.

prices = [10.0, 25.50, 8.75]
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:

words = ["cat", "elephant", "dog", "butterfly"]
[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.

words = ["cat", "elephant", "dog", "butterfly"]
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

text = "hello world"
result = text.upper()
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

text = "hello world"
result = String.upcase(text)
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
text = "hello world"
text.upper()        # Method called ON the string
text.split()        # Method called ON the string
text.replace("l", "x")

numbers = [1, 2, 3]
numbers.append(4)   # Method called ON the list

1.43.2 Elixir: Functions Belong to Modules

# Functions live in modules, data is passed TO them
text = "hello world"
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

numbers = [1, 2, 3]
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

h String.upcase    # Shows function documentation
h String           # Shows all String module functions

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:

  1. Search for the concept, not the exact word: Search “case” instead of “upper”
  2. Use the module overview: Look at h String first to see all available functions
  3. 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
numbers = [1, 2, 3]
result = len(numbers)  # len() is an expression, returns 3

Elixir:

# This also works - everything is an expression
numbers = [1, 2, 3]
result = length(numbers)  # length() is an expression, returns 3

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)
result = 5 if True else 3  # This works

Elixir:

# This works perfectly - if is an expression that returns a value
result = if true do
  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
x = 10
# print(y = x + 5)  # SyntaxError! Assignment doesn't return anything

Elixir:

# Assignment returns the assigned value
x = 10
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
x = 10
if x > 5:
    message = "big number"
else:
    message = "small number"

print(message)  # "big number"

Elixir:

# if is an expression - it returns a value
x = 10
message = if x > 5 do
  "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
result = (lambda x, y: x + y)(3, 4)  # Returns 7

Elixir:

# def is also used to create named functions
def add(x, y) do
  x + y
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
result = (fn x, y -> x + y end).(3, 4)  # Returns 7

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
x, y = (1, 2)
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
numbers = [1, 2, 3, 4, 5]
doubled = [x * 2 for x in numbers]
filtered = [x for x in doubled if x > 4]
result = sum(filtered)
# result is 18

Elixir:

# You can chain and compose operations naturally
result = [1, 2, 3, 4, 5]
|> 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
length [1, 2, 3]        # Works fine
IO.puts "hello"         # Works fine
String.upcase "hello"   # Works fine

# With parentheses (also valid)
length([1, 2, 3])       # Also works
IO.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
fruits = ["apple", "banana", "cherry"]

# 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
prices = [10.0, 25.50, 8.75]

# 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
words = ["cat", "elephant", "dog", "butterfly"]

# 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:

  1. Readability: Code can look more like natural language
  2. Flexibility: You can choose the style that makes your code clearest
  3. Pipe operator: Supports the elegant |> chaining syntax
  4. 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
fruits = ["apple", "banana", "cherry"]
Enum.map(fruits, fn x -> String.upcase(x) end)
# Result: ["APPLE", "BANANA", "CHERRY"]

# Example 2: Add tax to prices  
prices = [10.0, 25.50, 8.75]
Enum.map(prices, fn x -> x * 1.08 end)
# Result: [10.8, 27.54, 9.45]

# Example 3: Get word lengths
words = ["cat", "elephant", "dog", "butterfly"]
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 argument
  • String.replace/3 - takes 3 arguments
  • Enum.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
fruits = ["apple", "banana", "cherry"]
list(map(str.upper, fruits))        # A bit harder to read, but module name gives it away that a function is being used...

words = ["cat", "elephant", "dog", "butterfly"] 
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
fruits = ["apple", "banana", "cherry"]
Enum.map(fruits, &String.upcase/1)  # The & says "I'm making a function"

words = ["cat", "elephant", "dog", "butterfly"]
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:
my_function = fn x -> x * 2 end
my_function.(arguments)
#           ↑ 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
double = fn x -> x * 2 end
double.(5)  # Notice the dot

# ✅ 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
  add_tax = fn price -> price * 1.08 end
  
  # Without the dot rule, what does this line mean?
  add_tax    # Am I returning the function or calling it with no args?
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
  add_tax = fn price -> price * 1.08 end
  
  add_tax           # Return the function (no dot)
end

def calculate_price(base_price) do
  add_tax = fn price -> price * 1.08 end
  
  add_tax.(base_price)  # Call the function (dot + argument)
end

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)
fruits = ["apple", "banana", "cherry"]
Enum.map(fruits, fn x -> String.upcase(x) end)
# Result: ["APPLE", "BANANA", "CHERRY"]

# Stored function version
uppercase_fn = fn x -> String.upcase(x) end
Enum.map(fruits, uppercase_fn)  # Pass the function directly (no dot)
# Result: ["APPLE", "BANANA", "CHERRY"]

Python:

# Inline version
fruits = ["apple", "banana", "cherry"]
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
prices = [10.0, 25.50, 8.75]
Enum.map(prices, fn x -> x * 1.08 end)
# Result: [10.8, 27.54, 9.45]

# Stored function version
add_tax = fn x -> x * 1.08 end
Enum.map(prices, add_tax)  # Pass the function directly
# Result: [10.8, 27.54, 9.45]

Python:

# Inline version
prices = [10.0, 25.50, 8.75]
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:

uppercase_fn = fn x -> String.upcase(x) end
fruits = ["apple", "banana", "cherry"]

# 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()

fruits = ["apple", "banana", "cherry"]

# 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:

add_tax = fn x -> x * 1.08 end
prices = [10.0, 25.50, 8.75]

# 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

prices = [10.0, 25.50, 8.75]

# 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
calculate_total = fn price, tax_rate, fee -> price * (1 + tax_rate) + fee end
prices = [10.0, 25.50, 8.75]

# 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

prices = [10.0, 25.50, 8.75]

# 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

my_function = fn x -> x * 2 end

# ❌ Wrong - missing the dot when calling directly
my_function(5)

# ✅ Correct - with the dot when calling directly
my_function.(5)

# ✅ 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

  1. MAP - Transform each item (same number out as in)
  2. FILTER - Keep only some items (fewer or same number out)
  3. 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 PatternNEXT UP - Keep only items that meet a condition - Examples to explore: filtering by length, positive numbers, etc.

REDUCE PatternCOMING 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

words = ["cat", "elephant", "dog", "butterfly"]
# Only long words
[word for word in words if len(word) > 5]
# Result: ["elephant", "butterfly"]

Example 2.2: Filter by numeric condition

numbers = [1, -2, 3, -4, 5, 0]
# Only positive numbers
[num for num in numbers if num > 0]
# Result: [1, 3, 5]

Example 2.3: Filter by string pattern

fruits = ["apple", "banana", "cherry", "apricot"]
# Only fruits starting with "a"
[fruit for fruit in fruits if fruit.startswith("a")]
# 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:

words = ["cat", "elephant", "dog", "butterfly"]
[word for word in words if len(word) > 5]  # Keep only long words
# 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

words = ["cat", "elephant", "dog", "butterfly"]
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:

numbers = [1, -2, 3, -4, 5, 0]
[num for num in numbers if num > 0]  # Keep only positive numbers
# Result: [1, 3, 5]

This should be even simpler than the string example. Give it a shot!

Python Pro

Yes done - easy, peasy

numbers = [1, -2, 3, -4, 5, 0]
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:

fruits = ["apple", "banana", "cherry", "apricot"]
[fruit for fruit in fruits if fruit.startswith("a")]  # Keep only fruits starting with "a"
# 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.

fruits = ["apple", "banana", "cherry", "apricot"]
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

numbers = [1, 2, 3, 4, 5]
sum(numbers)  # Combine into total
# Result: 15

Example 3.2: Find maximum

numbers = [3, 1, 4, 1, 5, 9, 2]
max(numbers)  # Combine into largest value
# Result: 9

Example 3.3: Join strings

words = ["Hello", "beautiful", "world"]
", ".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:

numbers = [1, 2, 3, 4, 5]
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):

numbers = [1, 2, 3, 4, 5]
sum(numbers)  # Result: 15

Python - Reduce way (shows the pattern):

from functools import reduce
numbers = [1, 2, 3, 4, 5]
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

iex(42)> numbers = [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
iex(43)> Enum.sum(numbers)
15
iex(44)> Enum.reduce(numbers, fn x, acc -> x + acc end)
15

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):

numbers = [3, 1, 4, 1, 5, 9, 2]
max(numbers)  # Result: 9

Python - Reduce way (shows the pattern):

from functools import reduce
numbers = [3, 1, 4, 1, 5, 9, 2]
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

iex(45)> Enum.max(numbers)
5
iex(46)> Enum.reduce(numbers, fn x, acc -> if x > acc then x else acc end)
** (SyntaxError) invalid syntax found on iex:46:53:
    error: syntax error before: 'else'
iex(46)> Enum.reduce(numbers, fn x, acc -> if x > acc do  x else acc end)
** (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
    x
  else
    acc
  end
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
length([1, 2, 3])     # Really: Kernel.length/1
abs(-5)               # Really: Kernel.abs/1  
round(3.7)            # Really: Kernel.round/1
min(5, 3)             # Really: Kernel.min/2

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() → Elixir length() (auto-imported from Kernel)
  • Python max() → Elixir max() (auto-imported from Kernel)
  • Python abs() → Elixir abs() (auto-imported from Kernel)

Now let’s tackle the final reduce example - joining strings!

Python - Easy way (built-in method):

words = ["Hello", "beautiful", "world"]
", ".join(words)  # Result: "Hello, beautiful, world"

Python - Reduce way (shows the pattern):

from functools import reduce
words = ["Hello", "beautiful", "world"]
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.

iex(47)> words = ["Hello", "beautiful", "world"]
["Hello", "beautiful", "world"]
iex(51)> Enum.join(words, ", ")
"Hello, beautiful, world"
iex(52)> Enum.reduce(words, fn x, acc  -> acc + x end)
** (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.

iex(52)> Enum.reduce(words, fn x, acc  -> acc <>  x end)
"Hellobeautifulworld"
iex(53)> Enum.reduce(words, fn x, acc  -> acc <> "," <>  x end)
"Hello,beautiful,world"
iex(54)> Enum.reduce(words, fn x, acc  -> acc <> ", " <>  x end)
"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

words = ["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
# 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

iex(56)> words = ["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
iex(57)> words = Enum.map(words, fn x -> String.downcase(x) end)
["apple", "banana", "cherry", "date", "elderberry"]
iex(58)> words = Enum.filter(words, fn x -> String.length(x) > 4 end)
["apple", "banana", "cherry", "elderberry"]
iex(59)> length(words)
4

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

numbers = [1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
# 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.

iex(61)> numbers = [1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
[1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
iex(61)> numbers = Enum.filter(numbers, fn x -> x > 0 end)
[1, 3, 5, 7, 8, 10]
iex(62)> numbers = Enum.map(numbers, fn x -> x * x end)
[1, 9, 25, 49, 64, 100]
iex(63)> Enum.sum(numbers)
248

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

sentences = ["Hello world", "Elixir is fun", "I love programming", "Code every day"]
# 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.

iex(67)> sentences = Enum.map(sentences, fn x -> String.split(x) end)
[
  ["Hello", "world"],
  ["Elixir", "is", "fun"],
  ["I", "love", "programming"],
  ["Code", "every", "day"]
]
iex(73)> words = Enum.filter(words, fn x -> String.length(x) >= 4 end)
["Hello", "world", "Elixir", "love", "programming", "Code", "every"]
iex(74)> length(words)
7

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:

  1. Get the bread
  2. Toast the bread
  3. Add cheese
  4. Add lettuce
  5. 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():
    bread = get_bread()
    toasted = toast_bread(bread)
    with_cheese = add_cheese(toasted)
    with_lettuce = add_lettuce(with_cheese)
    final = add_mayo(with_lettuce)
    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
name |> String.upcase()

# 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()

name = "  alice johnson  "
result = clean_name(name)
print(result)  # "Alice Johnson"

Elixir with piping:

def clean_name(raw_name) do
  raw_name
  |> String.trim()
  |> String.capitalize()
end

name = "  alice johnson  "
result = clean_name(name)
IO.puts(result)  # "Alice Johnson"

3.6 More Complex Example: Building a Greeting

Python (multiple steps):

def create_greeting(name, title):
    cleaned = name.strip().title()
    formal = f"{title} {cleaned}"
    return f"Welcome, {formal}!"

greeting = create_greeting("  bob smith  ", "Dr.")
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:

value |> then(fn x -> # do something with x end)

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

greeting = create_greeting("  bob smith  ", "Dr.")
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 righttop to bottom
  • Each step transforms the data and passes it to the next step
  • Easy to addremove, or reorder steps

Benefits:

  1. Readable: Reads like step-by-step instructions
  2. Debuggable: Easy to inspect data at any point
  3. Modular: Easy to add/remove transformations
  4. No Nesting: No confusing inside-out reading
  5. 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.

Rememberdata |> 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):

words = ["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)
length(words)

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

iex(81)> ["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
["apple", "BANANA", "Cherry", "date", "ELDERBERRY"]
iex(82)> |> Enum.map(fn x -> String.downcase(x) end)
["apple", "banana", "cherry", "date", "elderberry"]
iex(83)> |> Enum.filter(fn x -> String.length(x) > 4 end)
["apple", "banana", "cherry", "elderberry"]
iex(84)> |> length()
4

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:

numbers = [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)
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.

iex(85)> [1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
[1, -2, 3, -4, 5, -6, 7, 8, -9, 10]
iex(86)> |> Enum.filter(fn x -> x > 0 end)
[1, 3, 5, 7, 8, 10]
iex(87)> |> Enum.map(fn x -> x * x end)
[1, 9, 25, 49, 64, 100]
iex(88)> |> Enum.sum
248

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:

sentences = ["Hello world", "Elixir is fun", "I love programming", "Code every day"]
sentences = Enum.map(sentences, fn x -> String.split(x) end)
words = List.flatten(sentences)
words = Enum.filter(words, fn x -> String.length(x) >= 4 end)
length(words)

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.

iex(89)> ["Hello world", "Elixir is fun", "I love programming", "Code every day"]
["Hello world", "Elixir is fun", "I love programming", "Code every day"]
iex(90)> |> Enum.map(&String.split/2)
** (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)
iex(90)> |> Enum.map(&String.split/1)
[
  ["Hello", "world"],
  ["Elixir", "is", "fun"],
  ["I", "love", "programming"],
  ["Code", "every", "day"]
]
iex(91)> |> List.flatten
["Hello", "world", "Elixir", "is", "fun", "I", "love", "programming", "Code",
 "every", "day"]
iex(92)> |> Enum.filter(&(String.length(&1) >=4))
["Hello", "world", "Elixir", "love", "programming", "Code", "every"]
iex(93)> |> length
7

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.

data |> function(args)  # Same as: function(data, args)

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
data |> String.upcase() |> String.reverse()

# Better without pipes - single function call
String.upcase(data)

# Good for pipes - multiple steps
numbers |> Enum.filter(&(&1 > 0)) |> Enum.sum()

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)
data |> String.upcase |> String.reverse

# Required for zero-argument functions
data |> String.trim() |> System.system_time()

# Required for clarity with multiple arguments  
data |> String.replace("old", "new")

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

list = [1, 2, 3]
new_list = [0 | list]        # Creates new list, original unchanged
# 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:

person = {"name": "Alice", "age": 30, "city": "New York"}
empty_dict = {}

Elixir:

person = %{"name" => "Alice", "age" => 30, "city" => "New York"}

# 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 :)
atom_map = %{name: "Alice", age: 30, city: "New York"}  # atom keys - cleaner syntax

# 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:

name = person["name"]           # Raises KeyError if key doesn't exist
age = person.get("age", 0)      # Returns default value if missing

Elixir:

# Returns nil if key doesn't exist. Thank God - no exceptions when keys are not found. It is a graceful nil. 
name = person["name"]           
age = Map.get(person, "age", 0) # Returns default value if missing

# Atom keys enable dot notation:
name = atom_map.name            # Raises KeyError if key doesn't exist
name = atom_map[:name]          # Returns nil if key doesn't exist

# 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:

person["email"] = "alice@example.com"  # Add new key
person["age"] = 31                     # Update existing key

Elixir:

# Returns new map (immutable!)
updated_person = Map.put(person, "email", "alice@example.com")  # Adds or updates key
updated_person = %{person | "age" => 31}  # Update syntax - key must exist or raises KeyError

# 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
person.pop("age", None)         # Returns value, with default

Elixir:

updated_person = Map.delete(person, "city")    # Returns new map
{value, updated_person} = Map.pop(person, "age", nil)  # Returns {value, new_map}

4.6 Getting All Keys/Values

Python:

keys = list(person.keys())
values = list(person.values())
items = list(person.items())    # [(key, value), ...]

Elixir:

keys = Map.keys(person)
values = Map.values(person)
# No direct equivalent to items() - use Enum.to_list/1 if needed

4.7 Merging Maps

Python:

defaults = {"theme": "dark", "notifications": True}
user_prefs = {"theme": "light", }
settings = {**defaults, **user_prefs}  # user_prefs overrides defaults

Elixir:

defaults = %{"theme" => "dark", "notifications" => true}
user_prefs = %{"theme" => "light", }
settings = Map.merge(defaults, user_prefs)  # same behaviour as python

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 element
  • Enum.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
employee.name  # or employee[:name]

# Create a new map
%{name: employee.name, salary: employee.salary}

# Update an existing map
%{employee | salary: new_salary}

# Chain operations
data |> filter(...) |> map(...) |> reduce(...)

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
employees |> Enum.map(fn x -> x.name end) 

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 -> 
  employee.department == "Engineering" 
end)

# 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 -> 
  total_experience + employee.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
point = (3, 4)
person = ("Alice", 30, "Engineer")
empty_tuple = ()
single_item = (42,)  # Note the comma!

# Accessing elements (by index)
x = point[0]  # 3
y = point[1]  # 4

Elixir:

# Creating tuples  
point = {3, 4}
person = {"Alice", 30, "Engineer"}
empty_tuple = {}
single_item = {42}  # No comma needed

# 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 
elem(person, 0 ) #> "Alice"

4.16 Key Difference: Usage Philosophy

Python tuples are often used like immutable lists:

coordinates = [(1, 2), (3, 4), (5, 6)]
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:
result = {:ok, "Success!"}
{status, message} = result

error = {:error, "Something went wrong"}
{: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 -> 
  elem(response, 0) == :ok 
end)

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

  1. Pattern Matching: Each clause tries to match the input against its pattern
  2. First Match Wins: Elixir uses the first clause that matches
  3. 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:

  1. Multiple Function Clauses: How to write fn pattern1 -> result1; pattern2 -> result2 end
  2. Pattern Matching: How {:ok, _} matches any 2-tuple starting with :ok
  3. Real-World Patterns: How Phoenix/Elixir commonly uses {:ok, data} and {:error, reason} tuples
  4. Destructuring: How to extract data directly in the pattern: {:ok, user} gives you access to user

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
config = {"host": "localhost", "port": 3000, "ssl": True}
html_attrs = {"class": "btn", "id": "submit", "disabled": True}

Elixir (keyword lists):

# Creating keyword lists - two syntaxes
config = [host: "localhost", port: 3000, ssl: true]
config = [{:host, "localhost"}, {:port, 3000}, {:ssl, true}]  # Explicit syntax

html_attrs = [class: "btn", id: "submit", disabled: true]

5.2 Accessing Values

Python:

host = config["host"]
port = config.get("port", 8080)  # With default

Elixir:

host = config[:host]              # Returns "localhost"
port = Keyword.get(config, :port, 8080)  # With default

5.3 Key Differences from Maps

Maps (unique keys):

map = %{name: "Alice", name: "Bob"}  # Second "name" overwrites first
# Result: %{name: "Bob"}

Keyword Lists (duplicate keys allowed):

keyword_list = [name: "Alice", age: 30, name: "Bob"]  # Duplicates OK!
# Result: [name: "Alice", age: 30, name: "Bob"]

# First match wins when accessing
keyword_list[:name]  # Returns "Alice" (first occurrence)

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
user_path(conn, :show, user, [format: :json, locale: "en"])

# Ecto queries
User |> where([u], u.active == true) |> limit(10)

HTML attributes:

# Phoenix templates
text_input(form, :name, [class: "form-control", placeholder: "Enter name"])

Configuration:

# Phoenix configuration
config :my_app, MyApp.Repo,
  username: "postgres",
  password: "secret", 
  database: "my_app_dev",
  hostname: "localhost"

Try these examples in iex:

# Create a keyword list for HTML attributes
attrs = [class: "btn btn-primary", id: "submit", disabled: false]

# Access values
attrs[:class]
Keyword.get(attrs, :type, "button")  # With default

# Add more attributes (duplicates allowed)
new_attrs = [class: "btn-large"] ++ 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 -> 
  config[:type] != :submit 
end)

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:

  1. case statements (handling those {:ok, result} tuples)
  2. with statements (chaining operations that might fail)
  3. 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 = User("Alice", "alice@example.com", 30)
print(user.name)  # "Alice"

Elixir maps (what we’ve been using):

user = %{name: "Alice", email: "alice@example.com", age: 30}
user.name  # "Alice"

Elixir structs:

# First, define the struct
defmodule User do
  defstruct [:name, :email, :age]
end

# Then create instances
user = %User{name: "Alice", email: "alice@example.com", age: 30}
user.name  # "Alice"

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
post = %BlogPost{
  title: "Learning Elixir", 
  content: "Elixir is amazing!",
  author: "Alice"
}
# published: false, views: 0 (defaults)

# Access fields (same as maps)
post.title  # "Learning Elixir"
post.published  # false

# Update fields (same as maps)
published_post = %{post | published: true, views: 100}

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
greet_user(%User{name: "Alice"})  # "Hello, Alice!"
greet_user(%{name: "Bob"})        # "Hello, stranger!"

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
  
  schema "users" do
    field :name, :string
    field :email, :string
    field :age, :integer
    
    timestamps()
  end
end

# Creates a struct like:
# %MyApp.User{id: nil, name: nil, email: nil, age: nil, ...}

Changeset (for form validation):

changeset = %Ecto.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
task1 = %Task{title: "Learn Elixir", description: "Study structs"}
task2 = %Task{title: "Build Phoenix app", description: "Create a todo app", priority: :high}

# Update a task
completed_task = %{task1 | completed: true}

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

post = %BlogPost{title: "Hello", content: "World"}

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
  age < 13 -> "child"
  age < 20 -> "teenager"  
  age < 65 -> "adult"
  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

with {:ok, user} <- get_user(id),
     {: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:

with {:ok, user} <- Users.create(params),
     {:ok, _email} <- send_welcome_email(user) do
  {:ok, user}
else
  {:error, changeset} -> {:error, changeset}
  error -> {:error, "Failed to create user"}
end

8.5 Learning Order

  1. case - Most important, handles the {:ok, result} patterns
  2. with - Essential for Phoenix request pipelines
  3. cond - Simple but useful
  4. if/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
  pattern1 -> result1
  pattern2 -> result2
  pattern3 -> result3
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"
    code -> "Unknown status: #{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 
    api_response[0] == "ok"):
    user_data = api_response[1]
    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 
      api_response[0] == "error"):
    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} -> 
    render_admin_dashboard(user)
  
  {:ok, user} -> 
    render_user_dashboard(user)
  
  {:error, :invalid_token} -> 
    redirect_to_login()
  
  {:error, :expired} -> 
    show_token_expired_page()
end

9.5 Try It Yourself

Try these examples in iex:

# Simple number matching
age = 25
case age do
  x when x < 13 -> "child"
  x when x < 20 -> "teenager"
  x when x < 65 -> "adult"
  _ -> "senior"
end

# Tuple pattern matching
result = {:ok, "Success!"}
case result do
  {:ok, message} -> "Great: #{message}"
  {:error, reason} -> "Failed: #{reason}"
end

What do you get when you run these?