На любом языке, который имеет изменчивость, рекурсию или и то и другое, можно вызвать функцию с указателем на себя.По сути, все обычные полные языки Тьюринга обладают такими возможностями, поэтому ответов так много.
Реальный вопрос в том, как печатать такие функции. Не строго типизированные языки (такие как C / C ++) или динамически (или постепенно ) типизированные не представляют интереса, поскольку они позволяют принудительное приведение типов в некоторой форме, чтов основном делает задачу тривиальной.Они полагаются на программиста, чтобы предоставить тип и принять его как должное.Поэтому нас должны интересовать строго типизированные языки со статической системой типов.
Если мы сосредоточимся на 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
.Это повторение, и по умолчанию многие компиляторы запрещают такие уравнения (т. Е. Уравнения, в которых переменные одного типа встречаются с обеих сторон уравнения, отсюда и название проверка вхождения ).Если эта проверка отключена, компилятор разрешит это и аналогичные определения.Тем не менее, было замечено, что проверка на наличие ошибок выявляет больше ошибок, чем запрещает правильно оформленные программы.Другими словами, когда запускается проверка вхождения, это скорее ошибка, чем преднамеренная попытка написать хорошо типизированную функцию.
Поэтому в реальной жизни программисты неохотно представляют эту опцию в своих системах сборки.Хорошей новостью является то, что если мы немного помассируем оригинальное определение, нам не нужны рекурсивные типы.Например, мы можем изменить определение на следующее:
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
вызывает эту функцию, она фактически вызывает себя через дополнительную ссылку.Мы также можем заметить, что перенос нашей функции в значение какого-либо другого вида 1) (использование, запись, или вариант, или объект) также позволит ваше определение.Мы даже можем указать этот дополнительный тип помощника как [@@unboxed]
, чтобы компилятор не вводил дополнительный бокс вокруг оболочки.Но это своего рода обман.Мы по-прежнему не будем передавать функцию себе, но объекту, который содержит эту функцию (даже несмотря на то, что оптимизация компилятора удалит эту дополнительную косвенность, с точки зрения системы типов, это все еще разные объекты, поэтому проверка вхожденияне срабатывает).Итак, нам все еще нужна некоторая косвенность, если мы не хотим включать рекурсивные типы.И давайте придерживаться простейшей формы косвенного обращения, функции run
, и попробуем обобщить этот подход.
На самом деле, наша функция run
является частным случаем более общей фиксированной точкикомбинатор .Мы можем параметризовать 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
Веб-сайту Олега Киселева являетсяотличный ресурс, который показывает множество способов определения комбинатора с фиксированной точкой в OCaml, Scheme и Haskell.
1) По сути, это то же самое, что и делегированный подход, который был показан в других ответах (включая языки с выводом типа, такие как Haskell и OCaml, и языки, которые этого не делают, например C ++ и C # ).