Elixir In Action Book Club!

Chapter 4 done :white_check_mark:

Another great introductory chapter to Elixir. The more I read through the chapters, the more I wish I had read and found the first edition of the book back when I was getting started with Elixir.

I like that MapSets are introduced here, as they’re something that might otherwise be easy to forget, and the unique benefits of MapSets can sometimes help solve certain problems very simply and easily. Similarly, I appreciate that even records gained a mention, even though they’re such a rarely seen (for me!) tool in the Elixir toolbox. Generally, it’s great that these things are brought up at least in passing so that one knows what’s available, even if they were rarely the chosen tool.

I also really appreciated the explanation of how to achieve polymorphism with protocols, as I recall it was one of the more difficult topics to grok back when I first got acquainted with Elixir. Saša does a great way of explaining them in a way that’s easy to understand, and highlights their unique value proposition very well.

How about others, how was your experience through chapter 4? :eyes:

3 Likes

I think a major reason to separate the protocol implementation into its own file would be to minimize recompilation of other files that depend on the file that defines the actual module, so you can make independent changes to the protocol definition.
https://hexdocs.pm/mix/Mix.Tasks.Xref.html#module-dependency-types

3 Likes

OK, I had been playing catch-up with the general group, and had decided to make a write-up once I was caught up.

To give a little bit of my context: I have been using Elixir for the past 8 months coming from a mostly Python + JS background, therefore my ramp up has been as much about learning the language, as to how to write (mostly) functional programs.

To begin learning about Elixir I went through the official documentation and the first 4 Chapters of the 2nd edition of this book. On both accounts I was able to retain the basic elements, especially as I started putting them in practice on simple Phoenix apps.

So coming back to this book has been great, as I can move on from the basics to more intricate elements of Elixir/BEAM.

I will focus on those points that have stood out for me and I wish to fully keep in my mind as I work:

Chapter 2

  • Aliases are always atoms (this really clarified some function calls with module references)
  • The distinction of when to use the different access functions for Maps:
    • %{map | update_key: update_value} when keys are not dynamic, Map.put/3 when they are
    • map.key when the keys are atoms and not dynamic
    • map[key] along with Map.get/3 when the keys are dynamic (or the keys are not atoms)
    • finally Map.fetch/2 when they are dynamic and you want more certainty on whether the key existed or not
  • NOTE On Strings I would have added how to use the <> operator to split a very long string into multiple lines (without inserting the newline char… for the longest time I was constantly forgetting this and googling it :face_in_clouds: )
  • IO lists are super interesting… and I have the feeling at some point in my Elixir life they will be relevant (just not sure when or why :laughing:)

Chapter 3

  • Even binding variables to values is pattern matching (just a special case of it)
  • On an if statement if there is no else branch and the main branch is not executed then nil is returned
  • I had completely forgotten about cond branching
  • I had completely forgotten about tail recursion
  • The capture & operator can be used to reference an existing function - via module name, function name, arity - or to shorten a lambda definition

Chapter 4

  • When the time comes to implement protocols I should remember that there is an example here that I can reference

Chapter 5

  • BEAM processes are completely isolated and independent from each other, which means they keep their own copy of their context (so any data passed to them will be deep copied)
  • A process’ mailbox will operate on FIFO
  • A receive expression is blocking unless there is an after clause
  • In general be aware that lambda parameters may not be executed where they are declared! The example of using self() within lambdas was amazing to illustrate this point!

OK, this is all for now.

P.S. I’m already curious how this list will change the third time I go through this book :joy:

4 Likes

Chapter 4 completed.

I enjoyed working through the code and exercises, but have only this note.

Exercise: Deleting an entry

When I first implemented this, I replicated the Map.fetch pattern from update_entry, which validates existence of the id. Then, I looked at the example code which simply calls Map.delete without that check. That generates an error if the id does not exist. Of course, that version would also save time on larger collections. It had me curious as to when I may want to choose one pattern over another, choosing safety vs time savings.

1 Like

Chapter 5 completed. Nice chapter - not too long :slight_smile:

It’s amazing that with just spawn/1, send/2 and receive/1 and little more the amount that can be achieved!

I particularly enjoyed the part about the inner workings of the Scheduler, and the example of the infinite CPU bound loop which was proven not to block other jobs - it reminded me of the excellent talk from Saša from a few years ago…
The Soul of Erlang & Elixir

Looking forward to formalising all this a bit more with GenServer.

3 Likes

Chapter 5, check :white_check_mark: !

Some points that I found to be excellent reminders about BEAM’s concurrency:

  • Data is usually deep copied between processes. This has implications for memory usage considerations and garbage collection simplicity (per-process).
  • Data is not always deep copied, but instead can sometimes be copied by reference (binaries >64 bytes, literals, :persistent_term). This can have implications for GC as well (e.g. changes in :persistent_terms lead to a heavy GC pass)
  • The execution window within a scheduler before preemption is around 2000 function calls
  • Dirty schedulers are kinds of schedulers that can handle long-running CPU-bound tasks (see: dirty NIFs, dirty BIFs, dirty GC)
  • Process mailboxes are only limited by available memory (!)

All in all, I loved how this chapter dug deeper into the specifics of BEAM:s concurrency primitives and their use, such as by highlighting things like Erlang emulator flags. These are the things that one doesn’t typically need for day-to-day operations when working with Elixir, but are still very valuable pieces of information know about on occasion. Love the balance of detail with practicality!

How about others – how’s your Elixir In Action adventure going? :slightly_smiling_face:

3 Likes

I have completed chapter 5 also. I didn’t make any notes this time, though I noticed many mentions of ‘later chapters’ to cover more details amount different topics. I’m looking forward to getting there to see what they have to say. On a tangent, System.schedulers() reported 12 for my M2 Max MacBook Pro. This matches my 12 cores (8 performance and 4 efficiency). I’m not sure if/how I could specify to use only one type vs the other.

2 Likes

I have also completed chapter 5.

My only note this time is a suggestion for this part:

Note that this isn’t efficient; you’re using Enum.at/2 to choose a random PID. Because you use a list to keep the processes, and a random lookup is an O(n) operation, selecting a random worker isn’t very performant. You could do better if you used a map with process indexes as keys and PIDs as values. There are also several alternative approaches, such as using a round-robin approach. But for now, let’s stick with this simple implementation.

A simple and efficient way to randomly select a pid is to convert the list of pids to a tuple, and then use elem/2 to extract a random element:

            server_pid = elem(tuple_pool, :rand.uniform(100) - 1)

This approach is simpler and should be more efficient than using a map. If the example would be changed to use this approach, the example would still be fairly simple and the note about efficiency would not be needed.

6 Likes

Is the cost of converting to a tuple ever high enough where this wouldn’t be the case?

I believe Map.delete returns the map passed as an argument, should there be no ID found.

iex(1)> h Map.delete

  def delete(map, key)

  @spec delete(map(), key()) :: map()

Deletes the entry in map for a specific key.

If the key does not exist, returns map unchanged.

Inlined by the compiler.

## Examples

    iex> Map.delete(%{a: 1, b: 2}, :a)
    %{b: 2}
    iex> Map.delete(%{b: 2}, :a)
    %{b: 2}

I did not consider the initial cost of setting up the pool, because that cost should be irrelevant because the pool is presumably used for many requests. Still, I would expect that creating a tuple should be faster than creating a map.

The reason for my “should” here and in my previous post is that I am generally careful when talking about performance based on my knowledge of the implementation alone (as opposed to having run benchmarks or performed some other measurements). Code that “should” be faster based on how it is implemented is not always faster in practice.

1 Like

Reading Chapter 5:

It’s really a great book, as expected from Saša Jurić, given his other materials!

I would just note one thing… (this is not really to do with the content of the book, but the interface for readers)

It would be really great if I could pop-up the function definition for a function being referenced on one page, but that was defined a few pages back.

I don’t know if it’s to do with not getting enough sleep but I keep forgetting the function definitions and I have to scroll back a few pages. Not just once but multiple times on one section. The thing is, the section is concise, and clearly explained, but I’m scrolling back due to not remembering the functions.

This is not something that takes away from the book. I make this suggestion as what I think would be a cool, sprinkles-on-top, feature for future books, or even re-releases.

I’ve opted to copy the functions to a separate document and review them as and when.

I had some pause after chapter 2, but managed to work my way to the end of chapter 5, now.

First of all, I also want to underscore what an enrichment it is, to have @bjorng here. His insights about the BEAM are a terrific addendum to @sasajuric’s book.Thank you, Björn, to let us participate!

My resume of chapters 3 to 5:

Saša gives a fine declaration of pattern matching, multiclause functions and guards in chapter 3. The control structures are well explained and the examples are very helpful. The explanation of the “when” clause is also very good. I think for many people, who are coming from imperative languages, Saša does a good job in explaining the functional way of implementing structures. I also appreciate the part about Streams.

Chapter 4 is about data abstractions. It gives a good introduction on how to organize the code.

In chapter 5, the power of parallelism in Elixir begins to get tangible for me and some of the open questions, I had from chapter 1, are finally answered. This chapter was fun to read and work through, as I get some hunch about the benefits of the BEAM here.

I am looking forward to the next chapters.

3 Likes