haskell 将对应用性的理解从liftA2扩展到liftA3

z2acfund  于 2022-11-14  发布在  其他
关注(0)|答案(2)|浏览(109)

我目前正在努力理解应用函子。我的理解在某个地方有所欠缺,但我不能准确指出是哪里。Source from where我正在努力建立理解CIS 194 UPenn-Homework 10
我正在使用一个自定义数据类型及其应用示例,如下所示:

-- A parser for a value of type a is a function which takes a String
-- represnting the input to be parsed, and succeeds or fails; if it
-- succeeds, it returns the parsed value along with the remainder of
-- the input.
newtype Parser a = Parser { runParser :: String -> Maybe (a, String) }

instance Functor Parser where
  fmap :: (a -> b) -> Parser a -> Parser b
  fmap fn parserA = Parser (fmap (first fn) . parseAFunc)
    where parseAFunc = runParser parserA

appliedFunc p1 p2 str = case runParser p1 str of
    Nothing        -> Nothing
    Just (f, str2) -> case runParser p2 str2 of
        Nothing        -> Nothing
        Just (x, str3) -> Just (f x, str3)

instance Applicative Parser where
  pure a = Parser (\s -> Just (a, s))
  p1 <*> p2 = Parser (appliedFunc p1 p2)  

-- For example, 'satisfy' takes a predicate on Char, and constructs a
-- parser which succeeds only if it sees a Char that satisfies the
-- predicate (which it then returns).  If it encounters a Char that
-- does not satisfy the predicate (or an empty input), it fails.
satisfy :: (Char -> Bool) -> Parser Char
satisfy p = Parser f
  where
    f [] = Nothing    -- fail on the empty input
    f (x:xs)          -- check if x satisfies the predicate
                        -- if so, return x along with the remainder
                        -- of the input (that is, xs)
        | p x       = Just (x, xs)
        | otherwise = Nothing  -- otherwise, fail

-- Using satisfy, we can define the parser 'char c' which expects to
-- see exactly the character c, and fails otherwise.
char :: Char -> Parser Char
char c = satisfy (== c)

-- Below is a parser for positive Ints which parses
-- the prefix of contiguous digits in a given String
-- as an Int 
posUse :: Parser Int
posUse = Parser f
  where
    f xs
      | null ns   = Nothing
      | otherwise = Just (read ns, rest)
      where (ns, rest) = span isDigit xs

-- Below function takes a function and a pair and returns a pair 
-- with the function applied to the first element of pair
first :: (a -> b) -> (a, c) -> (b, c)
first fn (x, y) = (fn x, y)

使用上面的所有设置,我尝试使用上面定义的简单解析器来构造一些更复杂的解析器。所以fmap的类型是fmap :: Functor f => (a -> b) -> f a -> f b。根据我目前的理解,Applicationative允许我们将fmap扩展为具有n元函数和n个函子参数的函数,并返回结果函子。
fmap可以根据pure<*>定义为:

fmap1 :: Applicative f => (a -> b) -> f a -> f b
fmap1 f x = pure f <*> x

我理解为什么上面的代码可以正常工作,因为pure<*>的实现是合适的,因为这些类型很好地排列在一起。
将其扩展到fmap2(即liftA2):

fmap2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
fmap2 f x y = pure f <*> x <*> y

我对上述工作原理的理解如下:pure g的类型是f (a -> b -> c),所以pure g <*> x的类型是f (b -> c),因为函数是curried的,然后这个类型和y(f b)的类型结合,使用<*>,最终给予的结果的类型是f c,这就是我们需要的fmap2的类型。
当我试图用2个简单的解析器构建一个解析器时,这种理解并没有被打破,如下所示:

-- This Parser expects to see the characters ’a’ and ’b’ and returns them
-- as a pair.
abParser = liftA2 (\c1 c2 -> (c2 c1)) (char 'a') (char 'b')

上面的工作和预期的一样。
但是,当我试图使解析器intPair读取两个由空格分隔的整数值,并使用liftA3返回列表中的整数值时,我对lift的理解不起作用,因为我希望下面的代码起作用,但liter抱怨:

intPair :: Parser [Int]
intPair = liftA3 (\x y z -> [z x]) posUse (char ' ') posUse

上面的代码无法编译,因为最后一个参数需要具有Parser (Int -> a)类型(根据类型推断),但我传递了posUse,它具有Parser Int类型。
TL;DR:抱歉描述太长了。如果有人不想经历这一切(特别是自定义数据类型及其应用程序和功能函数示例)--请让我知道我对fmap2(又称liftA2)工作原理的理解是否正确,以及这种理解如何扩展到liftA3liftA3的定义似乎与使用<*>pureliftA2的定义的扩展不同。
编辑1:如上所述,下面的行被投诉的短绒

intPair :: Parser [Int]
intPair = liftA3 (\x y z -> [z x]) posUse (char ' ') posUse

liter期望最后一个参数的类型是Parser (Int -> a),但是在我定义了一个显式函数而不是传递lambda之后,它就像我期望的那样工作了。

fnn :: Int -> Char -> Int -> [Int]
fnn arg1 arg2 arg3 = [arg1, arg3]
intPair = liftA3 fnn posUse (char ' ') posUse

它的工作原理和我预期的一样。

bvjxkvbb

bvjxkvbb1#

是的,您对liftA2的理解是正确的,而liftA3只是更相似而已。我猜不出为什么它看起来不同,但是将liftA2的定义内嵌到liftA3的定义中可能会有所帮助。

liftA2 :: Applicative f => (a -> b -> c) 
                        -> f a -> f b -> f c
liftA2 f x y = pure f <*> x <*> y

liftA3 :: Applicative f => (a -> b -> c -> d) 
                        -> f a -> f b -> f c -> f d
liftA3 f a b c = liftA2 f a b <*> c

我从https://hackage.haskell.org/package/base-4.17.0.0/docs/src/GHC.Base.html中提取了这些定义,并稍微重新排列了liftA2的定义,使其更加明确--它与您的fmap2完全匹配。
现在,请注意liftA3包含了对liftA2 f a b的调用。我们知道liftA2 f a b是如何定义的,并且由于纯粹性,我们总是可以内联定义以生成语义相同的术语。因此,让我们这样做:

liftA3 f a b c = (pure f <*> a <*> b) <*> c

括号是多余的,但为了以防万一,我在替换时把它们加了进去。现在,如果我们去掉它们,我们会发现这里没有新的想法:

liftA3 f a b c = pure f <*> a <*> b <*> c

我们提升这个函数,得到f (a -> b -> c -> d),然后把它应用到a,得到<*>,以此类推,直到最后得到f d

ccrfmcuu

ccrfmcuu2#

在将命名函数转换为匿名函数时,您犯了一个愚蠢的错误。对比:

fnn arg1 arg2 arg3 = [arg1, arg3] -- your working code
fnn = \x y z -> [z x]             -- your broken code
fnn = \x y z -> [z, x]            -- type-correct code
fnn = \x y z -> [x, z]            -- more correct translation of your working code

相关问题