Adding an optional function argument to all() and any() builtins

I may be missing a standard idiom, but it seems to me that the any(...) and all(...) builtins are unnecessarily limited by requiring that the iterable they take as an argument is already in a form suitable for the intended kind of boolean comparison. Most of the time, when I want to check that any or all of a collection matches some test criterion, my iterable is not already in a valid form to pass to any() or all(), and so I either have to explicitly re-code a slightly modified version of the builtin, or wastefully use map() to apply my test to *all* the items in the list. This is only a few lines extra, but IMHO it would make these functions more useful and improve the readability of this common idiom if the test function could be supplied as an optional argument (which, if None, would default to the standard boolean comparison): For example, this unclear code: testval = False for i in mylist: if mytestfunction(i): testval = True break if testval: foo() or this wasteful (and still unclear, IMO) version: mylist_bool = map(mytestfunction, mylist) if any(mylist_bool): foo() would be replaced by the very natural form: if any(mylist, test=mytestfunction): foo() The "test" keyword arg is perhaps not the best name, I admit, but no other jumps prominently to mind ("match", "fn"?). I think this would be a nice feature, and would improve the expressivity of the language, much as the "key" optional arg to sort() does. Of course, maybe I just don't know the nice way to do these sorts of tests... please let me know if I missed something and am reinventing the wheel ;) Cheers, Andy

On Sun, Nov 21, 2010 at 3:20 PM, Masklinn <masklinn@masklinn.net> wrote:
In 2.7, both MRAB's generator expression solution and your imap() are improvements over map() because they don't create a temporary list. In 3.x, map = imap, but generator expression is more readable IMO.

On Sun, Nov 21, 2010 at 1:35 PM, Masklinn <masklinn@masklinn.net> wrote:
In many cases it will be though, because the gencomp is not eagerly evaluated, unlike listcomps and old map()s. gencomps are also definitely more memory efficient. Cheers, Chris

On 21/11/2010 22:03, Chris Rebert wrote:
if any(mytestfunction(i) for i in mylist): foo() wins hands down. Not only is it, I would guess, quite efficient, but it is immediately readable, in particular to newbies.* Whereas the readability of if any(map(mytestfunction, mylist)) depends on how familiar you are with map(). If you've never heard of it, it's quite impenetrable, as the name "map" is not IMHO very meaningful. (Not that I can think of anything better to call it, or that there is any chance of it being renamed. Still ... perhaps "apply_all" or some suc?) [* I hope I can be forgiven for speaking on behalf of newbies, when I don't - quite - class myself as one any more.] Best wishes Rob Cliffe

It is an improvement if there's no function. I think any(i > 3 for i in foo) is more readable than any(map(lambda i: i > 3, foo)) As to the original suggestion, the length of any(foo, test=bar) vs any(map(bar, foo)) is about the same. So I think this doesn't make anything simpler and unnecessarily adds complexity to any/all which then will inevitably lead to suggestions to make other things more complicated to match them. --- Bruce Learn about security: http://j.mp/gruyere-security On Sun, Nov 21, 2010 at 12:20 PM, Masklinn <masklinn@masklinn.net> wrote:

Masklinn <masklinn@masklinn.net> writes:
It is definitely an improvement by the criterion the OP mentioned, which was: Andy Buckley <andy@insectnation.org> writes:
-- \ “If [a technology company] has confidence in their future | `\ ability to innovate, the importance they place on protecting | _o__) their past innovations really should decline.” —Gary Barnett | Ben Finney

On 21/11/10 22:31, Ben Finney wrote:
Yes, at least syntactically: I had an odd blind spot here and never considered using a comprehension/generator expression! Neat. Still, Masklinn has suggested that this isn't necessarily performant even in Py 2.7 or 3.x, so there still appears to be a use-case for an extra argument to any/all. Or did I misunderstand the gencomp issue? It might be nice to add a suggestion of gencomp use to the documentation, as I'm sure there are others like me with (temporary) comprehension blind spots who think those functions have more limited applicability than is the case. Thanks, Andy

Andy Buckley wrote:
Yes, just like every function requires its input to already be in a form suitable for whatever the function expects to do. Just as any() and all() expect their argument to be truth-values (not necessarily True or False, but values to be inspected for their truthness), so sum() expects input consisting of numbers. Would you expect to be able to write this? mylist = ["a12", "b13", "c14", "d15"] sum(mylist, convert=lambda s: int(s[1:])) I hope not. Converting its input into a suitable format is not the responsibility of sum. sum's responsibility begins and ends with adding the input, any conversions should be done either externally: mylist = ["a12", "b13", "c14", "d15"] newlist = [int(s[1:]) for s in mylist] sum(newlist) or lazily using a generator expression as the argument to sum: mylist = ["a12", "b13", "c14", "d15"] sum(int(s[1:]) for s in mylist) There are variations on this using built-in map, itertools.imap, and similar. The problems with shifting the responsibility to any and all (or sum) instead of keeping it with the caller include: * It adds complexity to the API for all and any, and that makes the API harder to learn and harder to remember. * It duplicates functionality: now two more functions are expected to know how to accept a second argument and call it against the input before testing. * It can lead to "Jack of All Trades, Master of None" functions: oven.roast( chicken, add_stuffing="sage and onion", serve=["pre-dinner drinks", "appetizer", "main course", "desert"], wash_pots_afterward=False ) Why is the oven being asked to prepare the stuffing and serve the food? Now this is an extreme case, but any time you consider adding extra responsibility to a function, you're taking a step in that direction. There should be a clear benefit to the caller before accepting these extra costs. For example, list.sort(), sorted(), min() and max() now all take a key argument which is superficially similar to your suggested "test" argument. You would need to demonstrate similar benefit.
Re-coding all and any is usually a big No. That's almost always the wrong solution. In Python 3, map becomes a lazy iterator, which means it no longer applies the test to every item up front. In Python 2, the simple alternative is itertools.imap, which saves you from re-coding map: def lazymap(func, iterable): # Don't use this, use itertools.imap for obj in iterable: yield func(obj) But probably the best solution is to use a generator expression. # Like a list comp, except with round brackets. genexp = (mytestfunction(x) for x in mylist) any(genexp) This becomes even easier when you embed the generator expression directly in the function call: any(mytestfunction(x) for x in mylist) If mytestfunction is simple enough, you can do it in-place without needing to write a function: any(x >= 42 for x in mylist) [...]
The "test" keyword arg is perhaps not the best name, I admit, but no other jumps prominently to mind ("match", "fn"?).
When you have difficulty thinking of a name which clearly and concisely describes what the argument does, that's often a good hint that you are dumping too much responsibility onto the function. Having said that, I'd suggest that an appropriate name might be the same name used by sort and friends: key. -0 on the idea -- I don't find it compelling or necessary, but nor do I hate it. -- Steven

On Sun, Nov 21, 2010 at 1:12 PM, Steven D'Aprano wrote: When you have difficulty thinking of a name which clearly and concisely
I agree with almost everything you wrote, except the suggestion of "key". In sorted et al., the point of key is that you want a certain form used when comparing, but *not* the sorted list that is the final product. That's why you can't just use sorted(name.lower() for name in names) as a substitute for sorted(names, key=str.lower). Compare the following:
None of this applies to any/all, since they just return a True or a False, no matter what kind of iterable input they get. So, key is not a good name, and your earlier analysis of why there shouldn't be another keyword on any/all stands. -- Carl Johnson

On Sun, Nov 21, 2010 at 3:40 PM, Carl M. Johnson <cmjohnson.mailinglist@gmail.com> wrote:
-1 on adding such a keyword, for the reasons already brought up; but just to play Devil's Advocate, "predicate" (or perhaps "pred" or even "p" depending on how terse you want to be) would be a good name for the hypothetical parameter. Because one is testing whether any() or all() of the items in a collection satisfy a given predicate (in the trivial case, the "identity" bool() predicate). Cheers, Chris

On Sun, Nov 21, 2010 at 6:40 PM, Carl M. Johnson < cmjohnson.mailinglist@gmail.com> wrote:
However, if any/all acted like or/and and returned the last value examined (rather than simply True/False), then "key" would make sense. Then they turn into more useful selection tools. There have been several times when I wished that any/all acted like or/and because I needed the offending value for further use... -Bruce

On Nov 21, 2010, at 5:57 PM, Bruce Frederiksen wrote:
There have been several times when I wished that any/all acted like or/and because I needed the offending value for further use...
This is such a trivial function. Why not just roll your own and dump it in your personal utils? Raymond

Raymond Hettinger wrote:
In fairness, the same could be said about any() and all() in the first place. The advantages are that the built-ins are significantly faster than the pure-Python version (a factor of 3 times faster on my machine using Python 3.1), and they're available everywhere you go, without re-writing the functions or importing them. I'm (mildly) with Bruce on this one, but I think we missed the boat. Perhaps in Python4000 any() and all() should return the relevant object rather than True/False. -- Steven

On Sun, Nov 21, 2010 at 23:29, Steven D'Aprano <steve@pearwood.info> wrote:
That could be the source of some frightening, hard-to-detect regressions: if any([-1,0,1], test=is_even): # never taken, because 0 is returned instead of True -- Tim Lesher <tlesher@gmail.com>

On Mon, Nov 22, 2010 at 6:32 AM, Tim Lesher <tlesher@gmail.com> wrote:
Good point! The only way out of this for what I was wanting that I can think of becomes: first_even = next(filter(is_even, [-1, 0, 1]), None) In which case, I already have all of the tools that I need! I withdraw my suggestion. -Bruce

On Tue, Nov 23, 2010 at 1:58 AM, Bruce Frederiksen <dangyogi@gmail.com> wrote:
Again, the generator expression version will be more readable for most folks: first_even = next((x for x in seq if is_even(x)), None) Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 21/11/10 23:12, Steven D'Aprano wrote:
Ok, I like this argument: thanks for making it ;) If there weren't generator expressions, I would still argue for a keyword in this case, as any/all have usefulness on objects more general than one would pass to sum... but there are gen exps, so I'm more than happy. To reiterate, though, for those who like me have a blank day and fail to realise that generator expressions are a neat and powerful idiom in this case, I think it would be nice to mention this in their documentation. Thanks all, Andy

On Sun, Nov 21, 2010 at 3:34 PM, Andy Buckley wrote:
If you read the original blog post where GvR mentions any/all, he does so specifically in the context of making generator expressions more useful. :-) That said, probably very few Python users will ever read that post, so it should be better documented.

On Sun, Nov 21, 2010 at 17:56, Carl M. Johnson <cmjohnson.mailinglist@gmail.com> wrote:
But the blind spot of not thinking in terms of genexps is for "old" Python developers, not new ones who started off with them and probably don't use listcomps that often (I know I don't anymore). Documenting this specific suggestion leads down a rabbit hole of adding coding suggestions all over Python for how we feel something *should* be used when it isn't really our place to do so. But if someone wants to submit a patch to fix the docs another core developer might be willing to make the change other than me (I'm -0 on the change).

On Mon, 22 Nov 2010 01:34:57 +0000 Andy Buckley <andy@insectnation.org> wrote:
Yes; generally speaking, generator expressions should be mentioned in "official" doc along with every feature they are a useful parameter of, with a sensible example use case. Genexprs are extremely useful, but under-used in real code because they are somewhat new in the programming culture. Doc should help in making them more famous. Denis -- -- -- -- -- -- -- vit esse estrany ☣ spir.wikidot.com

On 11/21/2010 01:39 PM, Andy Buckley wrote:
This has the advantage that it doesn't need to iterate the whole list. Sometimes you can't beat a nice old fashion loop. ;-) I would write it like this... def test_any(iterable, test=bool): for i in iterable: if test(i): return True return False Then it would read fine in your program. if test_any(my_list, my_test_function): foo() Cheers, Ron

Ron Adam wrote:
Both any() and all() have lazy semantics and will stop as early as possible. They will only traverse the entire list if they have to. There's no need to write your own function to get that behaviour.
That would be more easily written as: def test_any(iterable, test=bool): return any(test(obj) for obj in iterable) But of course that raises the question of what advantage test_any() has over an inline call to any(test(obj) for obj in iterable). Aside: for what little it's worth, defaulting to bool as the test function causes a significant slow-down: [steve@sylar ~]$ python3 -m timeit -s "L=[0]*1000000" "any(L)" 10 loops, best of 3: 28 msec per loop [steve@sylar ~]$ python3 -m timeit -s "L=[0]*1000000" "any(bool(x) for x in L)" 10 loops, best of 3: 615 msec per loop In fairness, some of that slow-down is due to the overhead of the generator expression itself: [steve@sylar ~]$ python3 -m timeit -s "L=[0]*1000000" "any(x for x in L)" 10 loops, best of 3: 241 msec per loop but it's best to avoid calling bool() unless you absolutely need an actual bool. test_any should probably be written as: def test_any(iterable, test=None): if test: return any(test(obj) for obj in iterable) else: return any(iterable) although my care-factor is insufficient to actually bother timing it :) -- Steven

On Sun, Nov 21, 2010 at 3:20 PM, Masklinn <masklinn@masklinn.net> wrote:
In 2.7, both MRAB's generator expression solution and your imap() are improvements over map() because they don't create a temporary list. In 3.x, map = imap, but generator expression is more readable IMO.

On Sun, Nov 21, 2010 at 1:35 PM, Masklinn <masklinn@masklinn.net> wrote:
In many cases it will be though, because the gencomp is not eagerly evaluated, unlike listcomps and old map()s. gencomps are also definitely more memory efficient. Cheers, Chris

On 21/11/2010 22:03, Chris Rebert wrote:
if any(mytestfunction(i) for i in mylist): foo() wins hands down. Not only is it, I would guess, quite efficient, but it is immediately readable, in particular to newbies.* Whereas the readability of if any(map(mytestfunction, mylist)) depends on how familiar you are with map(). If you've never heard of it, it's quite impenetrable, as the name "map" is not IMHO very meaningful. (Not that I can think of anything better to call it, or that there is any chance of it being renamed. Still ... perhaps "apply_all" or some suc?) [* I hope I can be forgiven for speaking on behalf of newbies, when I don't - quite - class myself as one any more.] Best wishes Rob Cliffe

It is an improvement if there's no function. I think any(i > 3 for i in foo) is more readable than any(map(lambda i: i > 3, foo)) As to the original suggestion, the length of any(foo, test=bar) vs any(map(bar, foo)) is about the same. So I think this doesn't make anything simpler and unnecessarily adds complexity to any/all which then will inevitably lead to suggestions to make other things more complicated to match them. --- Bruce Learn about security: http://j.mp/gruyere-security On Sun, Nov 21, 2010 at 12:20 PM, Masklinn <masklinn@masklinn.net> wrote:

Masklinn <masklinn@masklinn.net> writes:
It is definitely an improvement by the criterion the OP mentioned, which was: Andy Buckley <andy@insectnation.org> writes:
-- \ “If [a technology company] has confidence in their future | `\ ability to innovate, the importance they place on protecting | _o__) their past innovations really should decline.” —Gary Barnett | Ben Finney

On 21/11/10 22:31, Ben Finney wrote:
Yes, at least syntactically: I had an odd blind spot here and never considered using a comprehension/generator expression! Neat. Still, Masklinn has suggested that this isn't necessarily performant even in Py 2.7 or 3.x, so there still appears to be a use-case for an extra argument to any/all. Or did I misunderstand the gencomp issue? It might be nice to add a suggestion of gencomp use to the documentation, as I'm sure there are others like me with (temporary) comprehension blind spots who think those functions have more limited applicability than is the case. Thanks, Andy

Andy Buckley wrote:
Yes, just like every function requires its input to already be in a form suitable for whatever the function expects to do. Just as any() and all() expect their argument to be truth-values (not necessarily True or False, but values to be inspected for their truthness), so sum() expects input consisting of numbers. Would you expect to be able to write this? mylist = ["a12", "b13", "c14", "d15"] sum(mylist, convert=lambda s: int(s[1:])) I hope not. Converting its input into a suitable format is not the responsibility of sum. sum's responsibility begins and ends with adding the input, any conversions should be done either externally: mylist = ["a12", "b13", "c14", "d15"] newlist = [int(s[1:]) for s in mylist] sum(newlist) or lazily using a generator expression as the argument to sum: mylist = ["a12", "b13", "c14", "d15"] sum(int(s[1:]) for s in mylist) There are variations on this using built-in map, itertools.imap, and similar. The problems with shifting the responsibility to any and all (or sum) instead of keeping it with the caller include: * It adds complexity to the API for all and any, and that makes the API harder to learn and harder to remember. * It duplicates functionality: now two more functions are expected to know how to accept a second argument and call it against the input before testing. * It can lead to "Jack of All Trades, Master of None" functions: oven.roast( chicken, add_stuffing="sage and onion", serve=["pre-dinner drinks", "appetizer", "main course", "desert"], wash_pots_afterward=False ) Why is the oven being asked to prepare the stuffing and serve the food? Now this is an extreme case, but any time you consider adding extra responsibility to a function, you're taking a step in that direction. There should be a clear benefit to the caller before accepting these extra costs. For example, list.sort(), sorted(), min() and max() now all take a key argument which is superficially similar to your suggested "test" argument. You would need to demonstrate similar benefit.
Re-coding all and any is usually a big No. That's almost always the wrong solution. In Python 3, map becomes a lazy iterator, which means it no longer applies the test to every item up front. In Python 2, the simple alternative is itertools.imap, which saves you from re-coding map: def lazymap(func, iterable): # Don't use this, use itertools.imap for obj in iterable: yield func(obj) But probably the best solution is to use a generator expression. # Like a list comp, except with round brackets. genexp = (mytestfunction(x) for x in mylist) any(genexp) This becomes even easier when you embed the generator expression directly in the function call: any(mytestfunction(x) for x in mylist) If mytestfunction is simple enough, you can do it in-place without needing to write a function: any(x >= 42 for x in mylist) [...]
The "test" keyword arg is perhaps not the best name, I admit, but no other jumps prominently to mind ("match", "fn"?).
When you have difficulty thinking of a name which clearly and concisely describes what the argument does, that's often a good hint that you are dumping too much responsibility onto the function. Having said that, I'd suggest that an appropriate name might be the same name used by sort and friends: key. -0 on the idea -- I don't find it compelling or necessary, but nor do I hate it. -- Steven

On Sun, Nov 21, 2010 at 1:12 PM, Steven D'Aprano wrote: When you have difficulty thinking of a name which clearly and concisely
I agree with almost everything you wrote, except the suggestion of "key". In sorted et al., the point of key is that you want a certain form used when comparing, but *not* the sorted list that is the final product. That's why you can't just use sorted(name.lower() for name in names) as a substitute for sorted(names, key=str.lower). Compare the following:
None of this applies to any/all, since they just return a True or a False, no matter what kind of iterable input they get. So, key is not a good name, and your earlier analysis of why there shouldn't be another keyword on any/all stands. -- Carl Johnson

On Sun, Nov 21, 2010 at 3:40 PM, Carl M. Johnson <cmjohnson.mailinglist@gmail.com> wrote:
-1 on adding such a keyword, for the reasons already brought up; but just to play Devil's Advocate, "predicate" (or perhaps "pred" or even "p" depending on how terse you want to be) would be a good name for the hypothetical parameter. Because one is testing whether any() or all() of the items in a collection satisfy a given predicate (in the trivial case, the "identity" bool() predicate). Cheers, Chris

On Sun, Nov 21, 2010 at 6:40 PM, Carl M. Johnson < cmjohnson.mailinglist@gmail.com> wrote:
However, if any/all acted like or/and and returned the last value examined (rather than simply True/False), then "key" would make sense. Then they turn into more useful selection tools. There have been several times when I wished that any/all acted like or/and because I needed the offending value for further use... -Bruce

On Nov 21, 2010, at 5:57 PM, Bruce Frederiksen wrote:
There have been several times when I wished that any/all acted like or/and because I needed the offending value for further use...
This is such a trivial function. Why not just roll your own and dump it in your personal utils? Raymond

Raymond Hettinger wrote:
In fairness, the same could be said about any() and all() in the first place. The advantages are that the built-ins are significantly faster than the pure-Python version (a factor of 3 times faster on my machine using Python 3.1), and they're available everywhere you go, without re-writing the functions or importing them. I'm (mildly) with Bruce on this one, but I think we missed the boat. Perhaps in Python4000 any() and all() should return the relevant object rather than True/False. -- Steven

On Sun, Nov 21, 2010 at 23:29, Steven D'Aprano <steve@pearwood.info> wrote:
That could be the source of some frightening, hard-to-detect regressions: if any([-1,0,1], test=is_even): # never taken, because 0 is returned instead of True -- Tim Lesher <tlesher@gmail.com>

On Mon, Nov 22, 2010 at 6:32 AM, Tim Lesher <tlesher@gmail.com> wrote:
Good point! The only way out of this for what I was wanting that I can think of becomes: first_even = next(filter(is_even, [-1, 0, 1]), None) In which case, I already have all of the tools that I need! I withdraw my suggestion. -Bruce

On Tue, Nov 23, 2010 at 1:58 AM, Bruce Frederiksen <dangyogi@gmail.com> wrote:
Again, the generator expression version will be more readable for most folks: first_even = next((x for x in seq if is_even(x)), None) Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 21/11/10 23:12, Steven D'Aprano wrote:
Ok, I like this argument: thanks for making it ;) If there weren't generator expressions, I would still argue for a keyword in this case, as any/all have usefulness on objects more general than one would pass to sum... but there are gen exps, so I'm more than happy. To reiterate, though, for those who like me have a blank day and fail to realise that generator expressions are a neat and powerful idiom in this case, I think it would be nice to mention this in their documentation. Thanks all, Andy

On Sun, Nov 21, 2010 at 3:34 PM, Andy Buckley wrote:
If you read the original blog post where GvR mentions any/all, he does so specifically in the context of making generator expressions more useful. :-) That said, probably very few Python users will ever read that post, so it should be better documented.

On Sun, Nov 21, 2010 at 17:56, Carl M. Johnson <cmjohnson.mailinglist@gmail.com> wrote:
But the blind spot of not thinking in terms of genexps is for "old" Python developers, not new ones who started off with them and probably don't use listcomps that often (I know I don't anymore). Documenting this specific suggestion leads down a rabbit hole of adding coding suggestions all over Python for how we feel something *should* be used when it isn't really our place to do so. But if someone wants to submit a patch to fix the docs another core developer might be willing to make the change other than me (I'm -0 on the change).

On Mon, 22 Nov 2010 01:34:57 +0000 Andy Buckley <andy@insectnation.org> wrote:
Yes; generally speaking, generator expressions should be mentioned in "official" doc along with every feature they are a useful parameter of, with a sensible example use case. Genexprs are extremely useful, but under-used in real code because they are somewhat new in the programming culture. Doc should help in making them more famous. Denis -- -- -- -- -- -- -- vit esse estrany ☣ spir.wikidot.com

On 11/21/2010 01:39 PM, Andy Buckley wrote:
This has the advantage that it doesn't need to iterate the whole list. Sometimes you can't beat a nice old fashion loop. ;-) I would write it like this... def test_any(iterable, test=bool): for i in iterable: if test(i): return True return False Then it would read fine in your program. if test_any(my_list, my_test_function): foo() Cheers, Ron

Ron Adam wrote:
Both any() and all() have lazy semantics and will stop as early as possible. They will only traverse the entire list if they have to. There's no need to write your own function to get that behaviour.
That would be more easily written as: def test_any(iterable, test=bool): return any(test(obj) for obj in iterable) But of course that raises the question of what advantage test_any() has over an inline call to any(test(obj) for obj in iterable). Aside: for what little it's worth, defaulting to bool as the test function causes a significant slow-down: [steve@sylar ~]$ python3 -m timeit -s "L=[0]*1000000" "any(L)" 10 loops, best of 3: 28 msec per loop [steve@sylar ~]$ python3 -m timeit -s "L=[0]*1000000" "any(bool(x) for x in L)" 10 loops, best of 3: 615 msec per loop In fairness, some of that slow-down is due to the overhead of the generator expression itself: [steve@sylar ~]$ python3 -m timeit -s "L=[0]*1000000" "any(x for x in L)" 10 loops, best of 3: 241 msec per loop but it's best to avoid calling bool() unless you absolutely need an actual bool. test_any should probably be written as: def test_any(iterable, test=None): if test: return any(test(obj) for obj in iterable) else: return any(iterable) although my care-factor is insufficient to actually bother timing it :) -- Steven
participants (17)
-
Alexander Belopolsky
-
Andy Buckley
-
Ben Finney
-
Brett Cannon
-
Bruce Frederiksen
-
Bruce Leban
-
Carl M. Johnson
-
Chris Rebert
-
Masklinn
-
MRAB
-
Nick Coghlan
-
Raymond Hettinger
-
Rob Cliffe
-
Ron Adam
-
spir
-
Steven D'Aprano
-
Tim Lesher