[HN Gopher] Good and Bad Elixir
___________________________________________________________________
Good and Bad Elixir
Author : todsacerdoti
Score : 200 points
Date : 2021-06-10 18:18 UTC (4 hours ago)
(HTM) web link (keathley.io)
(TXT) w3m dump (keathley.io)
| hmmokidk wrote:
| This captures exactly how I feel about with. Really good article.
| phtrivier wrote:
| The part about 'with' and 'else' is interesting but slightly
| conractictory with the part about handling errors in the "higher"
| function.
|
| That being said, my main issue with nested 'with' (or nested
| 'case', for that matter), is that nothing helps you make sure you
| handled all the possible way for any of the nested function to
| fail.
|
| And someone, someday, is going to add another way for one of the
| functions to fail, and you will not have written a handler for
| that because you're not clairvoyant. And you won't notice until
| it crashes in prod, and your customer complains.
|
| Good advice overall, otherwise !
| Grimm1 wrote:
| Agree with many things, big disagree with not using 'else' with
| 'with' and actually for the same reason you stated not to use it.
| I use 'with' when I have a pipeline, but the error conditions in
| the pipeline matter. In fact the example you gave in the article
| is exactly one of the cases where I think 'with' and 'else' are
| useful because you can clearly show the dependency of each action
| in the pipeline and clear failure cases to address them.
|
| I think there is a limit to how many things you should handle in
| a with statement, but the same goes for regular pipes as well.
| keathley wrote:
| My general feeling is that if the errors matter, `with`
| probably isn't the right construct. In those situations I
| prefer to use case. That's been helpful in my experience.
| devoutsalsa wrote:
| If the errors don't matter & there's no real chance of
| returning an error, why use &with/1 at all? &with/1 has the
| quirky behavior in that the return value of anything that
| doesn't pattern match get returned. So if &call_service/1 or
| &parse_response/1 doesn't return an an ok tuple, you're
| increasing your debugging surface by having to track down
| where the unexpected result came from. Good luck tracking
| down that rogue nil!
|
| Instead of this... with {:ok, response} <-
| call_service(data), {:ok, decoded} <-
| parse_response(response) do decoded end
|
| Maybe just do this... {:ok, response} =
| call_service(data) {:ok, decoded} =
| parse_response(response) decoded
|
| Or even this... data |>
| call_service!() # don't bother returning ok tuples
| |> parse_response!() # just use bang methods!
| pselbert wrote:
| One of the best parts of listening to Elixir Outlaws, the podcast
| that Chris co-hosts, is disagreeing with his wacky and strongly-
| held opinions on things. Personally, I don't agree with most of
| what he's calling out here, but it's still a great read to get
| you thinking.
| jolux wrote:
| 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 wrote:
| 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 wrote:
| > 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 wrote:
| 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.
| jolux wrote:
| Maybe it does, maybe it doesn't. Maybe somebody gave you
| a keyword list that they thought had unique keys and it
| didn't. The point is, the two structures are not
| semantically equivalent, and I think it's a mistake to
| conflate them in general. Just because you can use access
| syntax on both doesn't mean you should write a
| polymorphic function that does so. Sometimes you should!
| But not most of the time.
| macintux wrote:
| Nice. Solid advice from Chris.
|
| I'd wondered about the pipes construct in Elixir for exactly that
| reason. Sure, it looks nice and easy, but based on my experience
| with Erlang it seemed unlikely to be useful unless you are simply
| planning on crashing for any errors (which isn't really the
| Erlang way).
| hmmokidk wrote:
| You can use changesets or a similar pattern to avoid this.
| supernintendo wrote:
| I agree with almost all of this except for avoiding else and
| annotation in with statements. Here's my reasoning:
|
| 1. The argument the author makes here assumes that else clauses
| can or should only used be for error handling. There are
| instances where you might want to drop out of a with statement
| without returning an error however. Some examples might include
| connection Plugs that don't need to perform some behavior due to
| certain assigns already being present on the Connection struct or
| configurable / optional behavior encoded in an options list. In
| these instances, pattern matching on a successful fallback clause
| actually reduces complexity as it avoids the need to position
| that branching logic somewhere outside of the main with statement
| that it is actually concerned with.
|
| 2. I find error handling through with / else and annotated tuples
| to actually lead to very expressive, easy to read code. Having
| built and maintained a Phoenix application in production since
| 2015, this is actually a very common pattern I and other members
| of my team use so I'm curious as to why the author considers it
| something you do under "no circumstances".
| david_allison wrote:
| (non-Elixir dev here)
|
| Does Elixir have anything akin to Haskell's "do notation" for
| result types? The provided best practice code feels verbose, and
| that surprises me given that Elixir looked clean & modern.
|
| For example # Do... def main do
| with {:ok, response} <- call_service(data), {:ok,
| decoded} <- parse_response(response) do decoded
| end end
|
| would read in C# (.NET 3.5) as the following pseudocode, which
| personally looks much cleaner, avoiding `{:ok, ...}`:
| Result<T> Main(object data) { return from response in
| call_service(data) from decoded in
| parse_response(response) select decoded;
| }
| ch4s3 wrote:
| Your example is valid elixir code. def main
| do with {:ok, response} <- call_service(data),
| {:ok, decoded} <- parse_response(response) do
| decoded end end
|
| It may be a little semantically different but it's pretty
| similar.
| jolux wrote:
| Kinda, see
| https://hexdocs.pm/elixir/1.12/Kernel.SpecialForms.html#with...
| Grimm1 wrote:
| {:ok, foo} is just the largely community adopted soft error
| pattern in elixir
|
| call_service could just return response and remove the
| verboseness.
|
| so it would be:
|
| response <- call_service(data)
|
| I don't know C# but would that code explode if there was an
| error, instead of in again the soft error pattern likely
| getting {:error, foo} ?
|
| Regardless my point is that {:ok, bar} stuff isn't a language
| required aspect it's a community pattern if that makes it any
| better for you.
| masklinn wrote:
| > {:ok, foo} is just the largely community adopted soft error
| pattern in elixir
|
| It is _not_ a community pattern, because it 's the way OPT
| (Erlang's "standard library") functions work e.g. maps:find?
| returns `{ok, Value} | error`.
|
| And Elixir inherited that rather directly and build its
| standard library the same way. `Map.fetch` similarly returns
| `{:ok, value()} | :error`.
|
| It's not hard-coded into either language, but it's absolutely
| not "community adopted" or "a community pattern" either.
| Grimm1 wrote:
| Clearly not every library uses it, and not everything in
| the standard library adheres to it either, just as you said
| where the Erlang tie ins are.
|
| What would you call something then that clearly has
| optional adoption and isn't consistently used across the
| standard library?
| jolux wrote:
| In Elixir's standard library at least, it is pretty
| consistent. Functions that can fail return tagged tuples,
| and functions that will throw when they fail end in !.
| Grimm1 wrote:
| Map.pop is one counter example. The failure mode is just
| {nil/default, Map}
|
| Unless you use Map.pop! which raises an error, and
| because pop! raises an error I would then expect Map.pop
| to return {:error, nil} or {:error, Map} or something
| instead of {nil/Default, Map}.
|
| I won't try to back up where/if there's more this is but
| it was the one I had in mind while writing.
| jolux wrote:
| Well yes, because not finding a key in a map is not an
| error typically. Map.get is the same way, as is Access.
| It returns nil. If you want it to be an error you can use
| the version that raises.
| Grimm1 wrote:
| That's not true across many of the languages I use,
| dict.pop returns a key error in python for instance.
|
| By having a version that raises the standard library is
| basically saying not having a key is the error state.
| msie wrote:
| 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.
| msie wrote:
| Good to hear all the opinions. I'm an Elixir newbie and I
| appreciate them.
| ConanRus wrote:
| This is a standard Erlang pattern and what's make it so great.
| And Erlang VM is optimized for that.
| macintux wrote:
| 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 wrote:
| 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 wrote:
| There are lots of contradicting advice in the Erlang/Elixir
| land.
|
| Garret Smith likes microfunctions, while Sasa Juric avoids
| them.
|
| Same with "tagged with statement" pattern - Sasa Juric
| prefers it over Ecto.Multi in transactions, while this post
| discourages it's use at all.
| ludamad wrote:
| 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)
| brightball wrote:
| 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.
| matt_s wrote:
| 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).
| ofrzeta wrote:
| 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?
| derefr wrote:
| Mind you, when some of the Elixir books were written, the
| `with` syntax didn't exist yet.
| toolz wrote:
| 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 wrote:
| 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 wrote:
| 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.
| conradfr wrote:
| I never thought about piping into case ... I guess I'll try it
| next time ;)
| devoutsalsa wrote:
| If you really wanna piss OP off, pipe into &if/1 :)
| iex(2)> :something |> if do true else false end true
| iex(3)> nil |> if do true else false end
| false
| brightball wrote:
| We had a good discussion on this article yesterday in my local
| Elixir group.
|
| General consensus is that there's a lot of things pointed out
| here that are good to think about and consider, but don't treat
| them all as gospel.
|
| Some situations are better handled by other approaches, but
| understanding the goals of why the author has pointed these out
| will be beneficial for everyone.
| keathley wrote:
| > General consensus is that there's a lot of things pointed out
| here that are good to think about and consider, but don't treat
| them all as gospel.
|
| Definitely not gospel. These are patterns that I tend to use
| and believe have helped increase the maintenance of the various
| projects I've worked on. But I'm also guilty of going against
| these points when it seems appropriate.
| 1_player wrote:
| > 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 wrote:
| 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 wrote:
| The more I need to handle errors, the more I want to make
| every unexpected thing just raise an exception. 500s for
| everyone!
| whalesalad wrote:
| 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)
| aeturnum wrote:
| 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.
| pawelduda wrote:
| 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 wrote:
| I would recommend writing specs for your functions and using
| Dialyzer, too :)
| arthurcolle wrote:
| 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.
| jolux wrote:
| Depends what you mean by "custom types." Elixir supports
| structs, but they're not really as rich as classes in
| another language, for example. If you just want to write
| type signatures for your functions though, this is a
| pretty good guide:
| https://hexdocs.pm/elixir/1.12/typespecs.html
| arthurcolle wrote:
| Right, using structs for things I'm doing have been
| extremely painful, to say the least. Let's say I'm trying
| to create a module that represents a European option and
| I want to encapsulate things like "strike price",
| "expiration date", and the various Greeks in a way that
| are subsequently modifiable (perfectly find to then just
| respawn a new proc to have the new data). I haven't been
| able to identify a way to even reference other PIDs that
| exist which might be able to (with some wizardry) solve
| some of these problems.
|
| Right now I'm basically wrapping basically all my data in
| Agents with maps inside, but that is so lame.
|
| Thanks for the quick response. Maybe I'm trying too hard
| to use a saw to hammer a nail.
| gamache wrote:
| 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.
| keathley wrote:
| > 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.
| aeturnum wrote:
| Thank you for writing it! It was a pleasure to read and I
| appreciate you contributing to the community.
| theonething wrote:
| > 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.
| arthurcolle wrote:
| 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.
| jimbokun wrote:
| > 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.
| mattbaker wrote:
| 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 wrote:
| 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.
| jackbravo wrote:
| But how do you take this to a code review scenario? Are these
| suggestions that you can take or ignore?
| pdimitar wrote:
| 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 wrote:
| 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.
| skrebbel wrote:
| 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 wrote:
| Thanks! I'm glad you enjoyed the article and it could provide
| some useful insights.
| SinParadise wrote:
| 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.
| jib_flow wrote:
| I feel like the number of "I agree with everything except..."
| posts on this thread is an indication of what a great article
| this is.
|
| Practical, real world, and takes enough of a stand to wake up the
| pedants.
| losvedir wrote:
| 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.
| pmontra wrote:
| 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 wrote:
| Reminds me of the following from my Ruby/Rails days: https://
| api.rubyonrails.org/classes/ActiveSupport/HashWithIn...
| dnautics wrote:
| > 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 wrote:
| 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.
| bcrosby95 wrote:
| 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."
| dnautics wrote:
| > 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.
| keathley wrote:
| 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.
| mwcampbell wrote:
| > I'd also argue you shouldn't be doing side-effects like
| that in an init without a lot of care as well.
|
| I learned this the hard way. I wrote a GenServer that
| does some periodic database cleanup (maybe not the best
| solution), and I naively wrote the init function to
| immediately do the first cleanup on startup. Then I
| discovered a case off the happy path where the cleanup
| was failing due to a constraint violation, and the
| failing init brought down the whole application (edit:
| while someone was trying to use it, of course).
| jolux wrote:
| The scenario is not invented per se, it's something that
| really happens sometimes. Maybe the systems you interact
| with never return malformed data more than a few times,
| but the ones I work with do. Throwing an exception on a
| deserialization failure in order to crash your process
| just seems like punting on your retry logic to me.
| keathley wrote:
| Sorry, "invent" had the wrong connotations there. I just
| meant that there are scenarios where crashing wouldn't be
| acceptable. And in that case, sure, you should prefer to
| handle the error directly.
|
| But, allowing the process to crash doesn't punt retry
| logic at all. For instance, in the work scheduler
| example, the work scheduler would detect that a worker
| had crashed and would back off appropriately. This allows
| you to write your worker code in a way that avoids error
| handling while still managing faults.
| jolux wrote:
| The work scheduler example involves writing a work
| scheduler, though :). The logic is going to go somewhere,
| is what I'm saying.
| macintux wrote:
| 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 wrote:
| 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".
| mattbaker wrote:
| I think a lot of this is subjective and context dependent so the
| definitive "this is good" and "this is bad" language was a bit of
| a turnoff.
|
| BUT, I did like seeing someone's point of view with thoughtful
| explanations and examples, kind of like what you get out of a
| good discussion at an elixir meetup or something :D I enjoyed
| reading it.
| keathley wrote:
| Calling it "good and bad elixir" was probably overly charged.
| Its a little late to change it now, but something I'll consider
| in the future.
| acrispino wrote:
| I don't see how it's too late, unless HN commenters from
| today will be the only audience. You could also add an note.
| rubyn00bie wrote:
| 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.
| etxm wrote:
| 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.
| keathley wrote:
| 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.
| devoutsalsa wrote:
| 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.
___________________________________________________________________
(page generated 2021-06-10 23:00 UTC)