Raku中的模式匹配是否具有守卫条件?

7
在Scala中,模式匹配具有守卫模式:
val ch = 23
val sign = ch match { 
    case _: Int if 10 < ch  => 65 
    case '+' =>  1 
    case '-' =>  -1 
    case  _  =>  0 
}

这个Raku版本是这样的吗?
my $ch = 23;
given $ch  {
    when Int and * > 10 { say 65}
    when '+' { say 1  }
    when '-' { say -1 }
    default  { say 0  }
}

这样对吗?

更新:如jjmerelo所建议,我将我的结果发布如下,签名版本也很有趣。

multi washing_machine(Int \x where * > 10 ) { 65 }
multi washing_machine(Str \x where '+'    ) { 1  }
multi washing_machine(Str \x where '-'    ) { -1 }
multi washing_machine(\x)                   { 0  }

say washing_machine(12);      # 65
say washing_machine(-12);     # 0
say washing_machine('+');     # 1
say washing_machine('-');     # -1
say washing_machine('12');    # 0
say washing_machine('洗衣机'); # 0

你期望Scala语句case _: Int if 10 < ch => 65会做什么? - Christopher Bottoms
1
我认为你需要将它写成 when $_~~Int and $_>10 {…}。请注意,given 会在块内将值放入 $_ 中。 - Brad Gilbert
1
@ChristopherBottoms 它只返回数字65,可能if语句更复杂,比如 if 10 < ch < 120 - chenyf
@Brad Gilbert 你是对的 ^_^ - chenyf
“Raku版本是这样的吗?” 当将$ch的值设置为9时,我得到了错误的答案:代码返回65,而答案应该是0。然而,when (Int & (* > 10)) { say 65 } ;这一行似乎可以纠正错误。 - jubilatious1
4个回答

10

TL;DR 我已经写了另一个答案,专注于使用 when。而这个答案则专注于使用与Signature相结合的替代方法,Raku强大的模式匹配构造,以及where从句。

"Raku中的模式匹配是否有保护从句?"

根据我所知道的Scala很少的一些东西,一些(或者大多数)Scala的模式匹配实际上对应于使用Raku签名。(在这种情况下,防卫从句通常是where从句。)

引用Scala的创造者Martin Odersky在The Point of Pattern Matching in Scala中的话:

instead of just matching numbers, which is what switch statements do, you match what are essentially the creation forms of objects

Raku signatures覆盖了几种用例(耶,双关语)。这些包括Raku中等价于函数式编程典型用例,其中匹配值或函数类型签名(与Haskell类似),以及面向对象编程典型用例,其中匹配嵌套数据/对象并提取所需位(与Scala类似)。

考虑以下Raku代码:

class body { has ( $.head, @.arms, @.legs ) } # Declare a class (object structure).

class person { has ( $.mom, $.body, $.age ) } # And another that includes first.

multi person's-age-and-legs                   # Declare a function that matches ...

  ( person                                    # ... a person ...

    ( :$age where * > 40,                     # ... whose age is over 40 ...

      :$body ( :@legs, *% ),                  # ... noting their body's legs ...

      *% ) )                                  # ... and ignoring other attributes.

  { say "$age {+@legs}" }                     # Display age and number of legs.

my $age = 42;                                 # Let's demo handy :$var syntax below.

person's-age-and-legs                         # Call function declared above ...

  person                                      # ... passing a person.

    .new:                                     # Explicitly construct ...

      :$age,                                  # ... a middle aged ...

      body => body.new:
        :head,
        :2arms,
        legs => <left middle right>           # ... three legged person.

# Displays "42 3"

上述代码中有一个类似于 Scala 模式匹配守卫子句的紧密等效位置 -- where * > 40。(这可以很好地捆绑到subset类型中。)

我们可以定义其他的multi,对应不同的情况,例如,如果某人母亲的姓名与特定的正则表达式匹配,可能会提取出该人腿的“名称”(“left”,“middle”等)-- 希望您能理解。

一个不需要拆分人物的默认情况(multi)可以是:

multi person's-age-and-legs (|otherwise)
  { say "let's not deconstruct this person" }

(在上面的示例中,我们在签名中使用|前缀来捕获传递给多元函数的所有剩余结构/参数。鉴于我们对已捕获的结构/数据没有任何处理,我们也可以只编写(|)。)

不幸的是,我认为官方文档中没有提到签名解构。有人可以撰写一本关于Raku签名的书(确实如此,这当然是撰写东西的好方式--甚至是唯一的方式。揭示Raku签名强大力量的我最喜欢的文章是Moritz于2013年撰写的Pattern Matching and Unpacking。Moritz曾经撰写过Raku的书。希望如此。)

Scala的match/case和Raku的given/when似乎更简单

确实。

正如@jjmerelo在评论中指出的那样,使用签名意味着每种情况都会有一个multi foo (...) { ...},这在语法上比case ... => ...要复杂得多。

缓解措施:

  • 简单的情况可以像您在问题正文中编写的那样使用given/when

  • Raku可能会有一天获得非实验性宏,可以用来实现看起来更接近Scala的match/case结构的构造,省略重复的multi foo (...)


1
Perl 6的签名非常强大。我稍后会更新一个使用签名的版本。 - ohmycloudy
2
这是一个很好的答案。不过我看到其中存在两个潜在问题。首先,你必须声明multis,而在Haskell和Scala中它们只是语句。其次,没有默认的声明方式。也许可以使用Slurpies,但我不知道是否覆盖得全面。 - jjmerelo
1
@jjmerelo 重复的 multi foo (...) 确实相对冗长;也许宏有一天会有所帮助。最简单的默认确实使用 slurpy -- multi foo (|) {...}。我已根据您的反馈编辑了我的答案,谢谢。(我还写了另一个答案。) - raiph

7

根据我在this answer">这个答案中看到的,那并不是像Haskell中那样实现守卫模式。然而,Perl 6确实有与Scala相同的守卫:使用默认模式和if语句组合。 Haskell to Perl 6 guide">Haskell到Perl 6指南中有一个关于守卫的章节。它提到了使用where作为守卫的方法;所以这可能能回答你的问题。


2
multi 真是太棒了!似乎评论不支持代码,所以我将我的结果附在问题下面。 - chenyf

6
简述:您遇到了我称之为“WTF?!?”的问题:当Type和...无法检查and子句时。本答案介绍了when的问题所在以及如何修复它。我还写了另一个答案,重点介绍了如何使用where签名。

如果您想坚持使用when,我建议使用以下方法:

when (condition when Type) { ... } # General form
when (* > 10 when Int) { ... }     # For your specific example

我认为这并不令人满意,但是它首先作为一个保护条件检查了Type,然后检查条件是否通过,按预期工作。


“这样对吗?”

不对。

given $ch {
  when Int and * > 10 { say 65}
}

这段代码对于任何给定的整数都返回65,而不仅仅是在10以上的整数!

WTF?!我们应该在Raku's trap page上提及此问题。

我们还应该考虑提交问题,使Rakudo在一个以编译时常量值开头且接着使用and(或&&andthen等)的when结构中发出警告或拒绝编译。它可以在编译时失败或显示警告。


这是我能想到的最佳选项:

when (* > 10 when Int) { say 65 }

这利用了括号内when语句修饰符(也称后缀形式)。在* > 10之前会检查Int

这灵感来自Brad++的新答案,如果您要针对单个守卫子句编写多个条件,则看起来很好。

我认为我的变体比我以前版本中想出的其他选项更好,但仍不尽如人意,因为我不喜欢Int在条件之后


最终,特别是当RakuAST到达时,我认为我们将尝试使用新的模式匹配形式。希望我们能想出一些漂亮的东西,提供对此问题的完美消除。

真的吗? 到底发生了什么?

我们可以开始看到这段代码的潜在问题:

.say for ('TrueA' and 'TrueB'),
         ('TrueB' and 'TrueA'),
         (Int and 42),
         (42 and Int)

显示:

TrueB
TrueA
(Int)
(Int)
and构造布尔运算符评估其左侧参数。如果它评估为False,则返回该值,否则返回其右侧参数。
在第一行中,'TrueA'布尔值评估为True,因此第一行返回右侧参数'TrueB'
在第二行中,'TrueB'评估为True,因此and返回其右侧参数,即'TrueA'
但是第三行会发生什么呢?嗯,Int是一个类型对象。类型对象的布尔值评估为False!因此and将返回其左侧参数,即Int(然后.say将其显示为(Int))。
这就是问题的根源。
(继续到最后,编译器评估表达式Int and * > 10;立即返回and的左侧参数,即Int;然后成功将该Int与任何给定的整数匹配--完全忽略看起来像守卫子句的代码(and ...部分)。)
如果您将这样的表达式用作,例如,if语句的条件,则Int将布尔值评估为False,并且您将得到一个错误的负面结果。在这里,您使用了一个使用.ACCEPTSwhen,这导致了一个错误的正面结果(它是一个整数,但它是任何整数,而忽略了所谓的守卫子句)。这个问题很可能属于陷阱页面

嵌套的when子句在这里是不安全的,但根据另一个答案来说是安全的? - jubilatious1

4

多年前,我曾经发表过一条评论,提到你必须更加明确地使用像这样的代码来匹配$_:

my $ch = 23;
given $ch  {

    when $_ ~~ Int and $_ > 10 { say 65}

    when '+' { say 1  }
    when '-' { say -1 }
    default  { say 0  }
}

回到这个问题后,我意识到还有另一种方法。可以将when嵌套在另一个when中,这样做是安全的。
my $ch = 23;
given $ch  {

    when Int:D {
        when $_ > 10 { say 65}
        proceed
    }

    when '+' { say 1  }
    when '-' { say -1 }
    default  { say 0  }
}

请注意内部的 when 会成功地跳出外层的 when,这个又会从 given 块中成功退出。
如果内部的 when 不匹配,我们想要继续执行到外部的 whendefault,所以我们调用了 proceed 方法。
这意味着我们还可以将多个 when 语句组合在 Int case 中,避免重复进行类型检查。这也意味着,如果我们不测试一个 Int 值,那么那些内部的 when 检查将根本不会发生。
    when Int:D {
        when $_ < 10 { say 5 }
        when 10      { say 10}
        when $_ > 10 { say 65}
    }

2
谢谢Brad Gilbert,每次从你的回答中我都学到了新东西。 - chenyf
嵌套的when子句在这里是安全的,但根据另一个答案来说是不安全的? - jubilatious1

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