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