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:

5 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.

2 Likes

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.

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

6 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.

4 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.

9 Likes

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

1 Like

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}
2 Likes

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.

4 Likes

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.

2 Likes

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.

5 Likes

Finished Chapter 6 adn completed the conversion of the TodoList abstraction to a GenServer implementation.

During the conversion I found that I was cautious in relying on the difference in arity and privatisation between the implementation and interface functions. Is such dependence alright to permit as standard practice for such conversions, or is it better to use different names to make refactoring later more straightforward?

I

interface func head:

def add_entry(todo_list, entry)

Implementation fun head

defp add_entry(entry)

The interface calls GenServer.cast(…) which then calls the relevant handle_cast of the callback module. Instead of placing the logic for generating the new state directly in the handle_cast I just forwarded it to the implementation function, as this seems like a good compartmentalisation approach.

I really enjoyed chapter 6, I appreciated the way we are walked through developing are own less feature rich GenServer and how this tour teaches us some of the rational behind GenServers.

The way this book progresses is brilliant, it is obvious a great deal of attention was paid to the flow and ordering of the book.

Chapter six was very demystifying, I’ve marked it for a more attentive reread down the line BGG.

4 Likes

Chapter 7, when there is an amendment to update the todo_server so that a given todo_server knows its own name,there needs to be an amendment to the cache_server’s handle_get, as the Todo.Server.start needs to take a name/key arg when fetch fails. I

t did not need to take such an arg before.

Apologies if this is already shown in the chapter, and I missed it.

This refers to Todo.Cache, not the changes to Todo.Server.

2 Likes

Finished Chapter 7. The chapter is brilliant imo,

Chapter 7 explores GenServers, working with simple state persistence external to a stateful server, and the various ways in which performance as well as availability and consistency (I believe used in the axiomatic sense) can be jeopardised via:

Requests outpacing processing: Synchronous (via long running executions) and asynchronous (via unbound concurrency) risk
Caller Blocking: Synchronous risk
Long-running executions: Synchronous risk, GenSever timeout

The book looks at various solutions such as reply (and thus code execution) deferment and
pooling.

A very effective way to get familiar with GenServers, necessary considerations and some great associated design practices.

Each new chapter has been better than the last, covering a serious topic concisely and effectively, as well as, providing practice challenges that are relevant to the material and represent an opportunity to obtain a working understanding of the chapter…

3 Likes

Very late to the party here but just read through Chapter 1, I find the value proposition stated here super compelling. For me, I used to work on this complicated multiservice architecture in Kotlin and I remember thinking to myself at the time that we really should have a language that bakes in multiservice concepts. That language seems to be Elixir to me.

Another thing I find compelling is that the unique package of benefits that Elixir offers is perfectly for producing a reasonably scalable application with less effort than something like Ruby on Rails.

4 Likes

Sorry for the long gap, lots had happened.

Anyways, I’m back with Chapter 4.

This was a pretty good chapter especially for folks are new to Elixir. Most of the discussions are what you’d expect from a chapter (in any language) about abstraction.

One thing I did, and I would encourage folks who are trying this book for the first time to do, is, please try out all the codes, and the exercise. Coming from OOP land, I used to not like not being able to do a my_list.append(0, 1) and instead think with List.append(my_list, 0, 1). Didn’t take more than two modules for me to appreciate this. This way pairs with immutability perfectly, and also makes the code very well navigable. This is also true when composing modules, trying out the awesome Todo list example and adding slight variations or features would offer “A-ha moments” much faster.

Protocols formed the final seciton of chapter 4. It’s a very nice abstraction artifact when implemented right (no pun intended). I don’t recall ever having to create my own protocol, but I sure defimpl-ed a lot of those!

Modules, functions and forming abstractions out of the two takes up the crux of this chapter. And a lot about Elixir syntax gets cleared upon reading this. And rightfully this is the chapter that “soft” ends sequential Elixir and paves way for Concurrent Elixir.

3 Likes

I also finished chapter 5. But I will go up and catch up with the review of y’all and see if my learning gets a free upgrade :pray:

I will be back with my feedback later in the day. I feel this thread is destined to become an amazing companion to this book!

4 Likes

Time for some recaps, again.

Worked through chapters 6 and 7.

Chapter 6 is about GenServers which I allready had read about and what you will stumple upon in Elixir land on many occassions. The construction of the GenServer implementation proceeding from the Server Processes of the previous chapters helped me a lot to understand what is going on in GenServer, where only has been a clue before.

Chapter 7 introduces Mix, Elixir’s project management tool, which seems like a combination of Bundler, Gem, and Rake for people being used to Ruby. Very intuitive and going smooth so far. There is also an introduction to testing and persisting data.
The pros and cons of different process architectures are broached to get a feeling, which kind of requirements should be designed in what manner to avoid bottlenecks or errrors. The tasks given help to internalize the learned and to check, if everything is understood correctly. They helped me a lot and I can recommend to write your own code from the beginning, instead of just using Saša’s solutions. So, you can extend your code from chapter to chapter.

3 Likes