The clearest way(s) to check if a List contains...

There is more than one way to do it.Toby Inkster’s Creating your own Perl hits the nail on the head: with Perl you can choose the language that you code in

"So go on; create your own Perl. Make it your gift to yourself."

( Syntax::Collector makes it very simple, and will also help you bundle your “most used modules” - more useful modules in Toby’s article)

Today i’m going to explore one aspect of the Perl language:

how do you check that a list contains a given element?

Taking a concrete example, in english i would write:

do something if the ingredients list contains flour

I like to make my code “look” english, so it can be understood immediately. Your brain has trained for years to parse the english language and make sense of it: acknowledge this and turn it into an advantage. It’s also your best way to make your code readable for others.

So, how do we do it in Perl and how easy is that to read?

do_something if 'flour' ~~ @ingredients

Read: “do something if ‘flour’ IS IN the ingredients list” => two worms for two 2-letter words.
That might be what you’re looking for: it is short and clear, and it’s core since 5.10. I personally don’t use it, i prefer self-explanatory code. It’s up to you.
Edit: Smart Match is broken, you probably don’t want to use it (thank you for your comments, educated_foo and J. Berger).

What else does Perl have to offer ?

do_something if first {$_ eq 'flour'} @ingredients

with List::MoreUtils’s first() subroutine (but you probably want to import is through Util::Any instead). I hate it. That’s the worst possible way to do it.

“Is there a first element that’s found in the list of ingredients, and which is flour?”.

This doesn’t mean anything, it’s just confusing. Why would you do this to yourself and others when you can speak a lot more clearly? I’ve decided to put it in this list, because i have seen it in several answers on forums. Don’t do that. Instead, do:

do_something if any {$_ eq 'flour'} @ingredients

which is more explicit : you want to check whether any ingredient is flour. That might be what you want to use in production code: any { } is used enough that any Perl programmer will understand it immediately.

If for some reason you don’t want to import List::MoreUtils or Util::Any, there’s :

do_something if grep {$_ eq 'flour'} @ingredients

It’s clear, and it’s core. The main advantage is no-dependency. But don’t forget that it’s slower than any(), as it will always loop through every element of the list. It was benchmarked here.

I still don’t love it. I don’t like to have to read the name of the list at the end of the line, and then come back to the middle. I prefer writing something that is more straightforward to read:

do_something if any(@ingredients) eq 'flour'

with

use syntax 'junction';

(which activates the Syntax::Feature::Junction syntax extension - check out the other Syntax::Feature extensions)

“Do something if any ingredient is flour”

Perfect. And it comes from Perl 6, thank you :-) This is my personal favorite of all possibilities. But possibly not best in production code, as it is less common. If you put this in production code, other programmers will be surprised: they will lose time because of novelty, rather than gain time thanks to readability: “Why does ‘any’ not behave as usual?”

I have yet another proposal, if you are feeling even more adventurous :

do_something if @ingredients->contains('flour')

What this code does is obvious, because we are using the appropriate verb.
In my opinion it’s the most explicit of all solutions. It’s the only one that says what it does and why it’s there.
Unfortunately this method doesn’t exist in any CPAN module, but it’s easy to implement:

use syntax 'junction';
use autobox;
sub ARRAY::contains {   any( @{$_[0]} ) eq $_[1]   };

Shall i make a pull request to autobox::Core, for people who like me feel that it is missing?

Summary :

Now, let’s look back over the various solutions Perl has to offer :

do_something if 'flour' ~~ @ingredients   # ~~ operand.   BEWARE: it is broken.

do_something if grep {$_ eq 'flour'} @ingredients # grep (slower than 'any')

do_something if any {$_ eq 'flour'} @ingredients # List::MoreUtils / Util::Any

do_something if any(@ingredients) eq 'flour'   # use syntax 'junction';

do_something if @ingredients->contains('flour')   # added with autobox

So, which one is your favorite?

~ ~ ~

Going on a tangent: i have read that every time you need to search through a list, you should consider whether it should have been a hash in the first place. I’d like to read a clear explanation on how to best make this choice. Can anyone think of an explanation or a reference?

~ ~ ~

PS: is there a simple way to include links to CPAN modules in this blog? I looked in the Movable Type help section but didn’t find anything

~ ~ ~

Edit: i added the ~~ operator, after echowuhao’s comment.

18 Comments

$a ~~ @list should also work.
search through a list the time is N, but check on hash is constant time.
correct me if I am wrong. Thanks.

totally agree!
I do not know there is so many ways in perl to check if a elem exists in list. Thanks.

Since smart match is broken and changes between versions, grep ($_ eq 'flour') seems like the best way to me.

In Perl 5.18 smartmatch and given/when will be marked as experimental, this is not a good sign for using them in new code. https://metacpan.org/module/RJBS/perl-5.17.11/pod/perldelta.pod#Incompatible-Changes

Because 'flour' is converted to a regex, and matches e.g. 'afflourent'.

That person also says it works differently in 5.10, 5.12, and 5.14, which doesn't exactly inspire confidence.

Smart match in 5.10.0 was pretty broken. From 5.10.1 to 5.18, the behaviour has been fairly consistent, with the exception of a couple of bugs around tainting and array slices being fixed.

From 5.18, if warnings are enabled, using smart match will trigger a warning that the feature is still experimental.

Hopefully any future changes to smart match will attempt to retain the current behaviour if they see an explicit "use v5.14" or similar

The syntax I'd like is the following, which would also be the easiest to understand, even for someone who is new to perl:

  do_something() if 'flour' in @ingredients;

The smart match operator is closest, but there are various issues with smart match, as others have noted.

All variant is bad, good variant is:
do_something if 'flour' in @ingredients
but I don't now how make it

From 5.18, if warnings are enabled, using smart match will trigger a warning that the feature is still experimental.
Yeah, that's really annoying. I had to use Carp::Always to figure out where those warnings were coming from in one of my scripts. Turns out it was Perl6::Junction way down in the stack. PITA.

Leave a comment

About mascip

user-pic Perl, Javascript (AngularJS), Coffeescript, Firebase