哪些编程语言支持以自身为参数的函数?

时间:2019-04-22 05:11:59

标签: ocaml type-inference type-systems hindley-milner anonymous-recursion

我正在做学术练习(为了个人成长)。我想找到一种编程语言,允许您定义能够接受自身(即指向自身的指针)作为参数的函数。

例如,在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 语言中具有全局类型推断的这类函数的示例。

7 个答案:

答案 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 ++
  • C
  • C#
  • Python
  • 方案

C ++

好吧,所以这不是您会想到的第一语言,而且绝对不是一种轻松的方法,但这很有可能。这是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

您也可以在C语言中执行此操作:

https://ideone.com/E1LkUW

#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);
}

C#

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);
    }
}

Python

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才能使方法匹配。