我正在做学术练习(为了个人成长)。我想找到一种编程语言,允许您定义能够接受自身(即指向自身的指针)作为参数的函数。
例如,在JavaScript中:
function foo(x, y) {
if (y === 0) return;
x(x, y - 1);
}
foo(foo, 10);
上面的代码将在 y 达到零之前恰好执行11次 foo(),导致递归终止。
我试图像这样在OCaml中定义类似的功能:
let rec foo x y = if y < 1 then "hi" else x x (y - 1);;
但是失败,出现类型错误:
Error: This expression has type 'a -> 'b -> 'c
but an expression was expected of type 'a
The type variable 'a occurs inside 'a -> 'b -> 'c
我想知道,是否可以在OCaml中定义这样的功能?我对OCaml特别感兴趣,因为我知道它具有全局类型推断系统。我想知道这样的功能是否与全局类型推断兼容。因此,我正在寻找 any 语言中具有全局类型推断的这类函数的示例。
答案 0 :(得分:6)
在具有可变性或递归性或两者兼有的任何语言中,都可以使用指向自身的指针来调用函数。基本上,所有传统的图灵完整语言都具有这些功能,因此有很多答案。
真正的问题是如何键入此类函数。键入Non strongly typed语言(如C / C ++)或dynamically(或gradually)并不重要,因为它们支持某种形式的类型强制,这基本上使任务变得微不足道。他们依靠程序员提供类型并将其视为理所当然。因此,我们应该对使用静态类型系统的严格类型语言感兴趣。
如果我们将重点放在OCaml上,那么如果您通过-rectypes
选项,编译器将接受您的定义,该选项将禁用发生检查,不允许使用递归类型。实际上,您的函数类型为('a -> int -> string as 'a) -> int -> string
# let foo x y = if y < 1 then "hi" else x x (y - 1);;
val foo : ('a -> int -> string as 'a) -> int -> string = <fun>
请注意,由于函数并不是真正的递归,因此此处不需要rec
。递归是类型('a -> int -> string as 'a)
,这里as
扩展到左括号,即括号'a = 'a -> int -> string
。这是一个重复发生,默认情况下,许多编译器不允许使用此类方程式(即,在方程式的两边出现相同类型变量的方程式,因此称为 occurrence check )。如果禁用此检查,则编译器将允许使用此定义。但是,据观察,发生检查所捕获的错误多于不允许格式正确的程序。换句话说,当触发事件检查时,它更有可能是错误,而不是故意编写类型良好的函数的尝试。
因此,在现实生活中,程序员不愿意将此选项引入其构建系统。好消息是,如果我们稍微修改一下原始定义,则实际上并不需要递归类型。例如,我们可以将定义更改为以下内容,
let foo x y = if y < 1 then "hi" else x (y - 1)
现在具有类型
val foo : (int -> string) -> int -> string = <fun>
也就是说,它是一个函数,它接受类型为(int -> string)
的另一个函数并返回类型为(int -> string)
的函数。因此,要运行foo
,我们需要向其传递一个递归调用foo
的函数,例如
let rec run y = foo run y
这是递归起作用的地方。是的,我们没有直接将函数传递给自身。相反,我们为它传递了一个引用foo
的函数,当foo
调用此函数时,实际上它是通过一个额外的引用来调用自身的。我们可能还会注意到,将函数包装为其他类型的值(sup> 1)(使用,记录,变体或对象)也将允许您进行定义。我们甚至可以将那些额外的帮助程序类型指定为[@@unboxed]
,以便编译器不会在包装程序周围引入额外的装箱。但这是一种作弊。我们仍然不会将函数传递给它自己,而是一个包含该函数的对象(即使编译器优化会从类型系统的角度删除这些额外的间接访问,这些对象仍然是不同的对象,因此发生检查是未触发)。因此,如果我们不想启用递归类型,我们仍然需要一些间接性。让我们继续使用最简单的间接形式run
函数,并尝试推广这种方法。
实际上,我们的run
函数是更通用的fixed-point combinator的特定情况。我们可以使用run
类型的任何函数对('a -> 'b) -> ('a -> 'b)
进行参数设置,以使其不仅适用于foo
:
let rec run foo y = foo (run foo) y
,实际上我们将其命名为fix
,
let rec fix f n = f (fix f) n
具有类型
val fix : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>
而且,我们仍然可以将其应用于foo
# fix foo 10
Oleg Kiselyov web site是一个极好的资源,它展示了在OCaml,Scheme和Haskell中定义定点组合器的多种方法。
1)基本上与委托方法相同,其他答案中也显示了这两种方法(包括具有类型推断的语言(例如Haskell和OCaml)以及没有这种类型的语言(例如C ++和C#)。
答案 1 :(得分:4)
您的OCaml函数需要递归类型,即包含直接引用其自身的类型。如果在运行OCaml时指定-rectypes
,则可以定义此类(并具有此类的值)。
以下是与您的功能的会话:
$ rlwrap ocaml -rectypes
OCaml version 4.06.1
# let rec foo x y = if y < 1 then "hi" else x x (y - 1);;
val foo : ('a -> int -> string as 'a) -> int -> string = <fun>
# foo foo 10;;
- : string = "hi"
#
默认情况下不支持递归类型,因为它们几乎总是由编程错误引起的。
答案 2 :(得分:3)
我可以写一些例子:
好吧,所以这不是您会想到的第一语言,而且绝对不是一种轻松的方法,但这很有可能。这是C ++,它在这里是因为他们说要写您所知道的内容:)哦,我不建议您出于学术兴趣而这样做。
#include <any>
#include <iostream>
void foo(std::any x, int y)
{
std::cout << y << std::endl;
if (y == 0)
return;
// one line, like in your example
//std::any_cast<void (*) (std::any, int)>(x)(x, y - 1);
// or, more readable:
auto f = std::any_cast<void (*) (std::any, int)>(x);
f(x, y - 1);
}
int main()
{
foo(foo, 10);
}
如果强制转换太多(又太丑陋),则可以编写像波纹管这样的小包装纸。但是最大的优势是性能:您完全绕过了std::any
重类型。
#include <iostream>
class Self_proxy
{
using Foo_t = void(Self_proxy, int);
Foo_t* foo;
public:
constexpr Self_proxy(Foo_t* f) : foo{f} {}
constexpr auto operator()(Self_proxy x, int y) const
{
return foo(x, y);
}
};
void foo(Self_proxy x, int y)
{
std::cout << y << std::endl;
if (y == 0)
return;
x(x, y - 1);
}
int main()
{
foo(foo, 10);
}
以及包装程序的通用版本(为简便起见,省略转发):
#include <iostream>
template <class R, class... Args>
class Self_proxy
{
using Foo_t = R(Self_proxy<R, Args...>, Args...);
Foo_t* foo;
public:
constexpr Self_proxy(Foo_t* f) : foo{f} {}
constexpr auto operator()(Self_proxy x, Args... args) const
{
return foo(x, args...);
}
};
void foo(Self_proxy<void, int> x, int y)
{
std::cout << y << std::endl;
if (y == 0)
return;
x(x, y - 1);
}
int main()
{
foo(foo, 10);
}
您也可以在C语言中执行此操作:
#include <stdio.h>
typedef void(* dummy_f_type)(void);
void foo(dummy_f_type x, int y)
{
printf("%d\n", y);
if (y == 0)
return;
void (* f) (dummy_f_type, int) = (void (*) (dummy_f_type, int)) x;
f(x, y - 1);
}
int main()
{
foo((dummy_f_type)foo, 10);
}
这里要避免的陷阱是,不能将void*
用作x
的类型,因为将指针类型转换为数据指针类型是无效的。
或者,如注释中的leushenko所示,您可以将相同的模式与包装一起使用:
#include <stdio.h>
struct RF {
void (* f) (struct RF, int);
};
void foo(struct RF x, int y)
{
printf("%d\n", y);
if (y == 0)
return;
x.f(x, y - 1);
}
int main()
{
foo((struct RF) { foo }, 10);
}
https://dotnetfiddle.net/XyDagc
using System;
public class Program
{
public delegate void MyDelegate (MyDelegate x, int y);
public static void Foo(MyDelegate x, int y)
{
Console.WriteLine(y);
if (y == 0)
return;
x(x, y - 1);
}
public static void Main()
{
Foo(Foo, 10);
}
}
https://repl.it/repls/DearGoldenPresses
def f(x, y):
print(y)
if y == 0:
return
x(x, y - 1)
f(f, 10)
最后是一种功能语言
https://repl.it/repls/PunyProbableKernelmode
(define (f x y)
(print y)
(if (not (= y 0)) (x x (- y 1)))
)
(f f 10)
答案 3 :(得分:3)
正如Jeffrey指出的那样,如果激活-rectypes,OCaml可以处理此问题。而且,默认情况下未打开它的原因不是因为它是ML样式类型推断的问题,而是通常对程序员没有帮助(掩盖了编程错误)。
即使没有-rectypes模式,您也可以通过辅助类型定义轻松构造等效函数。例如:
type 'a rf = {f : 'a rf -> 'a}
let rec foo x y = if y < 1 then "hi" else x.f x (y - 1)
请注意,这仍然会推断其他所有内容,例如其他函数参数。样品使用:
foo {f = foo} 11
编辑:就ML类型推断而言,有-rectypes和没有-rectypes的算法之间的唯一区别是,后者在统一期间省略了 curss-check 。也就是说,在某种意义上,使用-rectypes,推理算法实际上变得“更简单”。当然,这假定类型可以适当地表示为允许循环的图形(有理树)。
答案 4 :(得分:2)
一种不可思议的递归/迭代语言(您要的名称)是Lisp的方言,称为Scheme。签出一本名为SICP的书籍来学习这种语言。调用self 是一种实现匿名递归的技术。
这是您的程序在Scheme中的外观:
(define (foo x y)
(if (= y 0) null (x x (- y 1))))
(foo foo 10)
答案 5 :(得分:2)
为完整起见,Haskell。
newtype U a = U { app :: U a -> a }
foo :: Int -> ()
foo y = f (U f) y
where
f x y | y <= 0 = ()
| otherwise = app x x (y-1)
尝试:
> foo 10
()
静态类型的语言似乎或多或少都在实现这一目标上:将函数放入记录中并将其作为参数传递给自身。 Haskell的newtype
会创建临时的“记录”,因此实际上是在运行时该函数本身。
动态类型化的语言只是将自我传递给自我,并以此完成。
答案 6 :(得分:0)
您可以在支持函数指针的C语言,支持delegate
的C#语言和Java语言中进行操作,在Java语言中,您可能需要声明自己的@FunctionalInterface
才能使方法匹配。