Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Three Ways to Debug Code in Elixir (appsignal.com)
136 points by clessg on Dec 4, 2021 | hide | past | favorite | 27 comments


After seeing Elixir pop up so many times on Hacker News these last few months, I deicded to give it a go for the Advent of Code.

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!


My 2 cents

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.


I understand, but a lot of my refactoring is within functions to simplify and improve legibility once they work, renaming variables with generic names from "result" or "data".

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!


Ah, yes, this is how I refactor in Elixir too. I feel icky about it from a DRY standpoint but I’m training myself to ignore the scolds that held me back from being productive for so long.


> I deicded to give it a go for the Advent of Code.

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


Yea, my experience has been that after getting deep with Elixir and understanding how and why it (and the BEAM) works the way it does, everything else feels very limiting. Like I know all of the problems I’m willingly setting myself up for long term.


> It's been pretty amazing so far

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


I want to try, I've been doing AoC in Python, but that's only so I can improve my problem solving skills. I want to do it in Elixir, among other languages but I have never written or read a line of elixir in my life. How did you begin? Did you start learning elixir side by side AoC or did you go through a tutorial or book _before_ December?


That's great! I very briefly went through the introductory docs, but otherwise I just dove right in. The language is really simple, the tough part is wrapping your head about immutability.

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!

[1]: https://github.com/Bogdanp/awesome-advent-of-code

[2]: https://github.com/MarcusCemes/advent-of-code-2021


for anyone using Elixir + vscode, this snippet is invaluable:

    {
     "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)


Here's an emacs yas-snippet that does the same thing (and was inspired by yours)

    # -*- 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)}")


I have almost the same one in my elixir.json when I use VS Code! I'm pretty sure your YT channel was the inspiration, and I've used that snippet a ton over the past year or two.

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


I use Keyboard Maestro for this. "iins" for IO.inspect(), "ppry" for require IEx; IEx.pry, and ">>" for |>. This way I can use these shortcuts in IEx, emacs, vscode, LiveBook, etc.


Recon trace is a worthwhile addition. I've used it for some hairy production bugs that don't reproduce locally. Allows you to unobtrusively instrument a running system and echo back function calls with their params, including process targeting if needed.

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.

https://ferd.github.io/recon/recon_trace.html


Yep, and here’s a nice intro video from this year’s ElixirConf:

https://m.youtube.com/watch?v=F7YtYTMud-Y


It can’t be emphasized enough how good IEx is.

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.


I will never forget the first time I started IEx from within a Phoenix project, and found that I had a live shell into the running application, while the Phoenix development server was running and reachable from the browser, all with hot code reloading... No need to create a dummy REST endpoint to try running a function.

My mind was blown.


Another thing worth trying, is connecting a Livebook instance (see: https://livebook.dev/) to a running application. Keep in mind Livebook allows for multiple users to work on the same notebook at once so this can come handy in pair-programming / debugging situations.


Give a remsh connection to a production cluster, you’ll find IEx really does mean “living inside” the process—you can reach out and add a live trace hook to e.g. a Phoenix Controller function, and then immediately see traces from your live prod users hitting that trace point. Without recompiling or restarting the app—their existing in-memory session state, local in-memory caches, etc. are suddenly exposed to you, without needing to discard any of it via a redeploy/restart and then wait for new user sessions to reproduce the problem.

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


It's like you are in bash, except you can run real functions, and your variables aren't stringly typed.


:observer.start is awesome too


I recently wrote a port of the python IceCream project[https://github.com/gruns/icecream].

Feel free to check it out for easier IO.inspect ergonomics:

https://github.com/joseph-lozano/ice_cream


Almost any time I'm running any Elixir project locally, I run it through IEx. You can do that by running "iex -S mix <app>" instead of just "mix <app>".

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.

https://alchemist.camp/episodes/iex-exs


I don't think I've ever used `IO.puts` for anything. As the article notes, `IO.inspect` is far superior.

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.


The idea that you have to 'ini' modules every time you rerun your app, and that setting breakpoints in the visual debugger does not always work has always baffled me.

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.


I'm working on a static typechecker for elixir. The plan is to have it eventually take advantage of elixir compiler hooks. Hopefully it's better than dialyzer (in theory it should; It will have subtractive types, and a broader set of literals, e.g string literals).... But I am taking a "scientific" approach - designing a type system based on discoveries I am making about how the BEAM works under the hood.


That's great, is there any public repo for this?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: