在Raku中实现函数对象“power”运算符

23
在APL中有一个强大的操作符,如果应用于函数f,则会叠加f的应用。如何在Raku中实现该运算符?
例如,使用以下定义的f
sub f(Int:D $i){ $i + 1 }

命令say (f ⍣ 4)(10);应该等同于say f(f(f(f(10))));

我下面的实现是针对一个参数的函数。

问题

  1. 如何扩展或替换以适用于多个(或任何)签名的更好实现?

  2. 如何定义新的幂运算符的“高优先级”?

  3. 是否有更好的方法来定义f ⍣ 0的“恒等函数”结果?

参考链接

这里是APL的的描述:“Power Operator”

是一个“带有两个点的星号”,或更正式地说,是“Apl Functional Symbol Star Diaeresis”。)

尝试

这是一种实现尝试:

sub infix:<⍣>( &func, Int:D $times where $times >= 0 ) {
    if $times == 0  {
      sub func2($d) {$d}
    } else {
      sub func2($d) {
        my $res = &func($d);
        for 2..$times -> $t { $res = &func($res) }
        $res
      }
    }
}

sub plus1(Int:D $i){ $i + 1 }

say 'Using (&plus1 ⍣ 0) over 4 :';
say (&plus1 ⍣ 0)(4);

say 'Using (&plus1 ⍣ 10) over 4 :';
say (&plus1 ⍣ 10)(4);

# Using (&plus1 ⍣ 0) over 4 :
# 4
# Using (&plus1 ⍣ 10) over 4 :
# 14

我按照这个教程页面进行了操作:https://docs.raku.org/language/optut

测试

Brad Gilbert的回答中提供的定义通过了以下所有测试。

use Test;

sub infix:<⍣> ( &func, UInt $repeat ) is tighter( &[∘] ) { [∘] &func xx $repeat }

proto plus1(|) {*}
multi plus1(Int:D $i){ $i + 1 }
multi plus1(Bool:D $b){ $b.Int + 1 }
multi plus1(Int:D $i, Bool:D $b){ $i + $b.Int + 1 }
multi plus1(Int:D $i, Int:D $j){ $i + $j + 1 }
multi plus1(@j){ ([+] @j) + 1}
multi plus1(Int:D $i, @j){ plus1($i) + plus1(@j) - 1 }
multi plus1(%h){ ([+] %h.values) + 1 }

plan 9;

is plus1([1, 3, 5, 3]), 13, 'plus1([1, 3, 5, 3])';

is plus1(3, [1, 3, 5, 3]), 16, 'plus1(3, [1, 3, 5, 3])';

is plus1(3, True), 5, 'plus1(3, True)';

is (&plus1 ⍣ 0)(4), 4, '(&plus1 ⍣ 0)(4)';

is (&plus1 ⍣ 10)(4), 14, '(&plus1 ⍣ 10)(4)';

is (&plus1 ⍣ 10)([1, 3, 5, 3]), 22, '(&plus1 ⍣ 10)([1, 3, 5, 3])';

is (&plus1 ⍣ 3)(4, True), 8, '(&plus1 ⍣ 3)(4, True)';

is (&plus1 ⍣ 3)(3, [1, 3, 5, 3]), 18, '(&plus1 ⍣ 3)(3, [1, 3, 5, 3])';

is (&plus1 ⍣ 3)({a => 1, b => 3, c => 5}), 12, '(&plus1 ⍣ 3)({a => 1, b => 3, c => 5})';

done-testing;

2
请注意,([+] @j) 可以写成 [+]( @j )]( 之间没有空格)。您也可以完全不使用括号来编写它,例如 1 + [+] @j[+] 1, |@j - Brad Gilbert
2个回答

17
  1. 如何扩展或替换该实现以适用于多个(或任何)签名?

Raku实际上有一个与APL的相似的运算符内置:列表重复运算符xx。 (毕竟,函数列表仍然是列表!)。使用xx,我们可以在一行中定义

sub infix:<⍣>(&fn, $count) { &{($^ω, |(&fn xx $count)).reduce({$^b($^a)})} };

然后按照您在&plus1示例中的方式使用它:

sub plus1(Int:D $i){ $i + 1 }

say 'Using (&plus1 ⍣ 0) over 4 :';
say (&plus1 ⍣ 0)(4);

say 'Using (&plus1 ⍣ 10) over 4 :';
say (&plus1 ⍣ 10)(4);

# Using (&plus1 ⍣ 0) over 4 :
# 4
# Using (&plus1 ⍣ 10) over 4 :
# 14

如果你想更加详细(支持一元和二元函数,并接受函数作为第二个操作数,就像你提到的 APL 一样),那么你可以将它扩展为类似于这样的代码:


#| Monadic repetition
multi infix:<⍣>(&fn:($), $count) { 
    &{($^ω, |(&fn xx $count)).reduce({$^b($^a)})} };
#| Dyadic repetition
multi infix:<⍣>(&fn, $i) { 
    sub (|c){(|c[0], |(&fn xx $i)).reduce: -> $sum, &f { f $sum, |c[*-1]}}};

#| "until" function operand
multi infix:<⍣>(&fn1, &fn2) {
    sub ($a, $b) { given fn1($a, $b) { when .&fn2($a) { $a }
                                       default        { .&?ROUTINE($b) }}}}

sub plus1(Int:D $i, Bool:D $b) { $i + $b.Int + 1 }
say (&plus1 ⍣ 6)(1, True);                  # OUTPUT: «13»

say (&{$^b + 1 ÷ $^a} ⍣ &infix:<≅>)(1, 1);  # OUTPUT: «1.618033989»
# equivalent to  1+∘÷⍣=1 ⍝ fixpoint: golden mean.

(请注意,您可以扩展第二个多功能以使用具有大于2的参数个数的函数,但您需要决定这些情况下需要什么语义。另请注意,Raku具有&infix:<∘>运算符,但它的作用与APL的不同,因为它不会传递调用参数。)

这唯一无法从Dyalog的幂运算符中获得的是提供负整数操作数以应用函数操作数的逆操作的能力。我们可以编写该多态,但是 Raku(像几乎所有非 APL 语言一样)没有像样地创建逆函数,因此似乎不太值得。

你可以使用is tighter/is equiv/is looser precedence traits指定优先级。我不确定您想要多高的优先级,但您基本上可以将其设置得尽可能高。

这自然而然地由将定义为约简来实现。

奖励Raku/APL信息:

如果您对Raku和APL都感兴趣,则可能想了解的内容不必回答您的问题:所有的Raku函数都可以以中缀形式调用,这使它们非常类似于APL的双元函数。因此,我们可以编写:

my &f = &plus1 ⍣ 6;
say 1 [&f] True;

say (&plus1 ⍣ 6)(1, True); 产生相同的效果。

此外,如果我理解正确,APL 动力算子最常使用的一种方式是通过使用布尔操作数进行“条件函数应用”。Raku 使用类似的习惯用法支持 &infix:<xx> 列重复运算符。因此,在 APL 中可能会出现这种情况:

SquareIfNegative ← { (*2⍣(⍵<0)) ⍵ }

在Raku中,可能是这样的:

my &square-if-negative = &{ $_² xx ($_ < 0)};

1
这是一个简洁而优雅的实现 - 谢谢!它能被修改以处理具有多个参数的函数吗?例如:plus1(Int:D $i, Bool:D $b){ $i + $b.Int + 1 } - Anton Antonov
2
@AntonAntonov 当然可以。我编辑了我的答案,加入了一个双元函数,我相信它的工作方式类似于 APL 版本。 - codesections
3
这是一个非常有启示性的答案。然而,提供的符号定义在我(更新后)发布的所有测试中都不通过。 - Anton Antonov

11

简短概述:

(太长了,没读完)
这个功能非常出色。

sub infix:<⍣> ( &func, UInt $repeat ) { [∘] &func xx $repeat }

重复组合

在数学中,乘法符号×类似于重复的加法+


2 × 5   =   2 + 2 + 2 + 2 + 2

在 Raku 中有其他的编写方式。

[+] 2,2,2,2,2
[+] 2 xx 5

Raku还有一个函数组合器,它很像函数加法.
(ASCII版本只是o。)

sub A (|) {…}
sub B (|) {…}
my \var = …;

A( B( var ))   eqv   (&A ∘ &B).(var)

以下是我们如何使用该运算符来处理您的代码的第一印象。

sub plus1(Int:D $i){ $i + 1 }

my &combined = &plus1 ∘ &plus1;

say combined 0; # 2

就像我们可以将乘法定义为实际上是重复的加法。

{
  sub infix:<×> ( $val, $repeat ) {
    &CORE:infix:<×>(
      [+]( $val xx $repeat.abs ), # <--
      $repeat.sign
    )
  }

  say 2 × 5;
}

我们可以对您的运算符执行相同的操作,用重复的函数组合来定义它。

sub infix:<⍣> ( &func, UInt $repeat ) {
  [∘] &func xx $repeat
}

sub plus1(Int:D $i){ $i + 1 }

say (&plus1 ⍣  1)(0); # 1
say (&plus1 ⍣ 10)(0); # 10

身份验证

我没有特别考虑$repeat等于0的情况。
但它仍然会做出一些合理的操作。


say (&plus1 ⍣ 0)(5); # 5

那是因为没有参数的形式 [∘]() 会返回一个函数,该函数将其输入不加修改地返回。
say (&plus1 ⍣ 0)('foobar'); # foobar

say [∘]().('foobar'); # foobar

基本上,身份结果已经实现了你想要的功能。


你可能会想知道当没有参数时,[∘] … 如何知道返回什么。

say ( [∘]      ).(4); # 4

事实上它确实不行,或者可以说[ ]无法实现,但 &infix:<∘> 可以。
.say for &infix:<∘>.candidates».signature;
# ()
# (&f)
# (&f, &g --> Block:D)

这就是两者都返回有意义结果的原因。

say [+]  ; # 0
say [×]  ; # 1

优先级

设置优先级的最佳方法是先确定哪个其他运算符具有类似的优先级。

我将以 为例来定义它。

  • You can set it to the same precedence level with is equiv. (Also sets the associativity to be the same.)

      sub sub infix:<⍣> ( &func, UInt $repeat ) is equiv( &infix:<∘> ) {…}
    

    Note that since Raku has a lot of places where you are referring to an existing infix operator there is a shorter way to refer to them.

      sub sub infix:<⍣> ( &func, UInt $repeat ) is equiv( &[∘] ) {…}
      #                                                   ^--^
    
  • You can set it to have a looser precedence level with is looser.

      sub sub infix:<⍣> ( &func, UInt $repeat ) is looser( &[∘] ) {…}
    
  • Or you can set it to have a tighter precedence level with is tighter.
    (Makes more sense in this case since × is tighter than +, so too should be tighter than .)

      sub sub infix:<⍣> ( &func, UInt $repeat ) is tighter( &[∘] ) {…}
    

默认的优先级与+运算符相同。


签名

至于函数签名,只是将每个结果传递给下一个函数。

sub A ( \a ) {
  a + 1
}
sub B ( \a, \b ) {
  a + b
}

say A(B(4, 5)); # 10

say (&A ∘ &B).(4, 5); # 10

假设 A 期望两个值,而 B 以列表形式提供了两个值。

sub A ( \a, \b ) {
  a + b
}
sub B ( \a, \b ) {
  a + b, 1
}

然后这行代码失败了。
它甚至都无法编译。

say A(B(4, 5));

这行代码并不会失败,实际上它返回了正确的值。

say (&A ∘ &B).(4, 5); # 10

如果您提供多个子集,它也会正常工作

因此总的来说,仅使用[∘]xx就可以完美地工作。

sub infix:<⍣> ( &func, UInt $repeat ) {
  [∘] &func xx $repeat
}

say (&plus1 ⍣ 3)(4);

实际上,您的操作符的代码长度可能比我们用来实现操作符的代码短了4个字符,但差别并不大。

say [∘](&plus1 xx 3)(4);

1
太好了,谢谢!你的实现通过了我为⍣制作的所有测试。(请参见我的更新问题。) - Anton Antonov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接