dogweather
Idiomatic, real-world Elixir resources?
Can anyone recommend books/courses/videos that use real-world Elixir? E.g.:
- Idiomatic error handling design, whether it’s
{ok/error, ...}or something else. - Uses specs as they “ought” to be, however that is.
- Uses the various mechanisms for creating types such as
@type,defstruct, and@enforce_keys. - Test-first coding generally, if that’s common.
- Idiomatic module and function naming.
- Use of keyword lists in function signatures.
(The resource or two I’ve purchased teach good architecture, apparently, but are not idiomatic, unfortunately.)
Most Liked
dimitarvp
This is definitely partially against my principles because I don’t believe in universal truths most of the time. But I’ve allowed myself to compile a list of those possible idiomatic good practices – which IMO go well beyond Elixir itself.
-
Use
@spec. However I’ll strongly recommend against that during rapid prototyping. It will just get in the way. The moment you find a module haven’t changed in 2 weeks however, go put specs on its functions and start chasing the green output ofdialyzer. -
Complement
@specwith your own guards. If you know a datatype you use is actually a list, DO NOT guard it withis_list(param). Define a guard and use that instead:
@spec orders :: [%Order{}]
defguard is_orders(x) when is_list(x)
def eligible_for_discount?(orders) when is_orders(orders) do
#...
end
-
Do not overdo docs. Many times they should even be superfluous. If your function is well-named and the parameter names hint at their purposes clearly enough you should be just fine by putting something like
Checks if this batch of orders is eligible for a corporate discountor even without docs at all. Module docs are mostly important for widely used libraries. For your own open-source pursuits and commercial projects they are mostly a distraction. Go for GitHub’s wiki articles, or Atlassian’s Confluence pages, etc. -
Don’t use
importif you can help it. Resist it with all your might. Only use it if you have no other choice and your code will not work otherwise. If you really must use it, use the:onlyoption. -
Don’t overdo
caseandcond. Use function heads with hardcoded parameter values, saydef length([]), do: 0for example. Readability often means less coding lines (though certainly not always; too much brevity can make your code very cryptic but that’s a bigger topic I won’t tackle here). -
Use macros and don’t be shy about it – but use them sparingly. For example when you want to transcode stuff and writing all functions manually would be tedious: that is a very valid usage. Macros should be used to make an otherwise annoying code generation easier and quicker. Some here in this forum get tempted to use them to introduce their own extra syntax in Elixir but I am very against this; these efforts usually attempt to emulate another language’s constructs and rarely seem aimed at actual productivity (at least not in the way I understand it; I realise this is a controversial opinion). They are usually used to rekindle nostalgia for paradigms the author fondly remembers from other languages. I am personally strongly opposed to these temptations; if using macros can both reduce the coding lines and improve readability then I’ll use them. But I am not a fan of the LISP approach where people make their own DSLs which are perfectly suited for the project… and nobody else except the author can maintain them or the project after they are introduced in it. We have to keep our work maintainable. That’s the professional courtesy I want to show to future maintainers of my code and thus I resist using macros in Elixir beyond simple readability improvement and reducing otherwise tedious coding work.
-
GenServer,Agentand many OTP primitives and derivatives should be used for (a) keeping state and (b) making your system resilient to partial failures. If you find yourself using them for other purposes then you are abusing them and should reconsider your approach. -
As noted above, I like
@enforce_keysfor structs, provided I am not in the rapid prototyping phase. I will prefer compile-time errors to runtime errors 99% of the time. -
Some extraneous code is acceptable if it makes using your modules more ergonomic and intuitive. For example I regularly make wrapper
Configmodules for my apps and libraries where I expose both genericput/getfunctions and specificput_*andget_*functions (say,put_timeoutorget_batch_maximum). I’ve had colleagues argue with me over this and I’ll concede this is a personal preference but to me the intuitive reading of my code almost as if it’s English should win over small extra amounts of code.
I am probably missing several others but hopefully this gives you an idea of what I find to be good practices. Apologies for this becoming rather huge.
sasajuric
I agree with app vs library point, but I’m not sold on the conclusion. I don’t have enough of a sample to know what “the wast majority of applications” does, but in my limited experience of trying both approaches for a couple of years, I definitely prefer app code with specs, because they help me understand what are the allowed inputs and what are the possible outputs, which in turn reduces the uncertainty when reasoning about the code.
Consider for example the following function:
def create_account(params)
What is this function accepting and what is it returning? To know this we need to look into the implementation. If we’re lucky, the impl is simple and we see everything in that function’s body. But quite often, we have to dig through a bunch of other private functions to piece it all together. This is cumbersome, energy draining, and error prone.
Consider in contrast the spec version:
@type account_params :: %{
first_name: String.t(),
last_name: String.t(),
email: String.t(),
password: String.t(),
role: Role.t()
}
@spec create_account(account_params()) :: {:ok, Account.t()} | {:error, Ecto.Changeset.t()}
def create_account(params) do
This already tells us so much more without forcing us to read the entire implementation. That is in my view a key benefit of typespecs. Combined with proper naming and well defined responsibilities they provide great help in reasoning about the code, library or app.
This is why I prefer to say that types are first and foremost about documenting, not about catching bugs. When a type checker complains, it means that there is a discrepancy between the spec and the implementation. From the standpoint of the desired program behaviour we don’t know what is wrong (in fact the program might even be correct), but we do know that the documentation (spec) doesn’t match the code, so we need to fix something to sync them.
I agree though that one should not be fanatical. I typically spec only API functions (i.e. exported funs which are not marked with @impl or @doc false), except for modules such as Phoenix controllers or Graphql resolvers, because specs don’t really help here at all. I might occasionally spec a private function if I estimate that it’s interface is complex and the inclusion of specs will assist the reader.
When it comes to docs/moduledocs, I don’t write them in the app code unless there are some important implications not obvious from the name/spec combo.
lucaong
My suggestion, if you feel you know Elixir well, but you want to step up your practical Elixir coding skills, would be to read the codebase of some quality open-source projects. The source code of Elixir core library is a great place to start, as well as delving into libraries that you use, and possibly contributing to them.
Subjective opinion here: I would not stress too much about what is idiomatic or not, but rather develop a sense for which codebases are easier to navigate and why. Reading other people’s code helps in this respect, because we always approach it as “beginners” with little context. As developers, I think we focus too much on code style on a superficial level (stuff that linters can enforce) and too little on deeper aspects of what increases/decreases cognitive load.
Popular in Chat/Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








