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