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