В чем преимущества letrec как функции языка программирования

Я посмотрел на все, что я могу найти о letrec, и я до сих пор не понимаю, что он привносит в язык как функцию. Кажется, что все, что можно выразить с помощью letrec, можно так же легко написать как рекурсивную функцию. Но есть ли причины выставлять letrec как особенность языка программирования, если язык уже поддерживает рекурсивные функции? Почему несколько языков выставляют оба?

Я понимаю, что letrec может использоваться для реализации других функций, включая рекурсивные функции, но это не имеет отношения к тому, почему он сам должен быть функцией. Я также читал, что некоторые люди находят его более читабельным, чем рекурсивные функции в некоторых списках, но опять-таки это не актуально, поскольку разработчик языка может приложить усилия, чтобы сделать рекурсивные функции достаточно читабельными, чтобы не нуждаться в другой функции. Наконец, мне сказали, что letrec позволяет более кратко выразить некоторые виды рекурсивных значений, но я пока не нашел мотивирующего примера.

2 ответа

TL;DR: define является letrec, Это то, что позволяет нам писать рекурсивные определения в первую очередь.

Рассматривать

let fact = fun (n => (n==0 -> 1 ; n * fact (n-1)))

К какому объекту относится имя fact внутри тела это определение относится? С let foo = val, val определяется в терминах уже известных объектов, поэтому он не может ссылаться на foo который еще не определен. С точки зрения объема это можно сказать (и обычно так), что RHS let Уравнение определяется во внешнем объеме.

Единственный путь для внутреннего fact на самом деле указать на тот, который определяется, это использовать letrec где определяемый объект может ссылаться на область, в которой он определяется. Таким образом, хотя определение объекта во время его определения является ошибкой, сохранение ссылки на его (будущее, на данный момент) значение вполне допустимо - в случае использования letrec то есть.

define вы ссылаетесь, это просто letrec под другим именем. В схеме тоже.

Без возможности определения сущности для ссылки на себя, т.е. на языки с нерекурсивным let Чтобы иметь рекурсию, нужно прибегнуть к использованию тайных устройств, таких как y-комбинатор. Что громоздко и обычно неэффективно. Другим способом являются такие определения, как

let fact = (fun (f => f f)) (fun (r => n => (n==0 -> 1 ; n * r r (n-1))))

Так letrec сводит к минимуму эффективность реализации и удобство для программиста.

Тогда возникает вопрос, зачем выставлять нерекурсивный let? Haskell действительно нет. Схема имеет как letrec а также let, Одной из причин может быть полнота. Другой может быть более простой реализацией для let с меньшим количеством самоссылочных структур во время выполнения, облегчающих работу сборщика мусора.

Вы просите мотивационный пример. Подумайте об определении чисел Фибоначчи в виде само-ссылочного ленивого списка:

letrec fibs = {0} + {1} + add fibs (tail fibs) 

С нерекурсивным let еще одна копия списка fibs будет определен для использования в качестве входных данных для функции поэлементного сложения add, Что приведет к определению другой копии fibs для этого, чтобы быть определенным в его терминах. И так далее; доступ к n- му числу Фибоначчи приведет к созданию и ведению цепочки из n-1 списков во время выполнения! Не красивая картинка.

И это при условии, что то же самое fibs был использован для tail fibs также. Если нет, все ставки сняты.

Что нужно, так это fibs использует себя, ссылается на себя, поэтому поддерживается только одна копия списка.

NB. Хотя это не специфическая проблема Схемы, я использую Схему для демонстрации различий. Надеюсь, вы можете прочитать немного кода LISP

letrec это просто особенный let где сами привязки определяются до того, как выражения, представляющие их значения, будут оценены. Вообразите это:

(define (fib n)
  (let ((fib (lambda (n a b) 
               (if (zero? n) 
                   a 
                   (fib (- n 1) b (+ a b))))))
    (fib n))

Этот код не работает с тех пор как fib существует в теле let оно существует в замыкании, которое оно определяет, поскольку привязка не существовала, когда вычислялась лямбда Чтобы исправить это letrec на помощь приходит

(define (fib n)
  (letrec ((fib (lambda (n a b) 
                  (if (zero? n) 
                      a 
                      (fib (- n 1) b (+ a b))))))
    (fib n))

Тот letrec это просто синтаксис, который делает что-то вроде этого:

(define (fib n)
  (let ((fib 'undefined))
    (let ((tmp (lambda (n a b) 
                 (if (zero? n) 
                     a 
                     (fib (- n 1) b (+ a b))))))
      (set! fib tmp))
    (fib n)))

Так что здесь вы четко видите fib существует, когда вычисляется лямбда, и позднее привязка устанавливается на само замыкание. Привязка такая же, только ее указатель изменился. Это круговая ссылка 101..

Так что же происходит, когда вы делаете глобальную функцию? Ясно, что если он должен повторяться, он должен существовать до того, как лямбда будет оценена, или среда должна быть мутирована. Здесь нужно исправить ту же проблему.

В реализации функционального языка, где мутация не в порядке, вы можете решить эту проблему с помощью Y (или Z) комбинатора.

Если вам интересно, как реализованы языки, советую начать со статей Мэтта Майтса.

Другие вопросы по тегам