我正在尝试在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”但需要一些不同于身份功能的“初始”值...或更好的身份功能?显然我在这里很困惑!
我想实现它没有必须编写一个显式的尾递归“循环”......似乎应该有一个优雅的方法来做到这一点,我只是卡住了。
答案 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
应用于list
和identity
时会发生什么。 A
与list
函数的域统一,因此它必须是一对(或空列表,但我们会掩盖它)。 C
与identity
函数的域统一,因此它必须是原子。那么两者的组成必须是一个带有原子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)