函数组合 Do Notation

7

是否有“do notation”语法糖可以用于简单的函数组合?

(.) :: (b -> c) -> (a -> b) -> a -> c

我想能够存储一些组合的结果以便稍后使用(同时仍然继续链式操作)。

如果可能的话,我不想使用RebindableSyntax扩展。

我正在寻找像这样的东西:

composed :: [String] -> [String]
composed = do
    fmap (++ "!!!")
    maxLength <- maximum . fmap length
    filter ((== maxLength) . length)

composed ["alice", "bob", "david"]
-- outputs: ["alice!!!", "david!!!"]

我不确定这种情况是否可能,因为之前函数的结果必须“通过”maxLength的绑定,但是我愿意听取其他类似表达选项的建议。基本上,我需要在组合过程中收集信息,以便稍后使用。

也许我可以使用状态单子做类似的事情?

感谢您的帮助!

编辑

以下方法可以解决问题:

split :: (a -> b) -> (b -> a -> c) -> a -> c
split ab bac a = bac (ab a) a

composed :: [String] -> [String]
composed = do
    fmap (++ "!!!")
    split 
        (maximum . fmap length)
        (\maxLength -> (filter ((== maxLength) . length)))

请明确您实际想要获得的组合。至少,给出您想要的“composed”的类型签名! - leftaroundabout
只是普通的函数组合,但具有存储插页式结果以供后续函数调用使用的能力(请参见示例)。我添加了类型。 - Chris Penner
1
你有必要使用 point-free 风格来编写吗?这个任务在非 point-free 风格下很容易实现。 - user253751
@immibis 我想我可以把一些函数拆分一下。然而,这个例子是简化的,实际使用中我会在 6 或 7 个函数调用之后重复使用一个早期的值,我不确定如何让它更清晰。您有这方面的示例吗? - Chris Penner
类似于 composed xs = filter ((== maxLength) . length) modified,其中 modified = fmap (++ "!!!") xsmaxLength = maximum $ fmap length modified(适当添加空格)。 - user253751
4个回答

5

实现类似功能的一种可能方法是箭头。基本上,在“存储插页式结果”中,您只需通过组合链分割信息流即可。这就是&&&(扇出)组合器所做的。

import Control.Arrow

composed = fmap (++ "!!!")
       >>> ((. length) . (==) . maximum . fmap length &&& id)
       >>> uncurry filter

这绝对不是易于人理解的好代码。

状态单子似乎也可以实现相关功能,但问题在于状态类型在 do 块的单子链中是固定的。这并不足够灵活,无法在组合链中获取不同类型的值。虽然当然有可能规避这个问题(其中包括 RebindableSyntax),但我认为这也不是一个好主意。


嗯,有趣!我在上面的编辑中尝试了类似的东西,但仍然希望看到更优雅一些的东西,现在还有点尴尬 :/ - Chris Penner

3

(<*>) 的类型在特殊函数实例 Applicative 中是:

(<*>) :: (r -> a -> b) -> (r -> a) -> (r -> b)

生成的r -> b函数将其参数传递给r -> a -> br -> a函数,然后使用r -> a函数产生的a值作为r -> a -> b函数的第二个参数。
那么这与您的函数有什么关系呢?filter是一个有两个参数的函数,谓词和列表。现在,您试图做的一个关键方面是谓词是由列表生成的。这意味着您的函数核心可以用(<*>)表示:
-- Using the predicate-generating function from leftaroundabout's answer.
maxLengthOnly :: Foldable t => [t a] -> [t a]
maxLengthOnly = flip filter <*> ((. length) . (==) . maximum . fmap length)

composed :: [String] -> [String]
composed = maxLengthOnly . fmap (++ "!!!")

如果这个点自由的谓词生成函数不那么笨重,maxLengthOnly 的定义将是一个相当好的一行代码。

由于函数的Applicative实例在能力上等价于Monad实例,maxLengthOnly也可以表述为:

maxLengthOnly = (. length) . (==) . maximum . fmap length >>= filter

(顺便提一下,您在问题中添加的“split”实际上是函数的">>="。)使用Applicative的另一种编写方式是:
maxLengthOnly = filter <$> ((. length) . (==) . maximum . fmap length) <*> id

这不是巧合,它看起来很像leftaroundabout的解决方案:对于函数,(,) <$> f <*> g = liftA2 (,) f g = f &&& g
最后值得注意的是,在最新版本的maxLengthOnly中用fmap (++ "!!!")替换id是很诱人的,但这样做行不通,因为fmap (++ "!!!")改变了字符串的长度,从而影响了谓词的结果。但是,使用不会使谓词失效的函数,这种方法效果非常好:
nicerComposed = filter
    <$> ((. length) . (==) . maximum . fmap length) <*> fmap reverse

GHCi> nicerComposed ["alice","bob","david"]
["ecila","divad"]

2
正文:
leftaroundabout所提到的,您可以使用Arrows编写函数。但是,Haskell编译器ghc中有一个特性,即用于箭头的proc符号表示法。它非常类似于众所周知的do符号表示法,但不幸的是,很少有人意识到它。
使用proc符号表示法,您可以以更易读和优雅的方式编写所需的函数:
{-# LANGUAGE Arrows #-}

import Control.Arrow (returnA)
import Data.List     (maximum)

composed :: [String] -> [String]
composed = proc l -> do
    bangedL <- fmap (++"!!!")        -< l
    maxLen  <- maximum . fmap length -< bangedL
    returnA -< filter ((== maxLen) . length) bangedL

这在 ghci 中按预期工作:
ghci> composed ["alice", "bob", "david"]
["alice!!!","david!!!"]

如果你有兴趣,可以阅读一些带有精美图片的教程,了解箭头是什么以及这个强大功能如何运作,这样你就可以更深入地学习它:

https://www.haskell.org/arrows/index.html

https://en.wikibooks.org/wiki/Haskell/Understanding_arrows


1
值得一提的是,我认为proc符号的优势在于它允许您编写相当通用的箭头组合,就好像它们是lambda语法中的函数一样。如果您正在使用的范畴是(->),那么如果您要给这些中间结果命名,那么没有什么理由不使用lambda表达式/let...in绑定来完成同样的事情。 - leftaroundabout
@leftaroundabout,是的,除了(->)之外还存在箭头,您可能希望编写通用版本的函数组合,尽管我没有为此案例做到这一点,以使示例更简单。您的版本遵循了这篇博客文章的思路:http://degoes.net/articles/insufficiently-polymorphic 但有时候使用变量名可以提高代码的可读性(特别是对于箭头来说)。 - Shersh

1
你所拥有的本质上是一个过滤器,但随着列表迭代,过滤函数会发生变化。我建议使用以下函数f :: String -> (Int, [String])来将其建模为折叠而不是“分叉”组合:
  1. 返回值维护当前最大值和所有该长度的字符串。
  2. 如果第一个参数比当前最大值短,则将其删除。
  3. 如果第一个参数与当前最大值相同,则将其添加到列表中。
  4. 如果第一个参数更长,则将其长度作为新的最大值,并用新列表替换当前输出列表。

完成折叠后,只需从元组中提取列表即可。

-- Not really a suitable name anymore, but...
composed :: [String] -> [String]
composed = snd . foldr f (0, [])
    where f curr (maxLen, result) = let currLen = length curr
                                    in case compare currLen maxLen of
                                       LT -> (maxLen, result)       -- drop
                                       EQ -> (maxLen, curr:result)  -- keep
                                       GT -> (length curr, [curr])  -- reset

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