函数组合 Do Notation

Function Composition Do Notation

是否有 "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!!!"]

我不确定这样的事情是否可行,因为早期函数的结果本质上必须通过 "through" maxLength 的绑定,但我愿意听取任何其他类似表达方式的选择.基本上我需要在浏览作文时收集信息以便以后使用它。

也许我可以用状态 monad 做这样的事情?

感谢您的帮助!

编辑

这种东西有点管用:

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)))

实现类似目标的一种可能方法是箭头。基本上,在“存储间隙结果”中,您只是通过组合链拆分信息流。这就是 &&& (fanout) 组合器的作用。

import Control.Arrow

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

虽然这绝对不是人类可理解的好代码。

状态 monad 似乎也允许一些相关的东西,但问题是状态类型是通过 do 块的 monadic 链固定的。这不够灵活,无法在整个组合链中获取不同类型的值。虽然肯定有可能规避这一点(其中确实有 RebindableSyntax),但在 IMO 看来,这也不是一个好主意。

您所拥有的本质上是一个过滤器,但是当您遍历列表时,过滤功能会发生变化。我不会将其建模为 "forked" 组合,而是使用以下函数 f :: String -> (Int, [String]):

进行折叠
  1. return 值保持当前最大值和该长度的所有字符串。
  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

专用于Applicative函数实例的(<*>)类型是:

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

生成的 r -> b 函数将其参数传递给 r -> a -> br -> a 函数,然后使用 [=20= 生成的 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 (++ "!!!")

如果 pointfree 谓词生成函数不那么笨拙,这个 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

最后,还值得注意的是,虽然很想用 fmap (++ "!!!") 替换最新版本 maxLengthOnly 中的 id,但这是行不通的,因为 fmap (++ "!!!") 改变字符串的长度,因此影响谓词的结果。但是,使用不使谓词无效的函数,它会工作得很好:

nicerComposed = filter
    <$> ((. length) . (==) . maximum . fmap length) <*> fmap reverse
GHCi> nicerComposed ["alice","bob","david"]
["ecila","divad"]

leftaroundabout所述,您可以使用Arrows来编写您的函数。但是,ghc Haskell 编译器有一个特性,就是箭头的 proc 符号。它与众所周知的 do-notation 非常相似,但不幸的是,没有多少人知道它。

使用 proc-notation 你可以用下一个更可读和优雅的方式编写你想要的函数:

{-# 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