Three Ways to Debug Code in Elixir
blog.appsignal.com
blog.appsignal.com
It's been pretty amazing actually, it takes a little getting used to, but it really feels like a language that was designed in every way possible to be ergonomic and productive. I'm still not sure whether it's easier to read purely functional code over a few mutable variables and simple loops, especially with a lot of filter, reduce, scan, zip, etc.
It's been pretty amazing so far, I haven't gotten further than IO.inspect()ing yet. The language server is still leagues even behind something like Rust analyzer (even renaming symbols is still a work in progress), but it's amazing that such a language already has one of the leading web frameworks (Phoenix), and despite being very network orientated, it's also excellent at solving AoC problems that involve a lot of clever manipulation of strings, binary, integers, and the sorts!
I've been an Elixir developer for years now, loved it immediately and still loving it after all this time.
About refactoring: I've been working as a Java developer for the past couple of years and I must admit thaat Intellij does a pretty impressive job at helping you refactoring code and their code sense is top notch.
But, there's a but.
I've never found myself having to make nuclear refactorings in Elixir, not because I don't refactor my code, but because modules are self contained and not tied with anything else, most of the times it is faster to abstract something over what you already have than to refactor all the things.
For example, if you decide that you need a struct instead of the old map, you advd a new function that pattern matches on the struct and pass it down.
If the struct has a different structure, you process it and pass it down.
The "treat data as something that is constantly transformed" mentality can take you a long way.
When I can code in java in a way similar to what I do in Elixir, things get easier in Java as well.
I miss symbol renaming in Elixir too, but it's never been a big stopper in the 6 years I've used it as a primary language.
edit: biggest strength of Elixir is Jose Valim himself. If you watch his videos you can see him finding a bug while doing something, actually explaining why that's a bug in Elixir (or some library) open an issue and ask for PRs
All while showing you complex software patterns that I though I could never face before using Elixir.
It's a lot faster to do a cheeky F2, knowing that it's not a blind find/replace but a static code analysis with the ability to understand variable shadowing. Definitely not a show stopper though, and it feels more old-school, where every letter you type is more meaningful, if that makes sense.
And a huge +1 for José Valim and the whole community behind him!
That reminds me: in case anyone is curious how that looks in practice, it just so happens the creator of Elixir (José Valim) has been streaming Advent of Code exercises using Elixir (and Livebook): https://www.twitch.tv/josevalim
Wait till you learn about async tests that are pinned to single database checkouts. And -- how async is composable with respect to resource pinning. AFAICT no other language does this (not even erlang).
There's an awesome repo with examples in pretty much every language [1], I go there afterwards to learn how other people did it. It's amazing to see so many different ways of approaching the problem. My solutions are also public [2], but bear in my I'm still only a few days in to learning Elixir!
If you do want to try attacking AoC with Elixir, I suggest you research and understand these concepts, it's more than enough to get you going:
- Pattern matching. The "=" sign is _not_ an assignement, it's trying to fill the shape of the variables on the left with the stuff on the right.
- The pipe operator "|>", the return value of the previous function is implicitly as the first paramter of the next function, it makes it easy to build chains.
- There is no "return". Use small functions with `case` statements, or function overloading. The last value in a function is implicitly the return value, like in Rust.
- Enum.map(), filter(), reduce(), zip(), chunk(), these functions largely get you through the manipulation of data. The first three are very popular in JS.
- Immutability. There's no mutating data, hence why you need reduce() for example.
- There are no loops, instead, get used to doing recursion. hd(), tl() are useful for this.
- Anonymous functions `fn a, b -> a + b end`, there is also a shorthand form `&(&1 + &2)`, and you can also pass functions by reference `&sum/2`
- The "/1", "/2", ... after a function is the arity, it's not scary, it just means the number of paramters that it takes.
- Function overloading is amazing, it's like pattern matching. If you provide constants in the function declaration, it will only be called if those constants match:
```
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
```
I find the above really elegant. It's correct, the special case for "0" is very explicit, and if you try a negative number, Elixir will simply be unable to match the given problem to a function definition (n < 0), which makes more sense than throwing "BadArgument" errors/exceptions.
If you really want to just solve problems, I advise sticking with Python, there's nothing wrong with it. Most languages also provide a lot of functional concepts, I'm sure Python is no exception (with some libraries). Be prepared that functional languages require you to think differently, but it is really fun to do things in a functional way!
Good luck!
{
"Inspect": {
"prefix": "ins",
"body": "|> IO.inspect(label: \"$0$TM_LINE_NUMBER\")",
"description": "Adds a pipeline with a labelled `IO.inspect`",
}
}
when you type ins<tab> it injects a labelled IO.inspect that makes it easy to track where the data came from; usually in a debugging session the line numbers will be sufficient to track, and more importantly, it's almost always easy to ninja out these IO.inspects in one shot, because they are almost always the same number of characters long (since contiguous line numbers are usually in the same decade). Credit goes to one of my former junior devs who built this.video demo: https://www.youtube.com/watch?v=JXQZhyPK3Zw&t=1410s (note slickbits doesn't seem to exist anymore)
# -*- mode: snippet -*-
# name: Piped Inspect
# key: pin
# --
|> IO.inspect(label: "$2$1 (`(buffer-name)`:`(line-number-at-pos))`)")
I also like to use a non-piped version: # -*- mode: snippet -*-
# name: Labeled Inspect
# key: lin
# --
IO.inspect($1, label: "$2$1 (`(buffer-name)`:`(line-number-at-pos))`)")
And occasionally a logger-based one: # -*- mode: snippet -*-
# name: Logger Inspect
# key: logi
# --
Logger.info("$2$1: #{inspect($1)}") "Inspect": {
"prefix": "ins",
"body": "|> IO.inspect(label: \"${1:$TM_LINE_NUMBER}\")$0",
"description": "Adds a pipeline with a labelled `IO.inspect`",
}
The only difference is that my version above makes the line number into a tab stop section so it's quick to swap out with another label.There are a number of powerful tools for BEAM runtime tracing. I prefer this one because the API is simple enough to commit to memory, and it protects against some common foot guns.
Maybe it’s because it is my first experience with a language like Elixir, but man, it just feels like I’m living inside the program while it’s running in a way that an IDE debugger doesn’t. It’s so cool and so powerful.
My mind was blown.
(You can even redefine the module in-place in memory to see how changes affect what you see. Brings a whole new meaning to “hotfix”: you can do hotfixes that don’t even exist on disk!)
Feel free to check it out for easier IO.inspect ergonomics:
I like REPLs in general, but IEx is particularly handy for poking around at a program. With a .iex.exs file you can automatically run aliases, imports and other helpers every time you start the REPL. These can be set up both globally and per-project.
Also, the Erlang observer is super neat. One really cool thing you can do with it is attach it to a remote machine, which comes in real handy.
There is also a weird sort of Stockholm syndrome (appropriate for a derivative of a swedish language, I suppose ?) where people find great reasons to explain why you don't really need a "fancy" debugger (as in, visual studio from 1998.)
Which is weird because it would have been useful to me about once per day of elixir programming in the last five years. But then, so would have a half-decent static type checker, and it's not like it's coming any time soon. So IO.inspect it is.
Which is okay, I guess.