loading...

How to Utilize Enum.any?, with a Refactoring Twist!

noelworden profile image Noel Worden Updated on ・3 min read

This week I didn't have any mind-blowing revelations, but those are like unicorns, I'm fortunate when I come across them, but I know that it wont happen all the time. I did -through a colleague's PR comment- discover another Elixir function that will make my future life a little bit easier, Enum.any?/2.

I was back in the world of custom validations, and they were getting more deeply nested than I liked, but I didn't see any way to make them more concise.

There's a lot going on here, but let's focus on the case statements that are being executed. With this particular validation the error shown to the user is the same in the case of all the falses, in hindsight that's a clue that this could be cleaned up.

When it's all said and done I need to check if total_merch_rev minus total_group_costs equals net_revenue_shared. But the fields coming in are not in the exact same shape, and 0 != 0.00. I first have to explicitly check if the fields are zero, and if not, then check if total_merch_rev minus total_group_costs equals net_revenue_shared?

That logic would look something like this:

This is where Enum.any?/2 comes into play, as it invokes the function for each item in the collection returning true if at least one invocation returns true, and false otherwise. Basically both of those chunks of logic can be dropped into that function, and if either return true, the changeset would be returned, and if either return false the error would be returned. So, the refactor would look something like this:

The rev_minus_cost == @zero_with_decimal && net_rev == @zero_with_decimal line is equivalent to:

And, the rev_minus_cost == net_rev line covers this case:

But, as I write this Im seeing how this can be refactored further and not even use Enum.any?/2! Basically, the problem I was originally having was how the data was coming in, versus the result of the mathematical logic. Again, it all came down to the fact that 0 != 0.00. But by rounding both the total_merch_rev minus total_group_costs result and net_revenue_shared data to the hundredth place, I'm essentially normalizing the data, and it can be compared to itself directly, even if the logic result or field is 0.

So, the result of that refactor is this:

Well, I have to say, this is pretty awesome. While revisiting past work to write about a function, I see a way to refactor that code even further while writing about it, and ultimately refactor it in a way that it doesn't even need the function that the post is highlighting. It's great that deeply nested logic can be cleaned up so efficiently with Enum.any?/2 (along with the related function Enum.all?/2), but I'm actually pretty happy that I was able to removed the need for it in this particular example.


This post is part of an ongoing This Week I Learned series. I welcome any critique, feedback, or suggestions in the comments.

Discussion

pic
Editor guide
Collapse
wulymammoth profile image
David

Nice post, Noel!

I, too, have seen Elixir code that's gone awry with a bunch of nested conditions. It's usually a code smell whether we're operating in a functional programming or not.

Enum.any? def looks like it helps in flattening. Another thing that I've found useful, and maybe you will as well is with special form. I rely it on quite a bit when the pipe operator is not applicable, because the functions all return an ok/error-tuple (i.e. {:ok, _} / {:error, _}) or don't handle them. I love Drew Olsen's post on the topic here

And lastly, I think you spotted what part of the issue was -- ensuring that the data-types match. Dealing with numbers and money can be hairy business. I've used the the money library in the past and actually just included in a new project that I started - github.com/elixirmoney/money. I typically also perform all data clean-up in the controller before they ever enter any business domain code (which also includes validations). I ensure that maps are converted to structs (never handling data that isn't well-formed and no maps with string keys unless its metadata), decimals that are supposed to represent money to currency structs (via the money library) all before pushing them through changeset validations. I'll let the consumer/client know that their request is malformed and return 4xx if it isn't structured correctly. By the time it reaches the changeset validations, the datatypes should be correct and I can run validations without performing data-type transformation from one type to another just to compare them. I'm free to just just validate whether the values are valid or not.

Collapse
noelworden profile image
Noel Worden Author

Hey David,

Thanks for pointing me to the with expression. I actually wrote a post of my own here. I am by no means an expert, but just writing the post helped me get a better handle on all the possibilities.

A big caveat we have in the project I'm working on is when/if to do the rounding. I ended up rolling my own module to simply add the $ and commas in the correct positions, since ultimately that's all we needed.

Thanks for reaching out, I appreciate the feedback and advice!

Collapse
wulymammoth profile image
David

Ah! Totally missed that! Hard to go back to Ruby after all this jazz, huh?