Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perl 6 calculate the average of an int array at once using reduce

I'm trying to calculate the average of an integer array using the reduce function in one step. I can't do this:

say (reduce {($^a + $^b)}, <1 2 3>) / <1 2 3>.elems;

because it calculates the average in 2 separate pieces.

I need to do it like:

say reduce {($^a + $^b) / .elems}, <1 2 3>;

but it doesn't work of course.

How to do it in one step? (Using map or some other function is welcomed.)

like image 997
Terry Avatar asked Jul 25 '19 19:07

Terry


2 Answers

TL;DR This answer starts with an idiomatic way to write equivalent code before discussing P6 flavored "tacit" programming and increasing brevity. I've also added "bonus" footnotes about the hyperoperation Håkon++ used in their first comment on your question.5

Perhaps not what you want, but an initial idiomatic solution

We'll start with a simple solution.1

P6 has built in routines2 that do what you're asking. Here's a way to do it using built in subs:

say { sum($_) / elems($_) }(<1 2 3>); # 2

And here it is using corresponding3methods:

say { .sum / .elems }(<1 2 3>); # 2

What about "functional programming"?

First, let's replace .sum with an explicit reduction:

.reduce(&[+]) / .elems

When & is used at the start of an expression in P6 you know the expression refers to a Callable as a first class citizen.

A longhand way to refer to the infix + operator as a function value is &infix:<+>. The shorthand way is &[+].

As you clearly know, the reduce routine takes a binary operation as an argument and applies it to a list of values. In method form (invocant.reduce) the "invocant" is the list.

The above code calls two methods -- .reduce and .elems -- that have no explicit invocant. This is a form of "tacit" programming; methods written this way implicitly (or "tacitly") use $_ (aka "the topic" or simply "it") as their invocant.

Topicalizing (explicitly establishing what "it" is)

given binds a single value to $_ (aka "it") for a single statement or block.

(That's all given does. Many other keywords also topicalize but do something else too. For example, for binds a series of values to $_, not just one.)

Thus you could write:

say .reduce(&[+]) / .elems given <1 2 3>; # 2

Or:

$_ = <1 2 3>;
say .reduce(&[+]) / .elems; # 2

But given that your focus is FP, there's another way that you should know.

Blocks of code and "it"

First, wrap the code in a block:

{ .reduce(&[+]) / .elems }

The above is a Block, and thus a lambda. Lambdas without a signature get a default signature that accepts one optional argument.

Now we could again use given, for example:

say do { .reduce(&[+]) / .elems } given <1 2 3>; # 2

But we can also just use ordinary function call syntax:

say { .reduce(&[+]) / .elems }(<1 2 3>)

Because a postfix (...) calls the Callable on its left, and because in the above case one argument is passed in the parens to a block that expects one argument, the net result is the same as the do4 and the given in the prior line of code.

Brevity with built ins

Here's another way to write it:

<1 2 3>.&{.sum/.elems}.say; #2

This calls a block as if it were a method. Imo that's still eminently readable, especially if you know P6 basics.

Or you can start to get silly:

<1 2 3>.&{.sum/$_}.say; #2

This is still readable if you know P6. The / is a numeric (division) operator. Numeric operators coerce their operands to be numeric. In the above $_ is bound to <1 2 3> which is a list. And in Perls, a collection in numeric context is its number of elements.

Changing P6 to suit you

So far I've stuck with standard P6.

You can of course write subs or methods and name them using any Unicode letters. If you want single letter aliases for sum and elems then go ahead:

my (&s, &e) = &sum, &elems;

But you can also extend or change the language as you see fit. For example, you can create user defined operators:

#| LHS ⊛ RHS.
#| LHS is an arbitrary list of input values.
#| RHS is a list of reducer function, then functions to be reduced.
sub infix:<⊛> (@lhs, *@rhs (&reducer, *@fns where *.all ~~ Callable)) {
  reduce &reducer, @fns».(@lhs)
}
say <1 2 3> ⊛ (&[/], &sum, &elems); # 2

I won't bother to explain this for now. (Feel free to ask questions in the comments.) My point is simply to highlight that you can introduce arbitrary (prefix, infix, circumfix, etc.) operators.

And if custom operators aren't enough you can change any of the rest of the syntax. cf "braid".

Footnotes

1 This is how I would normally write code to do the computation asked for in the question. @timotimo++'s comment nudged me to alter my presentation to start with that, and only then shift gears to focus on a more FPish solution.

2 In P6 all built in functions are referred to by the generic term "routine" and are instances of a sub-class of Routine -- typically a Sub or Method.

3 Not all built in sub routines have correspondingly named method routines. And vice-versa. Conversely, sometimes there are correspondingly named routines but they don't work exactly the same way (with the most common difference being whether or not the first argument to the sub is the same as the "invocant" in the method form.) In addition, you can call a subroutine as if it were a method using the syntax .&foo for a named Sub or .&{ ... } for an anonymous Block, or call a method foo in a way that looks rather like a subroutine call using the syntax foo invocant: or foo invocant: arg2, arg3 if it has arguments beyond the invocant.

4 If a block is used where it should obviously be invoked then it is. If it's not invoked then you can use an explicit do statement prefix to invoke it.

5 Håkon's first comment on your question used "hyperoperation". With just one easy to recognize and remember "metaop" (for unary operations) or a pair of them (for binary operations), hyperoperations distribute an operation to all the "leaves"6 of a data structure (for an unary) or create a new one based on pairing up the "leaves" of a pair of data structures (for binary operations). NB. Hyperoperations are done in parallel7.

6 What is a "leaf" for a hyperoperation is determined by a combination of the operation being applied (see the is nodal trait) and whether a particular element is Iterable.

7 Hyperoperation is applied in parallel, at least semantically. Hyperoperation assumes8 that the operations on the "leaves" have no mutually interfering side-effects -- that is to say, that any side effect when applying the operation to one "leaf" can safely be ignored in respect to applying the operation to any another "leaf".

8 By using a hyperoperation the developer is declaring that the assumption of no meaningful side-effects is correct. The compiler will act on the basis it is, but will not check that it is true. In the safety sense it's like a loop with a condition. The compiler will follow the dev's instructions, even if the result is an infinite loop.

like image 137
raiph Avatar answered Oct 28 '22 19:10

raiph


Here is an example using given and the reduction meta operator:

given <1 2 3> { say ([+] $_)/$_.elems } ;
like image 44
Håkon Hægland Avatar answered Oct 28 '22 18:10

Håkon Hægland