Scheme:使用fold实现n-argument compose

时间:2009-11-07 14:21:54

标签: functional-programming scheme

我正在尝试在Scheme中找到多参数“compose”的“最佳”实现(我知道它在某些实现中是内置的,但我假设目前我正在使用没有这个的那个)

对于2参数撰写函数,我有这个:

(define compose
  (lambda (f g)
    (lambda x
      (f (apply g x)))))

这样做的好处是,如果最右边的函数需要额外的参数,那么它们仍然可以通过组合函数传递。这具有令人满意的特性,即在某事物之上组成身份功能不会改变功能。

例如:

(define identity
  (lambda (x) x))

(define list1
  (compose identity list))

(define list2
  (compose identity list1))

(list2 1 2 3)
> (1 2 3)

现在做一个“n-argument”compose我可以这样做:

(define compose-n
  (lambda args
    (foldr compose identity args)))

((compose-n car cdr cdr) '(1 2 3))
> 3

但是这不再保留那个漂亮的“身份”属性:

((compose-n identity list) 1 2 3)
> procedure identity: expects 1 argument, given 3: 1 2 3

问题是用于foldr命令的“初始”函数。它已建成:

(compose identity (compose list identity))

所以......我不确定最好的解决方法。 “foldl”似乎是更好的自然选择,因为我希望它从左边的“身份”开始而不是 ......

但这是一个天真的实施:

(define compose-n
  (lambda args
    (foldl compose identity args)))

有效(必须颠倒函数应用程序的顺序):

((compose-n cdr cdr car) '(1 2 3))
> 3

没有解决问题,因为现在我最终不得不将身份功能放在左边!

((compose-n cdr cdr car) '(1 2 3))
> procedure identity: expects 1 argument, given 3: 1 2 3

就像,我需要使用“foldr”但需要一些不同于身份功能的“初始”值...或更好的身份功能?显然我在这里很困惑!

我想实现它没有必须编写一个显式的尾递归“循环”......似乎应该有一个优雅的方法来做到这一点,我只是卡住了。

4 个答案:

答案 0 :(得分:4)

您可能想尝试this version(使用SRFI 1中的reduce):

(define (compose . fns)
  (define (make-chain fn chain)
    (lambda args
      (call-with-values (lambda () (apply fn args)) chain)))
  (reduce make-chain values fns))

这不是火箭科学:当我在#scheme IRC频道上发布此消息时,Eli注意到这是compose的标准实现。 :-)(作为奖励,它也适用于您的示例。)

答案 1 :(得分:4)

OP提到(在对我的回答的评论中)他对Scheme的实现没有call-with-values。这是一种伪造它的方法(如果你可以确保<values>符号永远不会在你的程序中使用:你可以用(void)(if #f #f)或任何你不喜欢的东西替换它使用过,并且得到了实施的支持):

(define (values . items)
  (cons '<values> items))

(define (call-with-values source sink)
  (let ((val (source)))
    (if (and (pair? val) (eq? (car val) '<values>))
        (apply sink (cdr val))
      (sink val))))

这样做是因为它伪造了一个带有<values>符号标题的列表的多值对象。在call-with-values网站,它会检查此符号是否存在,如果没有,则会将其视为单个值。

如果你的链中最左边的函数可能返回一个多值,你的调用代码必须准备好解压缩<values> - 头列表。 (当然,如果您的实现没有多个值,那么您可能不会非常关注它。)

答案 2 :(得分:2)

这里的问题是你试图混合不同的程序。你可能想要curry list然后这样做:

(((compose-n (curry list) identity) 1) 2 3)

但那并不是很令人满意。

您可以考虑使用n-ary身份功能:

(define id-n
  (lambda xs xs))

然后你可以创建一个专门用于编写n元函数的撰写程序:

(define compose-nary
  (lambda (f g)
    (lambda x
      (flatten (f (g x))))))

使用以下命令编写任意数量的n元函数:

(define compose-n-nary
  (lambda args
    (foldr compose-nary id-n args)))

哪个有效:

> ((compose-n-nary id-n list) 1 2 3)
(1 2 3)

编辑:根据类型进行思考会有所帮助。让我们为我们的目的发明一种类型表示法。我们将对的类型表示为(A . B),将列表类型表示为[*],其约定为[*]等同于(A . [*]) A是列表的car的类型(即列表是一对原子和列表)。让我们进一步将函数表示为(A => B),意思是“取A并返回B”。 =>.都与右侧相关联,因此(A . B . C)等于(A . (B . C))

现在......鉴于此,这里是list的类型(读::为“有类型”):

list :: (A . B) => (A . B)

这是身份:

identity :: A => A

种类有所不同。 list的类型由两个元素构成(即列表的类型具有种类* => * => *),而identity的类型由一种类型构成(标识的类型具有种类* => *

作文有这种类型:

compose :: ((A => B).(C => A)) => C => B

查看将compose应用于listidentity时会发生什么。 Alist函数的域统一,因此它必须是一对(或空列表,但我们会掩盖它)。 Cidentity函数的域统一,因此它必须是原子。那么两者的组成必须是一个带有原子C并产生一个列表B的函数。如果我们只给这个函数原子,这不是问题,但是如果我们给它列表,它会窒息,因为它只需要一个参数。

以下是咖喱的帮助:

curry :: ((A . B) => C) => A => B => C

curry应用于list,您可以看到会发生什么。 list的输入与(A . B)统一。结果函数接受一个原子(汽车)并返回一个函数。该函数依次获取列表的其余部分(类型为B的cdr),最后生成列表。

重要的是,curry list函数与identity属于同一类,因此可以毫无问题地编写它们。这也是另一种方式。如果您创建一个带成对的标识函数,它可以使用常规list函数组合。

答案 3 :(得分:1)

虽然将“空”列表转移到身份功能本身会很好,但放弃这个似乎会产生以下结果,这不是太糟糕:

(define compose-n
  (lambda (first . rest)
    (foldl compose first rest)))

((compose-n cdr cdr car) '(1 2 3))

((compose-n list identity identity) 1 2 3)