Brian Lovin
/
Hacker News
Daily Digest email

Get the top HN stories in your inbox every day.

aeturnum

I disagree with so much of this! Though I can tell the author is coming from a lot of experience.

How often is it that you don't know if you're expecting a map or a keyword list? I don't think I've ever intentionally put myself in that situation (though it's nice that access supports both).

They say to avoid piping result tuples, but I actually think their example code shows the strength and weaknesses of both approaches. I use pattern matching on tuples when you want more complicated behavior (that might be hard to read in a big else block for a with) and a nice with statement when there's 'one true path.'

In general, I think one of my favorite things about Elixir is that they've done a good job of avoiding adding features you should never use. These all feel like personal judgements about tradeoffs rather than actual hard rules.

keathley

> These all feel like personal judgements about tradeoffs rather than actual hard rules.

I'm glad it came across this way. These are all ideas that I believe have helped me build more maintainable systems. But that's just my experience and opinion. I'm happy people are finding stuff to disagree about because I also want to grow and improve my own ideas.

I regret titling the post with the words "good" and "bad". This started as an internal memo for my new team, grew into blog post and I didn't change the title. The title adds connotations that aren't very useful to the reader and sets up a more confrontational tone then I want. Live and learn.

Either way, thanks for taking the time to read through it and for providing feedback.

mercer

> I regret titling the post with the words "good" and "bad".

The title struck me as exactly what I would expect from you (based on various articles/comments and your podcast), and fwiw, I personally really appreciate it.

You're one of the people in the community that communicates in a way that often strikes me as 'overly' opinionated, but the combination of that and your actual experience tends to lead to fruitful discussion (see this thread). Plus, I often find myself wanting to 'prove you wrong' and then I end up doing a bunch of equally fruitful research.

So keep doing your thing, is what I'm saying, I suppose. It's appreciated :).

aeturnum

Thank you for writing it! It was a pleasure to read and I appreciate you contributing to the community.

theonething

> I disagree with so much of this! Though I can tell the author is coming from a lot of experience.

I bit off topic, but this sums up a lot of my experience with code reviews. People have opinions on what is the "one right way" and if I differ, we go through this drawn out discussion on Github about it. Most of the time, I see it as there are different ways to do it with different trade offs. I find myself just giving in so I can get the damned PR approved without a few days of back and forth.

Granted there are times when there is a much better way. In those cases, I very much appreciate the feedback. It's truly educational. In my experience though, most of the time, it's senior developers insisting their style/way is best.

0xFACEFEED

I think it's important for teams to establish best practices and enforce them. It's good for team cohesion and culture.

For example I've worked on teams that blocked PRs on minor typos in code comments. The signal this sent to me was "a little goes a long way, we care about the little things".

Sure enough that ended up being the cultural norm and the team benefited from that mentality. There were also downsides though! That's part of the trade-off, but the benefits of a unified "culture" outweighed the downsides by far.

What's reeeaaally bad is when you have competing points of view between tech leads and they passively enforce their own idea of what's "right" without regard for the other lead.

travisgriggs

I agree with this. There doesn't have to be One True Way, but within the scope of shared work it's useful if there's a sense of "around her we all..." especially if the multitude of ways compliment each other so that sum is greater than whole.

Where this gets hard for me, is in transitioning between teams, contributing to open source projects etc. The dialects can get kind of discordant. At least with Elixir, I find these differences are more palatable because the basic paradigm is consistent. In the "hybrid" languages, you get competing language models in the same code base.

arthurcolle

Agreed, this was a huge pain in the past for me. Then, the even MORE senior engineers are just like "Ok do the tests work? Yes? Ok whatever, approve"

So there's this middle section of "senior" (but still in age terms relatively junior in their lifecycle) that are just trying to exert dominance over a dumb web app that controls the flow of the spice (errmm.. code) in the org. It's really silly and deeply counterproductive.

bmy78

I agree with so much of this! I think it stems from so many software engineers have a quantitative background, where solving for x is either right or wrong. Much of “good” software engineering, however, is subjective and I think many of these folks have a difficult time reconciling that. So you get into these battles on which is the best way when, unless it’s something egregious, is probably good enough.

I have a writing and liberal arts background and so while bad writing is obvious, good writing comes in many stylistic forms. I think this has allowed me to see varying perspectives on “best practices.”

danmur

I've noticed this is much more pronounced if there's been no discussion before the PR (talking more about larger decisions than what's in the bulk of code review feedback I guess). I've observed many times people being frustrated because they write some code without making sure the relevant people are happy with the overall approach, then getting torn apart in the PR.

mattbaker

Yeah, I disagreed with a decent amount, but I tried to read it as “here are the patterns and practices I feel passionate about” and that made it an interesting read for me.

The sort of declarative good/bad language worries me a bit because people new to the language may take it as gospel, but overall I’m glad to see people putting their thoughts out there with explanations and examples :)

aeturnum

I do think that being opinionated about code style leads to code that's easier to read and maintain. I was just delighted to find someone whose tastes diverged so distinctly from my own in a smaller language community.

bcrosby95

Aside from using an automated formatter/linter, I think being opinionated about code style is only really sustainable for single person projects. Otherwise it leads to inconsistent code reviews where people are making changes for reasons they can't understand.

innocentoldguy

It's one thing to be opinionated about code styles, but I would argue that doing so when those opinions go against standard idioms is a recipe for disaster.

If I ran across a lot of these suggestions in code, I'd refactor them to be more idiomatic/practical.

jackbravo

But how do you take this to a code review scenario? Are these suggestions that you can take or ignore?

pdimitar

Most of those in the article you can ignore and get away with it for a long time. Some of them start to become problematic once the code grows quite a lot, others are not a problem ever because they are very dependent on the context.

joshribakoff

Also disagree with the author!

> You should not use else to handle potential errors (when using with)

Yes you should! Otherwise the with statement could return something that is not :ok and also not :error. I think therefore it’s good practice to put else {:error, error} -> {:error, error} especially because Elixir is not strongly typed so this helps constrain the possible types the statement can return. If you omit the else, your construct can return any number of types from values produced downstream.

gamache

IMO it's not as simple as "just use Access". In addition to maps and keyword lists, there are structs in play.

Access works with maps and keyword lists, but not structs. There are pieces of code I've written that reasonably accept map or struct, but not keyword list.

Map.get/3 also has a default value; Access only lets you use `nil` for that.

OTOH, the nil-eating behavior of Access is terribly convenient sometimes.

It's all about what you need at the time. There is more than one way to do it, for a reason.

jimbokun

> How often is it that you don't know if you're expecting a map or a keyword list?

It's just programming to the interface and not the implementation, which often has advantages and very rarely has any downsides.

pawelduda

The benefit of Map/Keyword is that you look at the code and you know right away what type it takes. Access is more universal but you look at it and have multiple possibilities.

jolux

I would recommend writing specs for your functions and using Dialyzer, too :)

arthurcolle

I've never been able to figure out how to write custom types in Erlang. Any resources in particular that you recommend? I am not trying to be obtuse but the Erlang docs are unreadable and I have admittedly written quite a bit of Elixir code. Most of the time I have to just figure things out ad-hoc from my own interactions with the shell.

pawelduda

I support that too, but it's yet another step as opposed to simply using a specific module to suit the data type :)

On another note, I find Dialyzer a bit slow to run even on modest codebases.

tym0

I'm not familiar with Elixir but I'm surprised that the "functional way" wouldn't be to use the pipes in conjunction with a map function that only applies each steps if it's a success and skip them/pass the value if it's an error.

skrebbel

I love this article to bits. The elixir community loves blogging, but so much elixir content is absolute beginner stuff, and so much of the remainder is unopinionated, descriptive stuff. I've found it really hard to strike on some good "how to best do xyz, what are the tradeoffs" style knowledge. This article is the first elixir thing I've read in a long time that really made me go "o wow yeah, I've been doing that wrong". Cool stuff!

keathley

Thanks! I'm glad you enjoyed the article and it could provide some useful insights.

SinParadise

I think this is true for most programming languages. I want more stylistic, opinionated stuff that comes with contexts and caveats, so I can get to understand their decision making process and avoid footguns that can only be learned through sufficient experience.

mikepurvis

I totally agree, though I think those articles are a lot harder (eg requiring more skill) to write well because you need to quickly ramp your readers on all of whatever the context is that's necessary to actually appreciate the nuance of the design decisions under discussion. You're basically by definition going to be out of the realm of "just follow best practice X" or "apply pattern Y or you're doing it wrong."

As a small example, I've been working on a small asyncio-based web service (Python) which is oriented around an expensive process that generates a result, where the result is stashed in sqlite and returned. I knew upfront that I needed a way to track when a particular result was already being prepared so that if I got a second request for it, it would collapse it into the first one and only do the work once. I wrote this as a twenty line memoizing decorator, but it turns out this issue as a name— cache stampeding. Once I realized that, I discovered that there are existing (and much more complicated/tunable) solutions to this problem, such as https://github.com/DreamLab/memoize/, but the article pitching that solution spends quite a bit of time getting to it— enough so that if I'd discovered it before building my own, I'm not sure I would even have appreciated its applicability:

https://tech.ringieraxelspringer.com/blog/open-source/cachin...

jkhdigital

This hit close to home, especially as a mostly independent developer who can’t ask the neighborhood senior engineer about stuff.

A much more basic example of this from my recent experience is discovering GenStage, which solves the generic problem of backpressure in sequential processing but doesn’t appear in the core Elixir documentation.

losvedir

Good stuff. I disagree about the first point, though, and have actually gone the opposite way, going from generic Access via `[]` to specific `Map.get` and `Keyword.get`.

The reason is that the underlying data structures are quite different, and operations and access patterns on them shouldn't be agnostic.

You should use Keywords basically only as options to functions, in my opinion, and that's just for historical consistency and interop with Erlang. Otherwise key-value pairs should be a map. And using `Keyword.get` and `Map.get` make it immediately obvious what the underlying data structure is, as well as prompt you to think about whether you should use the `!` version or provide defaults.

Otherwise, I think the tips are all decent to great. I am a little on the fence with exceptions, though. I know there's a meme in the community of "let it crash", but in my experience, that rarely feels like the right thing to do. I much prefer a Rust-style, "Result-all-the-things". It seems hard, in practice, to rely on crashes and restarts to maintain the right state invariants.

dnautics

> It seems hard, in practice, to rely on crashes and restarts to maintain the right state invariants.

I feel the opposite way. Perhaps if you are mostly doing things where you have control over a transient state (like a chain of validations on a database entry) the error tuples are easy...

But if you are doing something like checking in on cached state for something remote that you don't have control over... It is so, so much better to give up, restart your GenServer, and re-sync your cached state in the init() clause than try to "guess" what the remote thing has for its state after it's sent back an error.

losvedir

I dunno, I rarely get issues that crash just once. For example, in the blog post, it's recommended to use `Jason.decode!`. But what if whatever server you're hitting is returning invalid JSON for a time? If you crash, the GenServer will restart, it will crash again, and it will just be either in a crash loop (meanwhile DOS-ing the server?), or worse - if it happens in `init` - will take down its own supervisor after a number of failures, cascading up the tree until your app crashes and restarts.

I think it's better to handle the Jason.decode error, and apply some sort of backoff strategy on re-requests. In general, you have so little control over the timing and count and rate of restarts. It's a very blunt object to just crash. Again, with the `Jason.decode!` error, suppose it's in a GenServer that's updating its state. In some cases you may want to reset the state to empty (essentially what crashing does) and in other situations you may want to keep the previous state, from the last time you made that request. Maybe you want to track how many consecutive failures you're seeing and maintain the state for some time, but eventually blank it, or crash at that point.

It could very well depend on the problem domain, though, for sure. I mostly work on freestanding, pure Elixir apps, and a couple of Phoenix apps. I'm more thinking about the freestanding apps, since in Phoenix, sure, just crash your request, whatever.

keathley

You can certainly invent a scenario where using `Jason.decode!` wouldn't be appropriate. In that scenario, absolutely, handling the error and backing off is more appropriate. I'd also argue you shouldn't be doing side-effects like that in an init without a lot of care as well.

The same would be true if you were building a kafka consumer. You wouldn't want to crash in that scenario since you could easily poison the entire topic with one bad message.

There are ways to allow for crashes this way though. An alternative approach would be to use a dedicated process for scheduling work and a dynamic supervisor that can be used to start workers as needed. The work scheduler would monitor these worker processes. This means that you could freely crash the worker and allow the work scheduler to determine what further action must be taken. I've used both your approach, and this alternative approach in the past both to good effect.

dnautics

> I think it's better to handle the Jason.decode error, and apply some sort of backoff strategy on re-requests.

What's keeping you from implementing a back off strategy in your init? You can return :ignore to the supervisor and try again later.

If you're backing off inside the process itself, you're gonna get some weird shit where your service exists but it's not in a truly available state, and you will have to handle that by basically halfheartedly rewriting logic that already exists in OTP.

bcrosby95

Reminds me a bit of this: https://discord.statuspage.io/incidents/62gt9cgjwdgf

In particular: "14:55 - Engineers pinpoint the issue to be strongly correlated to a spike in errors in originating from our service discovery modules. It is determined that the service discovery processes of our API service had gotten into a crash loop due to an unexpected deserialization error. This triggered an event called "max restart intensity" where, the process's supervisor determined it was crashing too frequently, and decided to trigger a full restart of the node. This event occurred instantaneously across approximately 50% of the nodes that were watching for API nodes, across multiple clusters. We believed it to be related to us hitting a cap in the number of watchers in etcd (the key-value store we use for service discovery.) We attempt to increase this using runtime configuration. Engineers continue to remediate any failed nodes, and restore service to our users."

macintux

The general theme for people who’ve been working with Erlang for a long time seems to be: rely on crashes for truly unexpected scenarios, but validate data (especially at the edges) so crashes aren’t a regular occurrence.

candeira

In the spirit of Python's "Exceptions are when the runtime didn't know what to do, and Errors are when the programmer didn't know what to do", I'd say "rely on crashes for exceptions, but validate data against errors".

pmontra

I wish we had only maps and only with either strings or atoms as keys. I ended up too many times with code that failed because it got a string as a key instead of an atom or viceversa.

brobinson

Reminds me of the following from my Ruby/Rails days: https://api.rubyonrails.org/classes/ActiveSupport/HashWithIn...

devoutsalsa

I don't really like this article cuz:

- I like &Map.get/3 over the some_map[:some_key] syntax

- &with/1 can be hellza confusing in many case if you're calling a bunch of functions, and any ol' one of them can return an error message (e.g. where the heck did `{:error, :bad_arg}` come from?!

- piping into case statements is awesome, and I'll only stop doing it when people complain about it

- higher order functions are useful, and I don't see a problem using one's judgement as need on when to use them

- In the cases where using &with/1 is actually useful, so is using the else block if you really need to

...

Yeah, I just don't like this article's recommendations.

tomjakubowski

> - &with/1 can be hellza confusing in many case if you're calling a bunch of functions, and any ol' one of them can return an error message (e.g. where the heck did `{:error, :bad_arg}` come from?!

You agree with the article on this point:

with is best used when you can fall through at any point without worrying about the specific error or contrary pattern.

cormacrelf

I’ve never used Elixir, but when I saw piping into case statements I was confused as to why it’s appearing on a ‘do not use’ list. It does seem awesome! The key thing for error handling is to be able to do it inline, without introducing nesting, like those withs are doing. Maybe you don’t get that with case either. Does elixir have a return statement?

jkhdigital

Elixir, like Erlang, is functional so there are no statements. Everything is an expression, which evaluates to some value when executed. The “return” value is just the value of the last expression evaluated in the function call.

cormacrelf

Is there an equivalent of the Haskell Either monad & do-notation / Rust Result + the ? (try) operator?

pipingdog

Elixir does not have a return keyword.

innocentoldguy

I agree with piping into case statements. It's awesome. You're a better person than me. I'll never stop doing it no matter how loudly people complain. :)

dnautics

not to be too nitpicky, but the article doesn't say don't use higher order functions; the article says don't wrap higher order functions into something that hides it higher-orderness.

kvnhn

"Don’t hide higher-order functions

Higher-order functions are great, so try not to hide them away. If you’re working with collections, you should prefer to write functions that operate on a single entity rather than the collection itself."

Excellent advice -- and not just for Elixir. I find myself applying this technique often in Python, for example. While the example is fairly simple and contrived, the consequences they note ring true.

devoutsalsa

Yeah, I misread the bit about about higher order functions (or they edited the article).

msie

Lots of good points. I gotta blame my Elixir tutorial (Dave Thomas) for some bad habits: over-reliance on pipes and pattern-matching in function args. He hates if-then in a function. He would take an if-then in a function and split both clauses out into their own function. Seemed fishy to me. Trying too hard to be elegant.

macintux

I agree with Dave on the pattern matching in function heads, at least from your description. Garrett Smith’s blog post on tiny functions[0] has heavily influence my Erlang.

[0]: http://www.gar1t.com/blog/solving-embarrassingly-obvious-pro...

nemetroid

The author uses the word "religion" in a jocular manner, which seems appropriate, but probably not in the way he intended. This particular style often seems to get applied as a dogmatic pattern. The underlying principles are good, but the author should have stopped halfway.

For example, the article has this:

  handle_db_create_msg(Msg, State) ->
      log_operation(db_create, Msg),
      handle_db_create_result(create_db(create_db_args(Msg)), Msg, State).

  create_db_args(Msg) ->
      [create_db_arg(Arg, Msg)
       || Arg <- [name, user, password, options]].
  
  create_db_arg(name, Msg) -> db_name(Msg);
  create_db_arg(user, Msg) -> db_user(Msg);
  create_db_arg(password, Msg) -> db_password(Msg);
  create_db_arg(options, Msg) -> db_create_options(Msg).
  
  create_db(Name, User, Password, Options) ->
      stax_mysql_controller:create_database(Name, User, Password, Options).
There is no shared logic between the different cases in create_db_arg() (creating a false sense of abstraction where there is none). create_db() has to take its arguments as a list* in order for the design to work, but the use of a list suggests homogeneity between the elements, which is not the case.

It would have been much better to stop at this point:

  handle_db_create_msg(Msg, State) ->
      log_operation(db_create, Msg),
      Name = db_name(Msg),
      User = db_user(Msg),
      Password = db_password(Msg),
      Options = db_create_options(Msg),
      Result = stax_mysql_controller:create_database(Name, User, Password, Options),
      handle_db_create_result(Result, Msg, State).
*: which the author also forgot when making the final listing at the end of the blog post.

nivertech

There are lots of contradicting advice in the Erlang/Elixir land.

Garret Smith likes microfunctions, while Saša Jurić avoids them.

Same with "tagged with statement" pattern - Saša Jurić prefers it over Ecto.Multi in transactions, while this post discourages it's use at all.

ludamad

I notice he says the result needs less testing; to me such a rewrite amounts to an audit of the code and thus leaves one with more confidence, definitely. I'd just buy it more perhaps with stringent static types, but I've become a 'gradual typing purist' in a way (in this process, if you're not littering types everywhere you are missing a lot of value)

matt_s

Having lots of gnarly if-elsif-else blocks of code elsewhere where the logic conditions are varying in number it is nice to encapsulate that in a pattern match function.

For me, it doesn't mean "thou shalt not use if-then ever" just that if you have enough conditionals and logic needed, pattern matching is better suited for those cases (sorry for the pun).

derefr

Mind you, when some of the Elixir books were written, the `with` syntax didn't exist yet.

ofrzeta

pattern-matching in arguments means that the picking the function according to its signature is the actual decision process? If you have a function that doesn't contain a decision it is easier to unit test, isn't it?

innocentoldguy

No. It's the same. If you have a function with a large if statement in it, you'd want to test all conditions within that if statement to make sure it was working, right?

You'd do the exact same thing if you were testing a series of pattern-matched functions. The tests would be unchanged either way.

jkhdigital

I think it makes it easier to tick off the list of cases that need to be tested: one for each head.

ofrzeta

You'd have the number of tests broadly but they are simpler because they don't need to handle the cases hidden in the functions themselves. Maybe in the end it's the same or a matter of taste but it recently occurred to me that unit testing becomes "cleaner" that way.

brightball

I came out of that Dave Thomas classes with a lot more good habits than bad. I don't follow everything he does, but the vast majority of it made a lot of sense to me and results in much cleaner code.

innocentoldguy

I started my Elixir journey with Dave Thomas, too, and I agree with your commentary. I think most of Dave Thomas' style is highly organized and readable.

One of my favorite Dave Thomas tidbits was writing private functions like this:

  defp _private_function(params) do
    # Do something
  end
Whenever I see a function that starts with an underscore, I KNOW it is a private function in the current module. I don't have to go looking for it or wonder where it came from. I know at a glance that it is a private function.

Readability and organization are so important in code and I felt that Dave Thomas helped me get off to a great start in this regard.

msie

Good to hear all the opinions. I'm an Elixir newbie and I appreciate them.

ConanRus

This is a standard Erlang pattern and what's make it so great. And Erlang VM is optimized for that.

71a54xd

I write elixir full-time at a fintech startup and I can confidently say that this blog is clearly written by someone with significant experience. With statements are key to nailing a balance of error handling and pattern matching, however for trivial functional flows (that don't require error handling), pipes are entirely fine.

The biggest challenges for elixir currently are the lack of established HTTP libs with dynamic testing and db ORMs. For instance, interacting with PSQL is fine using ecto, but using a document based DB or something like Neo4J is an absolute headache. Deployment is also pretty easy, but that's a whole different side of development.

sergiomattei

I'm so glad to hear that last bit: seems like the deployment story in Elixir has really improved lately.

It wasn't long ago where the only posts in here would describe it as the biggest headache!

1_player

> Don’t pipe results into the following function

Mmmm. No. It makes sense into context of the example, but this is way too generic advice it's actually bad.

Pipes are awesome. Please _do_ pipe results into the following function when it makes sense to.

janderland

I didn’t get the impression they were saying never to do it (maybe I missed that bit in the text).

I think they specifically said, when piping, don’t force functions to handle the error of the upstream function.

Because each error needs to be handled differently, the downstream function’s implementation becomes tied to the upstream function. Then the downstream function is only usable with said upstream function.

devoutsalsa

The more I need to handle errors, the more I want to make every unexpected thing just raise an exception. 500s for everyone!

jkhdigital

Exactly, piping a bunch of ! functions is the way.

rubyn00bie

A lot of a great advice! but I'd like to shave some yak on my lunch so I'll comment on what I disagree with :)

I sort of disagree with Access vs Matching vs Map; if I change my data structure I'm changing the contract I've established. I want things to break in that case, and I want them to break as fast as possible. This seems like great advice for a controller action, not so much for a gen_server or anywhere I'm NOT dealing with user input.

And... I completely disagree with "Raise exceptions if you receive invalid data." If you receive invalid data return an error saying that... please for the love of someone's god, do not raise an exception. You have no idea what the concerns of the caller are, so just return an error tuple.

> This allows us to crash the process (which is good) and removes the useless error handling logic from the function.

No, the exception returned will likely have no meaning to the client and will more than likely confuse them or your. You will still have to write error handling logic. You still have to format the error. You still have to log or report the error... Now you are doing it in multiple places because you're raising and likely still having to handle data validation issues. The best of both worlds, IMHO, is returning an `{:error, exception}` tuple.

Edit: Forgot the "NOT" in the end of the second paragraph.

lmm

> If you receive invalid data return an error saying that... please for the love of someone's god, do not raise an exception. You have no idea what the concerns of the caller are, so just return an error tuple.

> No, the exception returned will likely have no meaning to the client and will more than likely confuse them or your. You will still have to write error handling logic. You still have to format the error. You still have to log or report the error... Now you are doing it in multiple places because you're raising and likely still having to handle data validation issues.

If it's invalid in the sense of indicating a bug in the caller (e.g. by violating your function's preconditions), raising an exception is the right thing to do. You don't understand the problem, and probably the only way it can be fixed is by the developer fixing it. Precisely because you have no idea what the concerns of the caller are, each caller needs to do its own validation and error handling logic.

If it's a case of "invalid" input but valid code (e.g. a function that's designed for parsing user input), then that's the case where doing validation yourself and returning an error is better.

(This kind of distinction is where it's really helpful to have a type system, so you can use separate types for raw input and parsed data that's expected to be valid).

etxm

So much this. “Let it crash” is cargo culted too hard IMO. If a caller (being another library or end user) can be given information to correct an error, give it!

Let it crash works well for a phone system where it doesn’t make sense to call both parties back if the thing failed.

dnautics

Uh I think the rule of thumb is, if you're writing a library do return an error tuple because it gives the caller the option, and if you're a mensch also provide a bang function. If you are consuming a library in most cases let it crash.

querulous

let it crash is great for unrecoverable errors as it gives you clean state as if the thing that caused the error never happened but it's terrible for cases where you want to actually capture the error and react to it. give me a structured log event or an error code every time over a stack trace

keathley

Your talking about errors in the context of a web request or RPC. In those situations then sure, return an `{error, exception`} (that's the pattern I advocate for as well). But, I've found a lot of benefit from designing systems where crashing was an acceptable outcome to be really beneficial and increased the systems overall resilience.

whalesalad

I am in love with Elixir but am still on like a 1 out of 10 on the mastery scale. Love seeing posts like this!

(I bit off more than I can chew by trying to write a full-featured API for an embedded GPS device that speaks a proprietary binary protocol - it's been a slow project)

toolz

Agree with almost everything, but one point doesnt attempt to reason about the pattern. Why shouldn't I pipe into a case expression? It's visually pleasing, more concise and removes an empty line.

Easier to talk about where I disagree, but overall great read!

plainOldText

Yeah, it wasn’t obvious why one should avoid |> case.

I also think it looks pleasing, plus it’s easy to read and it avoids allocating a new variable compared to the alternative example given.

keathley

I didn't make a very strong case for it. Overall, my experience has been that calling the side-effecting function in the case statement directly tends to be easier to maintain and easier to read. But, I think piping into case can work well in certain situations. Its not one of the points that I feel that strongly about.

jolux

I disagree about not using the module-specific `get` functions. They should be used when you expect what you’re operating on to be a specific type. Sometimes I want to write a function that only operates on maps, and Map.get will throw an error if you pass anything else. Granted, I write specs for all my functions, and I come from a static language background, so this is just my perspective. Other than this quibble I think this is solid advice.

keathley

Interestingly enough, my advice to not use `Map.get` is the most contentious point in the entire post. I didn't suspect that would be the case.

I'm certainly not against using those functions and the additional checking can be of use in certain circumstances. The use case I had in mind was specifically how people will use `Keyword.get` to grab configuration values or options. This means that if you load configuration from a json file or external service, and it comes back as a map, you now need to convert to a keyword list just to initialize a library. Its a small thing, but in those situations I would rather allow for more flexibility in the interface.

That said, I think this advice is particularly useful for people building libraries more so then people working on applications. I may try to add more nuance to this point in the post.

jolux

> The use case I had in mind was specifically how people will use `Keyword.get` to grab configuration values or options.

It seems like this advice is very dependent. I think you should decide whether a function accepts maps or keyword lists or both, but I do not think that all functions which operate on key-value structures should default to accepting both. They have very different performance characteristics, and keys are not unique in keyword lists.

keathley

I agree on the performance characteristics. But if your using `get` then it doesn't matter that keyword lists have non-unique keys. You just get the first one.

sb8244

> That said, I think this advice is particularly useful for people building libraries more so then people working on applications

This is a good point that I hadn't really thought about previously. I agree with your thoughts much more when I consider the different needs between libraries and applications.

Daily Digest email

Get the top HN stories in your inbox every day.

Good and Bad Elixir - Hacker News