我目前正在学习 haskell,我很难理解如何解释<$>and<*>的行为。
对于某些上下文,这一切都来自搜索如何or在使用时使用操作takeWhile,我发现的答案是这样的
takeWhile ((||) <$> isDigit <*> (=='.'))
Run Code Online (Sandbox Code Playgroud)
在我见过的大多数文档中,<*>与容器类型一起使用。
show <*> Maybe 10
Run Code Online (Sandbox Code Playgroud)
通过查看
(<$>) :: Functor f => (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)
它告诉我 <*> 保留外部容器(如果其内容)并将权限应用于内部,然后将其包装回容器中
a b f a f b
([Int] -> String) -> [Just]([Int]) -> [Just]([String])
Run Code Online (Sandbox Code Playgroud)
这对我来说是有意义的,在我看来,这f a本质上是在容器内部发生的,但是当我尝试相同的逻辑时,我可以理解,但我无法将逻辑关联起来
f = (+) <$> (read)
Run Code Online (Sandbox Code Playgroud)
所以因为f它变成了
a b f a f b
([Int] -> [Int -> Int]) -> ([String] -> [Int]) -> ([String] -> [Int -> Int])
Run Code Online (Sandbox Code Playgroud)
因此,f当我尝试弄清楚这段代码要做什么时,作为容器真的让我很困惑。我明白,当我这样写出来时,我可以计算出来并看到它基本上等同于.
(.) :: (b -> c) -> (a -> b) -> a -> c
b c a b a c
([Int] -> [Int -> Int]) -> ([String] -> [Int]) -> ([String] -> [Int -> Int])
Run Code Online (Sandbox Code Playgroud)
所以它可以写成
f = (+) . read
Run Code Online (Sandbox Code Playgroud)
为什么不直接这样写呢?为什么原始片段不直接写成
takeWhile ((||) . isDigit <*> (=='.'))
Run Code Online (Sandbox Code Playgroud)
或者<$>在这种情况下是否暗示了一些.并非如此的事情?
现在看<*>,它似乎与 <$> 基本完全相同,只是它需要两个容器,使用两个容器的内部,然后将其打包到容器中
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)
所以
Just show <*> Just 10
f a b f a f b
[Just]([Int->Int]->[Int]) -> [Just]([Int->Int]) -> [Just]([Int])
Run Code Online (Sandbox Code Playgroud)
然而,对于函数来说,事物如何相互传递就变得模糊了。查看原始片段并将其分解
f1 :: Char -> Bool -> Bool
f1 = (||) . isDigit
f2 :: Char -> Bool
f2 = f1 <*> (== '.')
Run Code Online (Sandbox Code Playgroud)
<*>的行为f2是
f a b f a f b
([Char] -> [Bool] -> [Bool]) -> ([Char] -> [Bool]) -> ([Char] -> [Bool])
Run Code Online (Sandbox Code Playgroud)
因此,使用之前的逻辑,我将其视为Char ->容器,但在弄清楚正在发生的情况时,它对我来说不是很有用。
在我看来,好像<*>是将函数参数传递到右侧,然后传递相同的函数参数,并将返回值传递到左侧?所以对我来说,它看起来相当于
f2 :: Char -> Bool
f2 x = f1 x (x=='_')
Run Code Online (Sandbox Code Playgroud)
<*>当我看到和时,弄清楚数据流向的位置对我来说有点费脑力<$>。我想我只是在寻找经验丰富的 Haskell-er 如何在头脑中阅读这些操作。
函数的应用实例非常简单:
f <*> g = \x -> f x (g x)
Run Code Online (Sandbox Code Playgroud)
您可以自己验证类型是否匹配。正如你所说,
(<$>) = (.)
Run Code Online (Sandbox Code Playgroud)
(忽略固定性)
所以你可以重写你的函数:
(||) <$> isDigit <*> (=='.')
(||) . isDigit <*> (=='.')
\x -> ((||) . isDigit) x ((=='.') x)
-- Which can simply be rewritten as:
\x -> isDigit x || x == '.'
Run Code Online (Sandbox Code Playgroud)
但了解函数实例为何如此以及它如何工作非常重要。让我们从以下开始Maybe:
instance Applicative Maybe where
pure :: a -> Maybe a
pure x = Just x
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
Nothing <*> _ = Nothing
_ <*> Nothing = Nothing
(Just f) <*> (Just x) = Just (f x)
Run Code Online (Sandbox Code Playgroud)
这里忽略实现,只看类型。首先,请注意我们已经创建了Maybe一个Applicative. 到底是 Maybe什么?你可能会说这是一种类型,但这不是真的 - 我不能写类似的东西
x :: Maybe
Run Code Online (Sandbox Code Playgroud)
- 这没有道理。相反,我需要写
x :: Maybe Int
-- Or
x :: Maybe Char
Run Code Online (Sandbox Code Playgroud)
或之后的任何其他类型Maybe。所以我们给出一个像or 这样Maybe的类型,它突然就变成了一种类型!这就是所谓的类型构造函数的原因。
这正是类型类所期望的——类型构造函数,您可以在其中放入任何其他类型。所以,用你的类比,我们可以考虑给出一个容器类型。IntCharMaybeApplicativeApplicative
现在,我的意思是
a -> b
Run Code Online (Sandbox Code Playgroud)
?
我们可以使用前缀表示法重写它(同样的方式1 + 2 = (+) 1 2)
(->) a b
Run Code Online (Sandbox Code Playgroud)
在这里我们看到箭头(->)本身也只是一个类型构造函数 - 但与 不同的是Maybe,它有两种类型。但Applicative只需要一个采用一种类型的类型构造函数。所以我们给它这个:
instance Applicative ((->) r)
Run Code Online (Sandbox Code Playgroud)
这意味着对于任何r,(->) r都是一个Applicative. 继续容器类比,(->) r现在是任何类型的容器,b使得结果类型为r -> b。这意味着所包含的类型实际上是函数给它一个r.
现在来说说实际的例子:
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)
替换(->) r为应用词,
(<*>) :: ((->) r (a -> b)) -> ((->) r a) ((->) r b)
-- Rewriting it in infix notation:
(<*>) :: (r -> (a -> b)) -> (r -> a) -> (r -> b)
Run Code Online (Sandbox Code Playgroud)
我们将如何编写实例?好吧,我们需要一种方法来从容器中获取包含的类型 - 但我们不能像使用Maybe. 因此,我们使用 lambda:
(f :: r -> (a -> b)) <*> (g :: r -> a) = \(x :: r) -> f x (g x)
Run Code Online (Sandbox Code Playgroud)
的类型f x (g x)是b,所以整个 lambda 具有类型r -> b,这正是我们正在寻找的!
编辑:我注意到我没有谈论purefor 函数的实现 - 我可以更新答案,但尝试看看您是否可以使用类型签名来自己解决它!