我发现自己越来越经常做这样的事情...
我有一个函数 f :: IO [a]
,然后我想在其上应用 g :: a -> b
的函数,以获取 IO [b]
。
很长的路是:
x <- f
let y = fmap g x
然后我缩短到:
x <- f
let y = g <$> x
,如今我宁愿做:
y <- fmap g <$> f
但随后使用Functor
法律,我可以看到我可以做:
(<$$>) = fmap . fmap
y <- g <$$> f
,尽管我经常看到这种fmap . fmap
提到它在基本库中没有特殊的绑定,而对我来说似乎是一个频繁的习语。从字面上看,我可以在整个地方使用它!所以现在我想知道,我是否没有足够努力地编写纯代码,这就是为什么我比经验丰富的haskell程序员击中它……或者有一种更优雅的方法来解释为什么别人不使用这个成语很多?
我是意见(在Haskell社区的一部分中似乎不受欢迎),您应该编写代码以清楚地表达您的意图并遵循读者的期望,并且不是然后,代数将代码重构为等效的形式,即"整洁",但不再清楚。
在这种情况下,似乎在这里有两次FMAP的事实似乎没有任何真正的含义。您正在调整IO操作的结果的事实并未在逻辑上归结为以下事实:调整恰好是在列表上映射的事实。如果您在代码中多次看到" fmap . fmap
模式",那只是因为fmap
是一个非常普遍的功能。
如果您写
y <- g <$$> x -- or any other made-up operator name
您不仅强迫读者学习一个不熟悉的操作员,而且一旦他们学习了它,他们还是必须将其扩展到y <- fmap g <$> x
,以了解正在发生的事情,因为您将两个无关的操作分组在一起。
最好的形式是您的y <- fmap g <$> x
(甚至更好,y <- map g <$> x
),因为它适合熟悉的模式var <- function <$> action
。
(顺便说一句,您提到了函数法律,但它们与问题中的任何重写无关,这只是扩展定义。)
好吧,您拥有的是函数的组成。该概念的最广泛实现&ndash;尽管并不像您想要的那样一般性&ndash;是 monad变形金刚。
http://hackage.haskell.org/package/transformers-0.4.4.3.0/docs/control-monad-monad-trans-list.html#v:listt
f' :: ListT IO a
f' = ListT f
在此上,您只需使用fmap g
即可获得一个可以使用runListT
的值h' :: ListT IO b
,然后将其评估为IO [b]
。
该包装和解开是否值得取决于您连续进行的操作数量,可以使用相同的组成。对于某些应用,Monad Transformers简直太棒了。在其他情况下,它们只是使东西比现在更笨拙。fmap . fmap
实际上还不错,是吗?通常我会呆在那儿。
函数(以及应用程序)组成,Compose
NewType定义了两个函数组成的函数和应用实例。这意味着您可以将IO [a]
包装在Compose IO [] a
中并应用fmap
,例如
import Data.Functor.Compose
getCompose $ fmap g $ Compose f
我发现自己也经常这样做,发现它确实令人困惑。我发现liftM . map
比fmap . fmap
更容易读取。