第十七天 - 通往幸福的编译之路

Compiling our way to happiness

第17天 - 通往幸福的编译道路

如果我们选择接受它,我们的任务就是解决SEND + MORE = MONEY代码中的问题。不,请等一下,让我这样说吧:

    S E N D
+   M O R E
-----------
  M O N E Y

它意味着相同,但是这样放置它更具视觉冲击力,特别是因为我们很多人在学校这样做了。

基本规则很简单。

  • 每个字母代表0到9之间的数字。
  • 字母代表不同的数字; 两个字母可能不共享相同的数字。
  • 前导数字(在我们的拼图中,SM)不能为零。如果为零他们就不会是前导数字!

鉴于这些限制因素,上述难题有一个独特的解决方案。

我鼓励你找到解决方案。写一点代码,坚持一下!在这篇文章中,我们会这样做,但后来(关键)不满足于此,并最终陷入嵌套玩偶的情况,其中代码编写代码直到出现真正整洁的东西。结论将拼出最终目标-坚持不住了,我被实时地通过多个委员会获悉,正确的说法是“ 一个终极愿景” -为了 Raku。

我们开工吧。

Marcus Junius Brute Force(The Younger)

我们当天的第一语言及其相应的解决方案是 Raku 本身。这里没有技巧; 我们只是像愤怒的公牛一样向问题域冲去,尝试一切。事实上,我们确保不要在这个问题上耍小聪明,只是尝试尽可能直接地表达解决方案。

for 0..9 -> int $d {
    for 0..9 -> int $e {
        next if $e == $d;

        my int $y = ($d + $e) % 10;
        my int $_c1 = ($d + $e) div 10;

        for 0..9 -> int $n {
            next if $n == $d;
            next if $n == $e;
            next if $n == $y;

            for 0..9 -> int $r {
                next if $r == $d;
                next if $r == $e;
                next if $r == $y;
                next if $r == $n;

                next unless ($_c1 + $n + $r) % 10 == $e;
                my int $_c2 = ($_c1 + $n + $r) div 10;

                for 0..9 -> int $o {
                    next if $o == $d;
                    next if $o == $e;
                    next if $o == $y;
                    next if $o == $n;
                    next if $o == $r;

                    next unless ($_c2 + $e + $o) % 10 == $n;
                    my int $_c3 = ($_c2 + $e + $o) div 10;

                    for 1..9 -> int $s {
                        next if $s == $d;
                        next if $s == $e;
                        next if $s == $y;
                        next if $s == $n;
                        next if $s == $r;
                        next if $s == $o;

                        for 1..9 -> int $m {
                            next if $m == $d;
                            next if $m == $e;
                            next if $m == $y;
                            next if $m == $n;
                            next if $m == $r;
                            next if $m == $o;
                            next if $m == $s;

                            next unless ($_c3 + $s + $m) % 10 == $o;
                            my int $_c4 = ($_c3 + $s + $m) div 10;

                            next unless $_c4 % 10 == $m;

                            say "$s$e$n$d + $m$o$r$e == $m$o$n$e$y";
                        }
                    }
                }
            }
        }
    }
}

你又看到了,它不漂亮,但它起作用了。这是你母亲警告你的那种缩进程度。不过,如果你问我,我更讨厌缩进。对于我们需要扫描其搜索空间的每个变量,我们都有一个。(只有有Y我们才能走捷径。)

虽然这是今天猛烈冲击的迂回而已,但MJD曾在博客上发表过关于此事的博客,然后我也在博客上写了这篇文章。从某种意义上说,这些博客文章非常关注“删除缩进”。今天的帖子是我三年后的想法。

我让路径遍历少了(以及所有其他路径)

我们的第二语言仍然主要是 Raku,但有一个简洁的假象扩展名amb,但是拼写为(令人回味)<-。它摆脱了所有显式for循环和缩进层级。

my $d <- 0..9;
my $e <- 0..9;
guard $e != any($d);
my $y = ($d + $e) % 10;
my $_c1 = ($d + $e) div 10;

my $n <- 0..9;
guard $n != any($d, $e, $y);
my $r <- 0..9;
guard $r != any($d, $e, $y, $n);
guard ($_c1 + $n + $r) % 10 == $e;
my $_c2 = ($_c1 + $n + $r) div 10;

my $o <- 0..9;
guard $o != any($d, $e, $y, $n, $r);
guard ($_c2 + $e + $o) % 10 == $n;
my $_c3 = ($_c2 + $e + $o) div 10;

my $s <- 1..9;
guard $s != any($d, $e, $y, $n, $r, $o);
my $m <- 1..9;
guard $m != any($d, $e, $y, $n, $r, $o, $s);
guard ($_c3 + $s + $m) % 10 == $o;
my $_c4 = ($_c3 + $s + $m) div 10;

guard $_c4 % 10 == $m;

say "$s$e$n$d + $m$o$r$e == $m$o$n$e$y";

这种解决方案更短,更紧凑,并且感觉不那么“聒噪”,并且只是通过摆脱for循环来加重。(我怀疑这与人们有时提到的那些命令性的声明谱有关。我们对循环不是那么感兴趣,只看到它完成了。)

我知道它不会完全弥补Raku没有amb运算符并且 guard在核心中(甚至在模块空间中)实现的事实,但是这里有一个简短的脚本将上述程序转换为今天的第一个版本:

my $indent = 0;
constant SPACE = chr(0x20);
sub indent { SPACE x 4 * $indent }

for lines() {
    when /^ my \h+ ('$' \w) \h* '<-' \h* (\d+ \h* '..' \h* \d+) ';' $/ {
        say indent, "for $1 -> int $0 \{";
        $indent++;
    }

    when /^ guard \h+ ('$' \w) \h* '!=' \h* 'any(' ('$' \w)+ % [\h* ',' \h*] ')' \h* ';' $/ {
        say indent, "next if $0 == $_;"
            for $1;
        say "";
    }

    when /^ guard \h+ ([<!before '=='> .]+ '==' <-[;]>+) ';' $/ {
        say indent, "next unless $0;";
    }

    when /^ my \h+ ('$' \w+) \h* '=' \h* (<-[;]>+) ';' $/ {
        say indent, "my int $0 = $1;";
    }

    when /^ \h* $/ {
        say "";
    }

    when /^ say \h+ (<-[;]>+) ';' $/ {
        say indent, $_;
    }

    default {
        die "Couldn't match $_";
    }
}

while $indent-- {
    say indent, "\}";
}

但我们也不会就此满意。哦,不。

在方程式中思考

第三种语言将我们进一步引入声明,摆脱了所有仅仅表明变量应该是不同项的 guard 从句。

ALL_DISTINCT

$d in 0..9
$e in 0..9
$n in 0..9
$r in 0..9
$o in 0..9
$s in 1..9
$m in 1..9

$y = ($d + $e) % 10
$_c1 = ($d + $e) div 10

($_c1 + $n + $r) % 10 == $e
$_c2 = ($_c1 + $n + $r) div 10

($_c2 + $e + $o) % 10 == $n
$_c3 = ($_c2 + $e + $o) div 10

($_c3 + $s + $m) % 10 == $o
$_c4 = ($_c3 + $s + $m) div 10

$_c4 % 10 == $m

我们现在完全处于约束编程领域,如果不提这一点,是不诚实的。我们已经抛弃了Raku的必要方面,我们只关注描述我们正在解决的问题的约束。

上述程序最重要的方面是我们赋值时。即使这主要是一种优化,在我们知道我们可以直接计算变量的值而不是搜索变量的情况下。

即使在这种情况下,我们也可以转换回以前的解决方案。不过,我现在会省略这样一个翻译。

我将在结论中回到这种语言,因为它在很多方面证明了,这是最有趣的一种。

第四语言

到目前为止,我们还有哪些必要的复杂性可以剥离?具体而言,这些方程式来自前一解决方案中指定的位置?我们怎样才能更简洁地表达它们?

我想你会喜欢这个。第四种语言只是表达了这样的搜索:

    S E N D
+   M O R E
-----------
  M O N E Y

等一下,为什么又来?是的,你没有看错。这个问题最具声明性的解决方案只是问题规范本身的ASCII布局!当问题域和答案域如此相遇时,难道你不喜欢它吗?

从这个布局上,我们可以再次转换回约束编程解决方案,从手动算法中编写方程式,以便我们在学校学习。

因此,我们不仅不需要编写那些加重for循环的东西; 如果我们足够顽强,我们可以从问题到解决方案一直生成代码。我们只需找到合适的语言就可以了。

结论

我对007的探索使我思考了上述事情:翻译程序。Raku 已经很好地公开了编译过程的一部分:解析。我们可以在用户空间和Raku工具链中使用 grammars。

我开始相信我们需要对编译管道的所有方面都这样做。在这里,让我把它作为口号或声明:

当我们带来操作文本/数据的所有功能也可以向内转到编译过程本身时,Raku将充分发挥其潜力。

我在不同语言之间编写(或想象)的那些翻译器,他们在压力下工作,但他们也很脆弱,有点浪费。问题在很大程度上是我们一直下降到文本。我们应该在AST级别执行此操作,其中所有结构都可用。

这种思想转变所带来的收益不容小觑。这是我们在Raku中找到Lispy启蒙的地方。

例如,带方程的第三种语言不必盲目地翻译成代码。它可以被优化,方程式篡改成更窄和更精确的方程式。从维基百科可以看出,有可能做到如此优秀,以至于一旦程序运行就没有剩下的搜索。

我的梦想:能够进行上述转换,而不是在文本文件之间,而是在Raku中的俚语之间。并且能够进行优化步骤。一切都没有离开语言的舒适。

comments powered by Disqus