组成功能组合:(。)。(。)如何工作?

时间:2013-07-11 05:26:12

标签: haskell currying pointfree

(.)有两个函数,它们使用一个值并返回一个值:

(.) :: (b -> c) -> (a -> b) -> a -> c

由于(.)需要两个参数,我觉得(.).(.)应该无效,但这很好:

(.).(.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c

这里发生了什么?我意识到这个问题措辞严厉......所有的功能都只是因为讨论而采取了一个论点。也许更好的方式是说这些类型不匹配。

7 个答案:

答案 0 :(得分:29)

让我们首先使用typechecker进行机械校样。我将在后面描述一种直观的思考方式。

我想将(.)应用于(.),然后我会将(.)应用于结果。第一个应用程序帮助我们定义一些变量的等价。

((.) :: (b -> c) -> (a -> b) -> a -> c) 
      ((.) :: (b' -> c') -> (a' -> b') -> a' -> c') 
      ((.) :: (b'' -> c'') -> (a'' -> b'') -> a'' -> c'')

let b = (b' -> c') 
    c = (a' -> b') -> a' -> c'

((.) (.) :: (a -> b) -> a -> c) 
      ((.) :: (b'' -> c'') -> (a'' -> b'') -> a'' -> c'')

然后我们开始第二次,但很快被卡住了......

let a = (b'' -> c'')

这是关键:我们想let b = (a'' -> b'') -> a'' -> c'',但我们已经定义了b,所以我们必须尝试统一 ---以匹配我们的两个定义为我们能做到最好。幸运的是,他们匹配

UNIFY b = (b' -> c') =:= (a'' -> b'') -> a'' -> c''
which implies 
      b' = a'' -> b''
      c' = a'' -> c''

通过这些定义/统一,我们可以继续申请

((.) (.) (.) :: (b'' -> c'') -> (a' -> b') -> (a' -> c'))

然后展开

((.) (.) (.) :: (b'' -> c'') -> (a' -> a'' -> b'') -> (a' -> a'' -> c''))

并清理它

substitute b'' -> b
           c'' -> c
           a'  -> a
           a'' -> a1

(.).(.) :: (b -> c) -> (a -> a1 -> b) -> (a -> a1 -> c)

,说实话,这有点违反直觉。


这是直觉。首先看一下fmap

fmap :: (a -> b) -> (f a -> f b)

它将一个功能“提升”为Functor。我们可以反复申请

fmap.fmap.fmap :: (Functor f, Functor g, Functor h) 
               => (a -> b) -> (f (g (h a)) -> f (g (h b)))

允许我们将功能提升到Functors的更深层次。

事实证明,数据类型(r ->)Functor

instance Functor ((->) r) where
   fmap = (.)

应该看起来很熟悉。这意味着fmap.fmap会转换为(.).(.)。因此,(.).(.)只是让我们转换(r ->) Functor更深层次的参数类型。 (r ->) Functor实际上是Reader Monad,因此分层Reader就像拥有多种独立的全局不可变状态。

或者喜欢有多个输入参数不受fmap影响。有点像在(> 1)arity函数的“只是结果”上组成一个新的延续函数。


最后值得注意的是,如果你觉得这些东西很有意思,那就形成了deriving the Lenses in Control.Lens背后的核心直觉。

答案 1 :(得分:15)

让我们暂时忽略类型,然后使用lambda演算。

  • Desugar中缀符号:
    (.) (.) (.)

  • 埃塔展开:
    (\ a b -> (.) a b) (\ c d -> (.) c d) (\ e f -> (.) e f)

  • 内联(.)的定义:
    (\ a b x -> a (b x)) (\ c d y -> c (d y)) (\ e f z -> e (f z))

  • 替换a
    (\ b x -> (\ c d y -> c (d y)) (b x)) (\ e f z -> e (f z))

  • 替换b
    (\ x -> (\ c d y -> c (d y)) ((\ e f z -> e (f z)) x))

  • 替换e
    (\ x -> (\ c d y -> c (d y)) (\ f z -> x (f z)))

  • 替换c
    (\ x -> (\ d y -> (\ f z -> x (f z)) (d y)))

  • 替换f
    (\ x -> (\ d y -> (\ z -> x (d y z))))

  • Resugar lambda表示法:
    \ x d y z -> x (d y z)

如果你问GHCi,你会发现它有预期的类型。为什么?因为函数箭头是右关联的以支持currying:类型(b -> c) -> (a -> b) -> a -> c实际上意味着(b -> c) -> ((a -> b) -> (a -> c))。同时,类型变量b可以代表任何类型,包括函数类型。看到连接?

答案 2 :(得分:7)

这是一个巧妙的案例,我认为首先掌握更一般的案例更简单,然后考虑具体案例。那么让我们考虑一下仿函数。我们知道仿函数提供了一种在结构上映射函数的方法 -

class Functor f where
  fmap :: (a -> b) -> f a -> f b

但是如果我们有两层仿函数呢?例如,列表列表?在这种情况下,我们可以使用两层fmap

>>> let xs = [[1,2,3], [4,5,6]]
>>> fmap (fmap (+10)) xs
[[11,12,13],[14,15,16]]

但模式f (g x)(f . g) x完全相同,所以我们可以编写

>>> (fmap . fmap) (+10) xs
[[11,12,13],[14,15,16]]

fmap . fmap的类型是什么?

>>> :t fmap.fmap
  :: (Functor g, Functor f) => (a -> b) -> f (g a) -> f (g b)

我们看到它根据我们的需要映射到两层仿函数。但现在请记住,(->) r是一个仿函数(来自r的函数类型,您可能更喜欢将其视为(r ->)),其函数实例为

instance Functor ((->) r) where
  fmap f g = f . g

对于一个函数,fmap只是函数组合!当我们编写两个fmap时,我们会映射函数仿函数的两个级别。我们最初的某些类型为(->) s ((->) r a),相当于s -> r -> a,我们最终得到s -> r -> b类型的内容,因此(.).(.)的类型必须为

(.).(.) :: (a -> b) -> (s -> r -> a) -> (s -> r -> b)

获取其第一个函数,并使用它来转换第二个(双参数)函数的输出。例如,函数((.).(.)) show (+)是两个参数的函数,首先将其参数相加,然后使用String将结果转换为show

>>> ((.).(.)) show (+) 11 22
"33"

然后考虑fmap的较长链,例如

,这是一种自然的概括
fmap.fmap.fmap ::
  (Functor f, Functor g, Functor h) => (a -> b) -> f (g (h a)) -> f (g (h b))

映射三层仿函数,相当于用三个参数组成的函数:

(.).(.).(.) :: (a -> b) -> (r -> s -> t -> a) -> (r -> s -> t -> b)

例如

>>> import Data.Map
>>> ((.).(.).(.)) show insert 1 True empty
"fromList [(1,True)]"

将值True插入到带有键1的空地图中,然后将输出转换为带有show的字符串。


这些功能通常很有用,因此您有时会将它们定义为

(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(.:) = (.).(.)

这样你就可以写

>>> let f = show .: (+)
>>> f 10 20
"30"

当然,可以给出(.:)更简单,有点的定义

(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(f .: g) x y = f (g x y)

可能有助于在某种程度上揭开(.).(.)的神秘面纱。

答案 3 :(得分:6)

以下是同一现象的一个更简单的例子:

id :: a -> a
id x = x

id的类型表明id应该带一个参数。事实上,我们可以用一个论点来称呼它:

> id "hello" 
"hello"

但事实证明我们也可以用两个参数来称呼它:

> id not True
False

甚至:

> id id "hello"
"hello"

发生了什么事?理解id not True的关键是首先看id not。显然,这是允许的,因为它将id应用于一个参数。 not的类型为Bool -> Bool,因此我们知道来自id类型的a应该是Bool -> Bool,因此我们知道此ID的出现有类型:

id :: (Bool -> Bool) -> (Bool -> Bool)

或者,括号较少:

id :: (Bool -> Bool) -> Bool -> Bool

因此,这种id的出现实际上需要两个参数

同样的推理也适用于id id "hello"(.) . (.)

答案 4 :(得分:4)

你是对的,(.)只有两个参数。你似乎只对haskell的语法感到困惑。在表达式(.).(.)中,它实际上是中间的点,它将另外两个点作为参数,就像表达式100 + 200一样,可以写成(+) 100 200

(.).(.) === (number the dots)
(1.)2.(3.) === (rewrite using just syntax rules)
(2.)(1.)(3.) === (unnumber and put spaces)
(.) (.) (.) ===

(.) (.) (.)开始,第一个(.)将第二个(.)和第三个(.)作为参数,这一点应该更加明确。

答案 5 :(得分:3)

是的,这是由于currying。 (.)因为Haskell中的所有函数只接受一个参数。你正在编写的是对每个组成的(.)的第一次部分调用,它采用了它的第一个参数(组合的第一个函数)。

答案 6 :(得分:1)

(首先阅读my answer函数组合,$运算符和无点样式。)

想象一下,你有一个简单的功能:它会累加2个数字,然后否定结果。我们称之为foo

foo a b = negate (a + b)

现在让我们一步一步让它变得无点,看看我们最终得到了什么:

foo a b = negate $ a + b
foo a b = negate $ (+) a b
foo a b = negate $ (+) a $ b
foo a b = negate . (+) a $ b
foo a   = negate . (+) a -- f x = g x is equivalent to f = g
foo a   = (.) negate ((+) a) -- any infix operator is just a function
foo a   = (negate.) ((+) a) -- (2+) is the same as ((+) 2)
foo a   = (negate.) $ (+) a
foo a   = (negate.) . (+) $ a
foo     = (negate.) . (+)
foo     = ((.) negate) . (+)
foo     = (.) ((.) negate) (+) -- move dot in the middle in prefix position
foo     = ((.) ((.) negate)) (+) -- add extra parentheses

现在让我们更仔细地分析表达式(.) ((.) negate)。它是(.)函数的部分应用,其第一个参数是((.) negate)。我们可以进一步改造它吗?是的,我们可以:

(.) ((.) negate)
(.) . (.) $ negate -- because f (f x) is the same as (f . f) x
(.)(.)(.) $ negate
((.)(.)(.)) negate

(.).(.)相当于(.)(.)(.),因为在第一个表达式中,中间的点可以在前缀位置移动并用括号括起来,这会产生第二个表达式。

现在我们可以重写foo函数:

foo = ((.).(.)) negate (+)
foo = ((.)(.)(.)) negate (+) -- same as previous one
foo = negate .: (+)
  where (.:) = (.).(.)

现在您知道(.).(.)等同于(\f g x y -> f (g x y))

(\f g x y -> f (g x y)) negate (+) 2 3 -- returns -5
((.).(.)) negate (+) 2 3 -- returns -5