如何以管道方式混合Haskell monadic和纯过滤器?

时间:2013-11-25 21:46:48

标签: haskell pipe operator-keyword

在之前的question中,我试图通过将它们组合在一起来询问如何混合纯函数和monadic函数,但是因为我可能错误地说了我的问题并且我的例子太简单了,我认为讨论已经进行了错误的方向,所以我想我会再试一次。

这是一个混合纯和一元过滤器的示例函数。在这个例子中,有一些纯粹的过滤器在monadic过滤器之间排序,试图减少工作量。

findFiles target = 
  getDirectoryContents target                    >>=
  return . filter (not . (=~ "[0-9]{8}\\.txt$")) >>=
  return . filter (=~ "\\.txt$")                 >>=
  filterM doesFileExist                          >>=
  mapM canonicalizePath

使用return混合纯函数的方式编写它的好处是,从上到下有一个可视流数据。不需要临时变量fmap<$>等。

理想情况下,我可以摆脱return以使其更清洁。我有想法使用一些算子:

(|>=) :: Monad m => a -> (a -> m b) -> m b
a |>= b = (return a) >>= b

但我不知道如何编写此函数以避免运算符优先级问题。这已经存在了吗?它类似于<$>,但与“其他方向”相似。如果没有,我该如何使这个操作符工作?

更一般地说,是否有一种以这种管道方式编写代码的好方法,或者我需要解决fmap和临时变量,如上一个问题所述?

4 个答案:

答案 0 :(得分:7)

唉。就这么简单:

infixl 1 |>=
(|>=) = flip fmap

findFiles target = 
  getDirectoryContents target           |>=
  filter (not . (=~ "[0-9]{8}\\.txt$")) |>=
  filter (=~ "\\.txt$")                 >>=
  filterM doesFileExist                 >>=
  mapM canonicalizePath

答案 1 :(得分:3)

借调迭戈诺兰,对于使用do符号,使用x <- ... - 符号,使用monadic赋值(let)或一个好的老式{绑定中间值,没有任何耻辱。 {1}}。代码的继承人会感谢你。

那就是说,如果你不能分数,你可能是一个类别理论家。说真的,你可以从John Hughes那里拿一页(见Programming with Arrows)并写下你的管道:

import Control.Arrow

findFiles = runKleisli $
    Kleisli getDirectoryContents >>>
    arr (filter (not . (=~ "[0-9]{8}\\.txt$"))) >>>
    arr (filter (=~ "\\.txt$")) >>>
    Kleisli (filterM doesFileExist) >>>
    Kleisli (mapM canonicalizePath)

这可能比使用一个人自己的特殊绑定操作符更有原则性,但如果你问我,它仍然比普通的尖端风格更丑陋。 De gustibus non est disputandum,正如罗马人常说的关于garum。

答案 2 :(得分:1)

使用(<$>)(也称为fmap)将纯函数映射到仿函数。大多数monad都有仿函数的例子。如果他们没有,那么您可以使用liftM

查看类型

liftM :: Monad m => (a -> b) -> m a -> m b

(<$>) :: Functor f => (a -> b) -> f a -> f b

你的看起来像这样(没有检查ghc)。

findFiles target = 
  ((filter (not . (=~ "[0-9]{8}\\.txt$")) .
  filter (=~ "\\.txt$")                 )        <$>
  getDirectoryContents target)                   >>=
  filterM doesFileExist                          >>=
  mapM canonicalizePath

但是在这一点上你可能最好使用do notation和let

答案 3 :(得分:1)

你需要一些额外的操作符,一个处理每个案例

Monad -> Monad
Monad -> Pure
Pure  -> Monad
Pure  -> Pure

您已经拥有Monad -> Monad案例(>>=),正如我在上一个问题的答案中所述,您可以|>=使用Pure -> Monad案例,但你仍然需要Monad -> Pure一个。这将是棘手的,因为唯一的类型安全的方法是通过让该运算符将您的纯函数转换为monadic函数。我推荐以下一组操作符

Monad -> Monad    >>=    m a -> (a -> m b) -> m b
Monad -> Pure     >|=    m a -> (a ->   b) -> m b
Pure  -> Monad    |>=      a -> (a -> m b) -> m b
Pure  -> Pure     ||=    (a -> b) -> (b -> c) -> (a -> c)

使用>表示“monad”和|表示“纯”的约定,所有结尾都以=表示“起作用”。希望类型签名对于实现是有意义的:

import Data.Char (toUpper)
import Control.Monad (liftM)

infixl 1 |>=
(|>=) :: Monad m => a -> (a -> m b) -> m b
a |>= b = b a

infixl 1 >|=
(>|=) :: Monad m => m a -> (a -> b) -> m b
a >|= b = liftM b a

infixr 9 ||=
(||=) :: (a -> b) -> (b -> c) -> a -> c
a ||= b = b . a

一个例子

test :: IO ()
test =
    getLine             >|=
    filter (/= 't')     ||=
    map toUpper         >>=
    putStrLn

> test
testing
ESING
>

这也等同于

test :: IO ()
test =
    getLine         >|=
    filter (/= 't') >|=
    map toUpper     >>=
    putStrLn

但是额外的||>组合可以让你实际编写这些函数,这些函数具有不同的实现,而不是通过monadic动作来提供它们。

但是,我仍然敦促您使用惯用的方法来使用fmap,do notation和临时变量。对于那些查看代码的人来说,它会更加清晰,并且在2个月内包括你。