【问题标题】:Apply function to argument before performing pattern matching在执行模式匹配之前将函数应用于参数
【发布时间】:2021-12-12 07:30:52
【问题描述】:

我是 Haskell 的新手,并试图不本能地思考命令。我有一个函数可以在模式匹配块中完成所有工作,但是它需要在参数上进行模式匹配一个函数应用于它之后。

以一种功能性的方式这样做让我明白了这一点:

foo :: Int -> String
foo n = bar $ show n

bar :: String -> String
bar [] = ""
bar (c:s) = "-" ++ bar s

foo 是我要实现的功能,而bar 是完成所有工作的地方。 foo 的存在只是为了提供正确的类型签名并在调用 bar 之前执行前驱 show 转换。在实践中,bar 可能会变得相当复杂,但我仍然没有理由将它作为一个单独的函数公开。

Haskell 执行show 之类的简单函数并对其结果进行“then”模式匹配的方法是什么?

我尝试将模式匹配更改为case 语句,但它不允许最重要的递归,因为没有函数可以递归调用。出于同样的原因,使用where clause applied to multiple patterns 也不起作用。

【问题讨论】:

  • 请注意,这是一个组合示例,因为foo 可以简单地定义为foo = bar . show
  • 把事情分解成多个功能是完全正常的;即使在命令式编程中,通常也建议保持程序/方法很小,将复杂的功能分解为多个单元。就像在大多数命令式语言中一样,您不必公开您编写的所有函数;模块系统完全能够导出foo 并保持bar 私有。但是,您使用where 找到的答案是一种很好的方式,可以完全清楚地表明bar 仅用于定义foo,与任何其他目的无关。
  • 惯用的方法是foo n = '-' <$ show n——即尽可能重用现有的递归函数,而不是自己编写递归。
  • 我可能是误会了,但你就不能foo n = case (show n) of... 吗?
  • @RobinZigmond 差不多,但是当您执行(c:s) 案例时,没有bar 可以递归调用。如果您尝试改为调用foo,您将传递一个String,它期待一个Int

标签: haskell functional-programming


【解决方案1】:

我记得Learn You A Haskell 似乎经常强调wherelet 比它们最初看起来更强大,因为“一切都是函数”并且子句本身就是表达式

这促使我看看我是否可以更努力地推动where,并使用它来定义辅助函数bar。事实证明我可以:

foo :: Int -> String
foo n = bar $ show n
  where bar [] = ""
        bar (c:s) = "-" ++ bar s

除非有更好的方法,否则我认为这就是我所追求的解决方案。看到这一点,我花了一些时间克服了我的迫切倾向,但它开始看起来合乎逻辑,并且比我想象的更“核心”。

如果这些假设让我偏离了方向,请提供替代答案!

【讨论】:

  • 旁白:我不确定这个限制是否与命令式/功能性划分有关。其他函数式语言(例如 ML 家族,甚至 λ-calculus with sums 即函数式语言的理论原型)对任意 表达式 进行模式匹配,而不是函数参数(您基本上得到了在 Haskell 中实现的内容)使用您的 where 构造,但您不必命名匹配器)。
【解决方案2】:

这取决于模式匹配函数是否递归。 在您的示例中,它被称为 bar 并且是递归的。

foo :: Int -> String
foo n = bar $ show n

bar :: String -> String
bar [] = ""
bar (c:s) = "-" ++ bar s

在这里,(可以说)最好的解决方案是您找到的那个:使用where(或let)并将其在本地定义为foo

foo :: Int -> String
foo n = bar $ show n
   where
   bar :: String -> String   -- optional type annotation
   bar [] = ""
   bar (c:s) = "-" ++ bar s

内部函数的类型注释是可选的。许多 Haskeller 认为顶级函数 foo 应该有它的签名(如果你不提供它,带有 -Wall 的 GHC 会发出警告),但也认为内部函数不必注释。对于它的价值,我喜欢在我认为从上下文中不明显时添加它。随意包含或省略它。

bar 不是递归时,我们还有其他选择。考虑这段代码:

foo :: Int -> String
foo n = bar $ show n

bar :: String -> String
bar []    = "empty"
bar (c:s) = "nonempty " ++ c : s

这里,我们可以使用case of

foo :: Int -> String
foo n = case show n of
   []    -> "empty"
   (c:s) -> "nonempty " ++ c : s

这首先调用函数show,然后对其结果进行模式匹配。我认为这比添加where 来定义bar 更容易阅读。

从理论上讲,即使在递归情况下,我们可以遵循case 方法,并利用fix(库中的一个函数)来关闭递归。我确实建议您这样做,因为使用where(或let)定义bar 更具可读性。我在这里添加这个可读性较差的选项只是为了完整性。

foo :: Int -> String
foo n = fix (\bar x -> case x of
   []    -> ""
   (c:s) -> "-" ++ bar s
   ) $ show n

这相当于第一个递归代码sn-p,但它需要更多的时间来阅读。助手fix 有其用途,但如果我在实际生产代码中阅读此内容,我会认为程序员试图表明他们“聪明”,而不是编写简单易读的代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多