柯里化 Haskell 与仿函数和函数组合

Currying with Haskell with functor and function composition

我有以下问题。我有一个功能很好用。

getName :: Style -> Css -> Maybe String
getName s css = map intToChar .  intToBase 26 <$> getIndex s css

我想知道为什么我不能这样定义它:

getName :: Style -> Css -> Maybe String
getName = map intToChar .  intToBase 26 <$> getIndex

我认为柯里化可以让我return作为函数接受两个参数而不是一个有两个参数的函数

如果对您的理解有帮助:

intToChar :: Int -> Char
intToBase :: Int -> Int -> [Int]
getIndex :: Style > Css -> Maybe Int

啊,不要忘记错误:

error:
    • Couldn't match type ‘[a] -> Maybe Int’ with ‘Int’
      Expected type: a -> Int
        Actual type: a -> [a] -> Maybe Int
    • Probable cause: ‘getIndex’ is applied to too few arguments
      In the second argument of ‘(<$>)’, namely ‘getIndex’
      In the expression: map intToChar . intToBase 26 <$> getIndex
      In an equation for ‘getName’:
          getName = map intToChar . intToBase 26 <$> getIndex
    • Relevant bindings include
        getName :: a -> [Char] (bound at src/Css/Css.hs:17:1)
   |        
17 | getName = map intToChar .  intToBase 26 <$> getIndex 
   |                                             ^^^^^^^^

问题是 scss 不能通过 eta conversion 消除,因为它们单独用作 getIndex 的参数,而不是 map intToChar . intToBase 26 <$> getIndex .

要走point-free,就得用函数组合。首先,使用 fmap 而不是 <$> 重写您的定义(并将 fmap 的不相关参数隐藏在局部变量后面)。

getName s css = fmap f (getIndex s css)
    where f = map intToChar . intToBase 26

现在更容易看出需要用函数组合先消去css:

-- f1 (f2 x) == f1 . f2
-- f1 = fmap f
-- f2 = getIndex s
getName s = fmap f . (getIndex s)
    where f = map intToChar . intToBase 26

然后 s:

-- f1 (f2 x) = f1 . f2
-- f1 = (fmap f .)
-- f2 = getIndex
getName = (fmap f . ) . getIndex
    where f = map intToChar . intToBase 26

也许需要加上括号。首先,您尝试应用的原则是有效的。也就是说,如果一个变量是函数的最后一个参数并且是函数中 top-level 调用的最后一个参数,则可以有效地“right-cancel”这两个变量,所以如果你有

prependMy x = mappend "my" x

然后你可以安全地“取消”x并得到等效的(模单态限制,这里不适用但有时会咬你)

prependMy = mappend "my"

现在,这是你的表情。

getName s css = map intToChar .  intToBase 26 <$> getIndex s css

为了清楚起见,现在相同的东西加上一些括号

getName s css = (map intToChar .  intToBase 26) <$> (getIndex s css)

现在 css 不在 top-level 上了。以前不是,现在更清楚不是了。如果您可以重新排列表达式,使 scss 实际上位于 top-level 调用处,那么您仍然可以取消。在这种情况下,您可以通过执行以下操作来摆脱 css

getName s css = (map intToChar .  intToBase 26) <$> (getIndex s css)
getName s css = ((map intToChar .  intToBase 26) <$>) (getIndex s css)
getName s css = (((map intToChar .  intToBase 26) <$>) . getIndex s) css
getName s = ((map intToChar .  intToBase 26) <$>) . getIndex s

然后你可以按照同样的逻辑去掉s

getName s = ((map intToChar .  intToBase 26) <$>) . (getIndex s)
getName s = (((map intToChar .  intToBase 26) <$>) .) (getIndex s)
getName s = ((((map intToChar .  intToBase 26) <$>) .) . getIndex) s
getName = (((map intToChar .  intToBase 26) <$>) .) . getIndex

但这并不比原来的更具可读性。像这样删除参数称为 pointfreeing 函数。它可以基本上对任何功能完成,但应该谨慎地完成,因为你很容易以难以理解的方式结束。

首先让我们eta-reduce讨论一个问题。这就需要把运算符改造成section

getName s css = map intToChar . intToBase 26 <$> getIndex s css

getName s = (map intToChar . intToBase 26 <$>) . getIndex s

又名

getName s = fmap (map intToChar . intToBase 26) . getIndex s

现在,可以将函数 post-composition 视为另一个函子操作,但需要明确提及:

getName s = fmap (fmap (map intToChar . intToBase 26)) (getIndex s)

这很尴尬。一些库定义了像 <<$>> 这样的运算符,允许将其压缩为

getName s = map intToChar . intToBase 26 <<$>> getIndex s

但这并不能很好地扩展——您需要为每个嵌套深度使用不同的运算符。不过,我们当然可以自己定义它:

infixl 4 <<<$>>>
(<<<$>>>) :: (Functor f, Functor g, Functor h)
            => (a->b) -> f (g (h a)) -> f (g (h b))
(<<<$>>>) = fmap . fmap . fmap

getName = map intToChar . intToBase 26 <<<$>>> getIndex

或者您可以合并,即将 compose 不同的仿函数合并为一个:

getName s = getCompose $ map intToChar . intToBase 26 <$> Compose (getIndex s)

...其中 kind-of 也扩展到包括另一个参数

getName = getCompose . getCompose $ map intToChar . intToBase 26 <$> Compose (Compose getIndex)

但这显然无法战胜之前的任何形式。同样的事情也可以用 ReaderT monad transformer 来表达,但那样变得同样不可读。对于涉及部分应用组合运算符的各种 point-free 恶作剧也是如此。

tl;dr 这不值得,只要保持原样即可。


一般来说 不是 (c->) 仿函数的粉丝。 Haskell 有足够多的仿函数,但它对普通的旧函数有很好的特殊语法,所以为什么不直接使用它并弄清楚什么仿函数只是一个函数组合。