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

Vla*_*ala 25 haskell currying pointfree

(.)采用两个函数,它们接受一个值并返回一个值:

(.) :: (b -> c) -> (a -> b) -> a -> c
Run Code Online (Sandbox Code Playgroud)

既然(.)两个参数,我觉得(.).(.)应该是无效的,但它完全没问题:

(.).(.) :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c
Run Code Online (Sandbox Code Playgroud)

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

J. *_*son 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'')
Run Code Online (Sandbox Code Playgroud)

然后我们开始第二次,但很快陷入困境......

let a = (b'' -> c'')
Run Code Online (Sandbox Code Playgroud)

这是关键:我们想要let b = (a'' -> b'') -> a'' -> c'',但我们已经定义了b,所以我们必须尝试统一 ---尽可能地匹配我们的两个定义.幸运的是,他们确实匹配

UNIFY b = (b' -> c') =:= (a'' -> b'') -> a'' -> c''
which implies 
      b' = a'' -> b''
      c' = a'' -> c''
Run Code Online (Sandbox Code Playgroud)

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

((.) (.) (.) :: (b'' -> c'') -> (a' -> b') -> (a' -> c'))
Run Code Online (Sandbox Code Playgroud)

然后扩大

((.) (.) (.) :: (b'' -> c'') -> (a' -> a'' -> b'') -> (a' -> a'' -> c''))
Run Code Online (Sandbox Code Playgroud)

并清理它

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

(.).(.) :: (b -> c) -> (a -> a1 -> b) -> (a -> a1 -> c)
Run Code Online (Sandbox Code Playgroud)

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


这是直觉.先来看看fmap

fmap :: (a -> b) -> (f a -> f b)
Run Code Online (Sandbox Code Playgroud)

它将一个功能"提升"为一个Functor.我们可以反复应用它

fmap.fmap.fmap :: (Functor f, Functor g, Functor h) 
               => (a -> b) -> (f (g (h a)) -> f (g (h b)))
Run Code Online (Sandbox Code Playgroud)

允许我们将功能提升到越来越深的层次Functors.

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

instance Functor ((->) r) where
   fmap = (.)
Run Code Online (Sandbox Code Playgroud)

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

或者喜欢有多个不受fmaping 影响的输入参数.类似于在(> 1)arity函数的"只是结果"上组成新的延续函数.


最后值得注意的是,如果你认为这些东西很有意思,它就形成了在控制中衍生镜片的核心直觉.镜头.

  • 圣球.你的直觉部分突然让这一点变得更加清晰. (6认同)

Jon*_*rdy 15

让我们暂时忽略类型,只使用lambda演算.

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

  • ETA-扩大:
    (\ 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可以代表任何类型,包括函数类型.看到连接?


Tox*_*ris 8

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

id :: a -> a
id x = x
Run Code Online (Sandbox Code Playgroud)

id的类型表示id应该采用一个参数.事实上,我们可以用一个参数来称呼它:

> id "hello" 
"hello"
Run Code Online (Sandbox Code Playgroud)

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

> id not True
False
Run Code Online (Sandbox Code Playgroud)

甚至:

> id id "hello"
"hello"
Run Code Online (Sandbox Code Playgroud)

到底是怎么回事?理解的关键id not True是首先看一下id not.显然,这是允许的,因为它将id应用于一个参数.类型notBool -> Bool,所以我们知道afrom的类型应该是Bool -> Bool,所以我们知道这个id的出现有类型:

id :: (Bool -> Bool) -> (Bool -> Bool)
Run Code Online (Sandbox Code Playgroud)

或者,括号较少:

id :: (Bool -> Bool) -> Bool -> Bool
Run Code Online (Sandbox Code Playgroud)

所以这种id的出现实际上需要两个参数.

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


Chr*_*lor 8

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

class Functor f where
  fmap :: (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)

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

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

但是模式f (g x)(f . g) x我们写的完全相同

>>> (fmap . fmap) (+10) xs
[[11,12,13],[14,15,16]]
Run Code Online (Sandbox Code Playgroud)

是什么类型的fmap . fmap

>>> :t fmap.fmap
  :: (Functor g, Functor f) => (a -> b) -> f (g a) -> f (g b)
Run Code Online (Sandbox Code Playgroud)

我们看到它根据我们的需要映射了两层仿函数.但是现在请记住,这(->) r是一个仿函数(函数的类型r,你可能更喜欢读它(r ->))和它的仿函数实例

instance Functor ((->) r) where
  fmap f g = f . g
Run Code Online (Sandbox Code Playgroud)

对于一个功能,fmap只是功能组合!当我们编写两个fmaps时,我们映射函数仿函数的两个级别.我们最初有一些类型(->) s ((->) r a),相当于s -> r -> a,我们最终得到一些类型s -> r -> b,所以(.).(.)必须是类型

(.).(.) :: (a -> b) -> (s -> r -> a) -> (s -> r -> b)
Run Code Online (Sandbox Code Playgroud)

它接受第一个函数,并用它来转换第二个(双参数)函数的输出.因此,例如,函数((.).(.)) show (+)是两个参数的函数,首先将其参数添加到一起,然后将结果转换为String使用show:

>>> ((.).(.)) show (+) 11 22
"33"
Run Code Online (Sandbox Code Playgroud)

fmap例如,考虑更长的链条就有了自然的概括

fmap.fmap.fmap ::
  (Functor f, Functor g, Functor h) => (a -> b) -> f (g (h a)) -> f (g (h b))
Run Code Online (Sandbox Code Playgroud)

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

(.).(.).(.) :: (a -> b) -> (r -> s -> t -> a) -> (r -> s -> t -> b)
Run Code Online (Sandbox Code Playgroud)

例如

>>> import Data.Map
>>> ((.).(.).(.)) show insert 1 True empty
"fromList [(1,True)]"
Run Code Online (Sandbox Code Playgroud)

它将值True插入带有键的空映射1,然后将输出转换为带有的字符串show.


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

(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(.:) = (.).(.)
Run Code Online (Sandbox Code Playgroud)

这样你就可以写了

>>> let f = show .: (+)
>>> f 10 20
"30"
Run Code Online (Sandbox Code Playgroud)

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

(.:) :: (a -> b) -> (r -> s -> a) -> (r -> s -> b)
(f .: g) x y = f (g x y)
Run Code Online (Sandbox Code Playgroud)

这可能有助于神秘化(.).(.).