Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Elixir Anti-Patterns (hexdocs.pm)
66 points by lobo_tuerto on June 28, 2024 | hide | past | favorite | 36 comments


> Complex else clauses in with

This has proven controversial, but sometimes the Else behavior is very sensitive to which of the With expressions failed (especially for better logging) and I maintain that the least terrible approach involves augmenting the With clauses using atoms to identify each expression when it fails, ex:

For example:

    with {_, {:ok, key} } <- {:phase_key,    get_key()         },
         {_, {:ok, door}} <- {:phase_unlock, unlock(door)      },
         {_, %Thingy{}  } <- {:phase_thingy, fetch_thingy(door)},
         {_, %Widget{}  } <- {:phase_widget, fetch_widget(door)} do
    else
         {:phase_key,    {:error, _msg} = err} -> # I can tell the key was missing, rather than the lock broke
         {:phase_unlock, {:error, _msg} = err} -> # I can tell the lock broke, rather than missing a key
         {:phase_thingy, nil                 } -> # I can tell this nil value is a missing thingy, not a widget
         {:phase_widget, nil                 } -> # I can tell this nil value is a missing widget, not a thingy
    end
As a big bonus, when something crazy-unexpected is emitted to cause a WithClauseError--like "hello world"--you have a much better chance of quickly diagnosing what happened from log messages, instead of inspecting a dozen different logical branching or trying to blindly repro it.

Pre-buttals:

"Just rewrite those 4 functions to return better somehow" -> No, that's not always practical or even possible. In the above example, the signatures of some of these functions are already perfectly fine on their own. The ambiguity comes when they get used near others in different ways, and trying to assign globally unique features to match on is a bad idea.

"Create 4 private functions to wrap things" -> Ugh, now the person reading the code has an even harder time, since they need to refer to functions elsewhere in the file, any of which might be hiding unexpected bonus behavior. Those 4 private functions might not even be useful in other with-statements in the same module, causing you to make even more private functions... and all of it still falls apart the moment your Else logging/cleanup needs to record or revert something from a prior (successful) step.


When using `with` and the `{:ok, term} | :error` patterns in Elixir, I just wish we had first class monads.

There are some third-party libraries to make working with these a little easier, but it never sat right with me. Probably being so close to the BEAM means we cannot stray very far from what Erlang is doing, but errors would be much nicer to handle with monads.

That said, pattern matching alleviates a lot of pain in many cases. If you have a pipeline of fallible steps, you can do:

    def step1({:ok, data}), do: ...
    def step1(:error), do: :error

    def step2({:ok, data), do: ...
    def step2(:error), do: :error

    etc.
I think the `with/else` anti-pattern is that your example just feels clunky because of this lack of monads. It is an anti-pattern not because it's a bad idea, but because it is simply unergonomic and just looks bad.

In general, whenever I feel the need to differentiate errors to handle them separately like your example, with a little refactor I can usually simplify the `with` expression and avoid having to do all that ceremony.


Gleam has an interesting approach with the USE statement. It looks better than with, but I haven't tried to write code with it yet


I think with is ok and actually kind of elegant if you don't care too much about the actual error.

To be honest I would prefer a return keyword and using early returns as in practice I'm not a fan of "the last statement is the return".


I find your snippet surprisingly readable. Thanks for the suggestion!


Oh yeah, the with statement was the ugliest part of elixir and also required unfortunately


Anti-pattern: Overuse and misuse of macros. (Or perhaps not a "pattern", since the manifestations are so varied?)

For non-Elixir folks, imagine something that looks like a regular function-call where expressions are evaluated and the results are passed in... but in reality all arguments are symbolic inputs to something that emits new code.

For example, you might see this unassuming line in your editor:

    describe_combo(a(foo),b(bar))
But what might actually be compiled is:

    # What might actually get compiled
    # Runtime results like "a(1)+b(2)=3"
    "a(" <> to_string(foo) <> ")+b(" <> to_string(bar) <> ")=" to_string(a(foo)+b(bar))
And if someone in your codebase is an evil misanthrope:

    foo = "haha I changed your local variable"
    IO.puts("surprise, suckers") # Prints to console
    kick_puppies()
Macros are actually part of the core DNA of the language and underpin how some of its syntax works... but with great power comes great potential for WTF.


Elixir macros are hygienic, so you need to be more explicit if you want to override variables as above. You also need to require macros before using them, but once you do you can kick all the puppies!

There is also a guide on meta-programming anti patterns: https://hexdocs.pm/elixir/macro-anti-patterns.html (and unnecessary use is one of them).


Elixir is one language I REALLY want to use as the solo developer working on internal infrastructure tools; but I'm very hesitant as I don't want to be cruel to the person who eventually replaces me and has to maintain these applications written in this seemingly (awesome) niche language.

Instead, I'm stuck writing applications in Go which is a language I absolutely despise but can't deny it's ability to get shit done regardless of the insane amount of boilerplate code I have to write.

Any predictions on whether Elixir will eventually gain more traction? The language and platform are rock solid and it's the best developer experience I've encountered. Phoenix is by far my favorite web framework and just feels very intuitive and complete. The lack of a good IDE is one huge pain point that I can't seem to overcome (I don't care how much people praise it, I know it's the best but VSCode is awful for Elixir development).

I just wish this language was more popular because it deserves to be!


I assure you that the Go evangelists before you(in general if not here specifically) had no such compunction about imposing that language on whoever was unlucky enough to inherit it.


Extremely well said. There are reasons gigantic corporations like Microsoft and Google spend all that money convincing you to use their new creation.


True, but you kind of get to introduce what you want as long as it's popular and well backed. Even if I know a tool or language is better suited for a task, I always prioritize my employer's ability to maintain that project over time.

That usually means using similar technologies, or otherwise popular, and well backed technologies. Go has that in abundance over Elixir.

For any personal project I'd take Elixir the vast majority of the time.


What I mean is that at some point Go was Rob Pike’s side project inside of Google and even Google wasn’t really using it. Back then it was an even more painful language to use. People outside of Google who latched on were imposing that choice on a lot of future maintainers.


As already mentioned: it will only gain traction if/when less conservative developers and managers decide to use it. If everyone plays it safe, then no: it doesn't stand a chance because Java, Go, JavaScript, Python (etc) already exist.

But I would argue the decision to adopt Elixir isn't really as simple as a "this language" or "that language". The most essential decision, I would suggest, is about Erlang... not so much the language as much as the virtual machine and OTP. If the problem you want to solve is well solved by the BEAM/OTP, then you start to look at the languages you might use with the BEAM/OTP. I think BEAM/OTP is underappreciated for many use cases and that are worth exploring; this broadens if you consider BEAM/OTP augmented with more performant programs built with a language like Rust/Zig/C/C++.

But if everyone plays the conservative game and doesn't see the risk/reward payoff with BEAM/OTP favorably... then it will be the same old boring players with only the ocassional influence from the sidelines.


Any predictions on whether Elixir will eventually gain more traction?

I have 10 years with the language this year, much of it as a professional fulltime focus in senior IC roles.

I would be shocked to my toes if there was ever going to be even 10 times more active Elixir devs than there are today, let alone more extreme growth. And I think that hypothetical plateau is an appropriate outcome commensurate with its ecosystem's breadth, applicability and quality today.

Many libraries have single or absentee authors, even some you would assume were critical to the community, and major vendors in many verticals don't give us any thought when providing SDKs/integrations. Elixir seemingly doesn't have any major backing or adoption from large polyglot orgs or top FortuneN groups. We have failed to reach critical mass for broad mindshare, IMO.

I have no personal plans to write net-new Elixir that I wasn't explicitly hired to work on in that specific stack.


Maybe more "traction" is not that important, in the end?

BEAM has been around for decades and has proven its worth. Elixir has been around for quite some time too, is super stable and reliable, has a pretty decent user base, real-world applications, ecosystem, etc. In many cases, it's arguably the best tool for the job. And it's not like some critical piece of puzzle is missing.

So it may stay kinda niche forever because people who fully understand the problem and fully realize how to solve it will also always be niche.


Part of this story is a self fulfilling prophecy. If devs like you avoid using Elixir because you’re afraid it isn’t popular enough, then it isn’t going to become more popular!


Why wouldn't you feel bad for the person having to write Go after you? Makes no sense to me


> Any predictions on whether Elixir will eventually gain more traction?

I don’t know about Elixir, but Gleam looks promising. Even less IDE support than Elixir right now though.


I know a BUNCH of elixir devs that would love to pick up an Elixir project that needs a new lead....just saying. There's plenty of talent looking at the moment.


I'm hiring for this, Fintech project, 100% Elixir + Phoenix + LiveView. Email at bio


Jose Valim gave an overview of this in last month's ElixirEU Conf https://www.youtube.com/watch?v=agkXUp0hCW8


Unpopular opinion given all the craze around Elixir these days. I find the syntax extremely verbose and hard to read compared to e.g. Python. I was told great things about it by engineers I really respect. But each time I start reading Elixir code bases to get a good look at what an actual Elixir project looks like, I find the code hard to read and understand. Is it just me?


I was a python main before I started using elixir and I had similar thoughts, but after becoming more familiar with it, I actually think elixir is far less verbose than python.

They’re different enough languages that it’s really difficult to just open up an elixir codebase and try to read it. Elixir, like ruby, has a lot of syntactic sugar that just won’t make sense if you approach it with a Python mindset. Elixir is also functional, which can be a pretty big mind bender.

It’s really a language that you need to start from the basics and go through the tutorials to learn the basic syntax, operators, etc and write your own small programs. If you’re like me, you’ll just end up discouraged if you expect to be able to learn Elixir just by reading existing code.


I’ve been writing Elixir professionally for ~10 years at this point. I find it to be extremely easy to read, but that’s because side effects by and large don’t exist, and when they do they are very obvious. Ive written quite a lot of Ruby, JS, Swift, Obj-C, and Java during my career and Elixir is miles ahead of those for readability.

With that said, I do think because the syntax being familiar for Pythonistas and Rubyists that there’s an assumption it works like those languages and runtimes. It does not. It’s a functional language, in a purely functional VM, and that causes a lot of grief to newcomers. There are many assumptions folks bring with them that don’t hold. I think you have to take a step back when you come to a functional language with an OOP background otherwise it’ll be painful.

I’d encourage you to just write it for a while and avoid things like GenServers. Get a feel for just writing functional (pun?) code. Id also suggest to avoid reading code by people new to Elixir as it’s often weird, or irregular, because they lack the experience to write things idiomatically; relying too much on OOP principles.

As an example, in Ruby you strive to write functions less than six lines (in general)— in Elixir this can be an antipattern. Because breaking up the function will unnecessarily abstract and muddle what’s going on. That in large part is driven by the fact there are no side effects; I can see everything that is going on. Whereas with Ruby side effects and meta programming are prolific, making the code extremely dense. Writing a method that’s 20-30 lines in Ruby almost guarantees it’s violating the single responsibility principle.

The one thing that did trip me up when starting elixir, that I now love, was shadowing variables. It makes things look like they’re being mutated but they absolutely are not. It’s just reusing the same name, not the same value.


Thanks a lot for sharing your perspective. This convinces me to give it a proper try!


I think there are two things going on:

1. The syntax is significantly different than what one may be used to. Fair enough. 2. The paradigm itself is different and thus requires you to learn way of structuring your code in some pretty fundamental ways. This is a much bigger deal than one may initially give it credit for. For example, pattern matching. The mere possibility of defining multiple versions of the same functions based on the number and kind of arguments completely changes how one writes functions.

Instead of:

function hello(x) {

if (x == 2) {

// do x

} else {

// do y

}

}

We can write:

def hello(2) do

# do x

end

def hello(_) do

# do y

end

This was very disruptive to my brain at first as I was not used to reading code in this manner. My brain said "find function, read definition" but it had to learn "find function with proper arity and arguments, read definition". It's hard to put into words, but this made understanding Elixir code feel more difficult at first. However, once I did sink my teeth in , start to maneuver around code bases, and write my own modules it is hard for me to "unsee it" and go back to anything else. I love it! I think the most mind-blowing moment was when I started to pull back the covers in Phoenix and discovered that the framework wasn't some magical black box of FancilyNamedProviders but was just a collection of functions that I could actually read my way through quite simply. I never had to stop and say "wait what did that FacadeOfGoodness do again" but just traced simple functions.


Having worked a bit in Ruby, Python, and Elixir I think a lot of the difficulty comes from the minor differences. There aren't any objects or classes so the structure is naturally different and there is a tendency to have a lot of small functions so it must be read differently. If you're seeing tons of deeply nested Enum.map type calls, that's probably just bad code.


I think its worth spending more time in it. I've been working on an MMO in Elixir and I find myself coming back to rework/expand earlier subsystems and expecting to dread understanding WTF is going on but that has not been the case at all! Regardless of what knowledge I had of Elixir at the time I wrote the module - I can quickly pick up the chain of thought I had when I first wrote it. The code usually boils down to a combination of atoms (symbols), pattern matching on atoms, tuples, return values, and some sort of enumeration using Enum.map or Enum.reduce to produce an output. There's no other fancy tricks beyond that. And regardless if the data changes from list of tuples, to a map, or a set, the Enumeration logic doesn't change.


Maybe you forgot how long it took to learn to read & understand the code of the programming languages you're familiar with. Elixir is so different from languages you might know there's a learning curve.


> Is it just me?

No, it's the same for everyone: unfamiliar things seem "wrong" until you internalize them.

Try working in Elixir for half a year - you'll see that the syntax you now find verbose is that way for a bunch of reasons, and you'll find that you have no problems at all reading Elixir code, including in big projects.

The problem is that confronting the unfamiliar doesn't feel nice for most people and that you can't really speed up the process. On the flip side, though, half a year of weekends is not that long of a time or significant an investment. Especially since you'll get quite a few generally helpful skills out of it (working with actors, for example).


I felt the same way at first coming mostly from JS. I think it's different enough that you need it to become familiar before it feels as comfortable.

That said, I don't think it's as easy to read as Python, but the perceived verbosity and reading difficulty definitely goes down with familiarity.


Tbh, I find well written Elixir to be easier to read than Python. But that's mostly down to functional vs imperative which is subjective/cultural. The same code using higher order functions and pipes that feels much simpler to me would likely be unpalatable to most Go programmer I know, and maybe a lot of Python programmers too (tbf, I can't stand the vast majority of Go code I have seen either).

Part of the verbosity is because Elixir is kind of a simple language (if you disregard the OTP stuff) and doesn't have a lot of sugar - ultimately it's just functions. Whatever else sugar is there is to nudge you towards a somewhat opinionated aesthete. In this specific regard, it's not unlike Go or Smalltalk (even if in any other way they are very different otherwise). I think the stylistic variance in how different people write Elixir is pretty low compared to how people write Perl/PHP/Ruby/Python, which is probably why reading difficulty quickly goes down with some familiarity.

Other part of verbosity is probably because culturally Elixir is kind of explicit. For example Phoenix is very convention heavy, but it's still low on framework magic. It's cool that Elixir (and Julia) has macros and that's good for library and framework authors to contain verbosity without sacrificing much of the explicitness. But as an application developer I very rarely need to reach for them either? Perhaps that's due to simplicity of language and synergy between data structure and standard library.

Though based on usage there are probably some areas where I would have preferred more sugar. For example, you just end up doing a lot of deconstructing/pattern-matching Maps. Every time I do `%{var1: var1, var2: var2} = map` I wonder if it would really be bad to have `{var1, var2} = map` like Javascript? It's not a very big deal though.

Also not to be a naysayer, there is a chance that the good feeling people have about Elixir is because they basically use it in the problem domain where the language and the runtime (and Actor Model) is an exceptionally good fit for. I mean web and network stuff is already large part of what people do, so that's not much of a con? Nevertheless would I pick Elixir for my next GUI or CLI program? I doubt it.


I like your take here quite a bit. Thank you for sharing!


You'd probably love gleam. Elixir ecosystem dwarfs it at this point, but the syntax and design choices are just phenomenal.


Elixir ecosystem is in turn dwarfed by dozens of other languages. It is still extremely niche in the big scheme of things. I can't personally recommend pursuing Gleam for any serious work on those grounds, but my risk aversion and sensitivity to hiring pools is on the very conservative end.




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

Search: