Perl 6参数传递之美

Perl 6尚未完成,但您已经可以玩弄它。我希望这篇文章能鼓励您尝试它。首先安装Pugs,这是一个用Haskell实现的Perl 6编译器。请注意,您还需要Haskell(有关如何获取Haskell的说明,请参阅Pugs的INSTALL文件中的说明)。

当然,Pugs还没有完成。它不可能完成。Perl 6的设计仍在进行中。然而,Pugs仍然具有许多即将将我们喜爱的语言变成更伟大的事物的关键特性。

一个简单的脚本

我将要承担一个很大的风险。我将向您展示一个执行牛顿法的脚本。请在开始之前不要放弃。

艾萨克·牛顿是一位著名的计算机科学家,同时也是一位天文学家、物理学家和数学家,正如ACM通讯所描述的那样。他和其他人开发了一种相当简单的方法来找到平方根。它是这样的

    #!/usr/bin/pugs
    use v6;

    my Num  $target = 9e0;
    my Num  $guess  = $target;

    while (abs( $guess**2 - $target ) > 0.005) {
        $guess += ( $target - $guess**2 ) / ( 2 * $guess );

        say $guess;
    }

这个版本总是找到9的平方根,幸运的是,它是3。这有助于测试,因为我不必记住一个更有趣的平方根,例如,2的平方根。当我运行这个时,输出是

    5
    3.4
    3.0235294117647058823529411764705882352941
    3.0000915541313801785305561913481345845731

最后一个数字是9的平方根,精确到小数点后三位。

这就是发生的事情。

一旦安装了Pugs,您就可以在shebang行中使用它(至少在Unix或Cygwin上是这样)。否则,像使用perl一样通过pugs调用脚本。

$ pugs newton

为了让Perl 6知道我想要Perl 6而不是Perl 5,我输入use v6;

在Perl 6中,基本原始类型仍然是标量、数组和散列表。还有更多类型的标量。在这种情况下,我使用浮点类型Num作为目标(我想要其平方根的数字)和猜测(我希望它将逐步改进,直到它是目标的确切平方根)。我可以在Perl 5中使用这种语法。在Perl 6中,它将成为规范(或者我希望是这样)。我使用了my来限制变量的作用域,就像在Perl 5中一样。

牛顿法总是需要一个猜测。不进行解释,我将说对于平方根来说,猜测几乎没有区别。为了简单起见,我猜测了数字本身。显然,这不是一个好的猜测,但最终它是有效的。

while循环将继续,直到猜测的平方接近目标。多接近由我来决定。我选择了.005,以提供大约三位精度。

在循环内部,代码使用牛顿公式在每一步改进猜测。我不会对此进行过多解释。(我已经抵制了数学老师时期的强烈诱惑,想要解释得更多。很高兴我抵制了。但如果您好奇,请查阅微积分教科书。或者更好的是,给我发电子邮件。我很乐意说更多!)我很快将展示该方法的一种更通用形式,这可能会唤醒听众中微积分爱好者的记忆,或者不会。

最后,在每次迭代的末尾,我使用say来打印答案。这比写print "$guess\n";要好。

除了使用say和声明数字的类型为Num之外,上述脚本与我在Perl 5中可能编写的脚本没有太大的区别。这是可以的。它即将变得更加Perl 6风格。

一个导出模块

虽然有一个可以找到平方根的脚本很好,但最好在几个方面进行泛化。一个很好的改变是将其变成一个模块,这样其他人就可以共享它。另一个是利用牛顿的力量来寻找其他类型的根,如立方根和其他更奇特类型的根。

首先,我将上面的脚本转换成一个导出newton子模块的模块。然后,我将处理方法的泛化。

完成后,我希望能够像这样使用该模块

    #!/usr/bin/pugs

    use Newton;

    my $answer = newton(4);

    say $answer;

因为say非常有用,我可以合并最后两个语句

        say "{ newton(4) }";

没错,如果你把字符串放在大括号里,它就会执行代码。

模块Newton.pm看起来是这样的

    package Newton;
    use v6;

    sub newton(Num $target) is export {
        my Num  $guess  = $target;

        while (abs( $guess**2 - $target ) > 0.005) {
            $guess += ( $target - $guess**2 ) / ( 2 * $guess );
        }

        return $guess;
    }

这里开始的是从Perl 5借用的熟悉的包声明。(在Perl 6本身中,package用于标识Perl 5源代码。该v6模块允许你在Perl 5程序中运行一些Perl 6代码。)紧接着是use v6;,就像原始脚本中一样。

在Perl 6中声明子例程不必与Perl 5不同,但应该这样做。这个声明说它需要一个名为target的数字变量。这样的真正的原型允许Perl 6在调用子例程时报告错误的参数错误。这一步将Perl 6移至许多大型应用开发商店可能的语言列表中。

在声明的末尾,就在主体大括号之前,我包含了is export。这会将newton放入使用该模块的人的命名空间中(至少,如果他们以正常方式使用该模块的话;他们可以明确拒绝导入)。没有必要显式使用Exporter并设置@EXPORT或其类似物。

其余的代码相同,只是它返回答案,并且在每次迭代中不再宣布其猜测。

指定默认值

将真正的、编译器强制的参数添加到子例程声明中是Perl的一个巨大进步。对于许多人来说,Perl 5中的这种特定宽松性使其无法进入关于项目使用哪种语言的讨论。我在上一份工作中亲身体验了这一不幸的现实。然而,Perl 6中的声明还有很多。

假设我想让调用者控制方法的准确性,但又想提供一个合理的默认值,如果调用者不想考虑一个好的值的话。我可能会写

    package Newton;
    use v6;

    sub newton(
        Num  $target,
        Num  :$epsilon = 0.005,  # note the colon
        Bool :$verbose = 0,
    ) is export {
        my Num  $guess  = $target;

        while (abs( $guess**2 - $target ) > $epsilon ) {
            $guess += ( $target - $guess**2 ) / ( 2 * $guess );
                    say $guess if $verbose;
        }

        return $guess;
    }

在这里,我引入了两个新的可选参数:$verbose,用于决定是否在每一步打印(默认是不打印)和$epsilon,这是我们数学类型经常用来表示容差的花体希腊字母。

虽然调用者可能像以前一样使用它,但她现在有了选项。她可以说

    my $answer = newton(165, verbose => 1, epsilon => .00005);

这提供了额外的精度,并且会在每次迭代时打印值(这会在迭代和驱动脚本中打印最后迭代两次:一次在循环中,一次在驱动脚本中)。请注意,命名参数可以按任何顺序出现。

做出假设

最后,牛顿法不仅可以找到平方根。为了使其泛化,需要更多的工作和一些额外的数学(我会再次将它们放在一边)。

提供想要找到根的函数很容易。例如,平方函数可以是

        sub f(Num $x) { $x**2 }

然后,在循环的更新行中,写

    $guess += ( $target - f($guess) ) / ( 2 * $guess );

改变f会改变你寻找的根。

问题是除号右边。对于关心的人来说,2 * $guess取决于函数(它是导数)。我可以要求调用者提供这个,就像这样

        sub fprime(Num $x) { 2 * $x }

然后更新将是

    $guess += ( $target - f($guess) ) / fprime($guess);

这种方法有两个问题。首先,你需要一种方法让调用者将这些函数传递给子例程。实际上这很简单;只需将类型为Code的参数添加到列表中

    sub newton(
        Num  $target,
        Code $f,
        Code $fprime,
        Num  :$epsilon = 0.005,
        Bool :$verbose = 0,
    ) is export {

第二个问题是调用者可能不知道如何计算$fprime。也许我应该将微积分作为使用模块的先决条件,但这可能会吓跑一些潜在用户。我想提供一个默认值,但这取决于函数是什么。如果我知道$f是什么,我可以为用户提供$fprime的估计值。

Perl 6正好提供了这种能力。以下是这个模块的最终版本,一次一点点

    package Newton;

    use v6;

这没什么新奇的。

    sub approxfprime(Code $f, Num $x) {
        my Num $delta = 0.1;
        return ($f($x + $delta) - $f($x - $delta))/(2 * $delta);
    }

对于那些关心的人来说(肯定至少有一个人关心),这是一个二阶中心差分。对于那些不关心的人来说,它是一个适合在newton子程序中使用的近似值。它接受一个函数和一个数字,并返回所需的除法值的估计。

    sub newton(
        Num  $target,
        Code $f,
        Code :$fprime         = &approxfprime.assuming( f => $f ),
        Num  :$epsilon        = 0.0005,
        Bool :$verbose        = 0,
    ) returns Num is export {
        my Num $guess  = $target / 2;

        while (abs($f($guess) - $target) > $epsilon) {

            $guess += ($target - $f($guess)) / $fprime($guess);

            say $guess if $verbose;
        }
        return $guess;
    }

使用此程序的脚本可能非常简单

    #!/usr/bin/pugs

    use Newton;

    sub f(Num $x) { return $x**3 }

    say "{ newton(8, \&f, verbose => 1, epsilon => .00005) }";

请注意,调用者必须提供函数f。示例中的函数是用于立方根的。

如果调用者提供了导数作为fprime,我会使用它。否则,就像示例中那样,我会使用approxfprime。与调用者提供的fprime不同,它需要一个数字和一个函数,而approxfprime需要一个数字和一个函数。需要的函数是调用者传递给newton的函数。你怎么传递它呢?Currying——即一次性提供函数的一个或多个参数,然后使用简化后的版本。

在Perl 6中,你可以通过在函数名称前放置子签名&来获取子程序的引用(前提是它在作用域内)。要currying,请在末尾添加.assuming并在括号中提供一个或多个参数值。所有这些都是说起来比做起来难。

    Code :$fprime         = &approxfprime.assuming( f => $f ),

此代码意味着调用者可能提供值。如果是这样,请使用它。否则,使用approxfprime并使用调用者的函数代替f

结论

Perl 6的调用约定设计得非常好。它不仅允许编译时参数检查,还允许使用具有或没有复杂默认值的命名参数,甚至包括curried默认函数。这将非常强大。事实上,在Pugs中,它已经如此。

在Pugs的examples/algorithms/目录中,有一个比这篇文章中示例更详细的版本。它被称为Newton.pm

免责声明

虽然很难说出这一点,但如果你需要重负载数值计算,不要使用纯Perl编写代码。而是使用FORTRAN、C或PDL。而且要小心。数值计算充满了意外陷阱,这可能导致性能不佳或结果完全错误。不幸的是,牛顿法在一般情况下是臭名昭著的风险。当对数值有疑问时,像我一样咨询该领域的专业人士。

标签

反馈

这篇文章有什么问题?请在GitHub上打开一个issue或pull request来帮助我们GitHub