Каков стандартный способ написания вложенных инструкций определения (например, в схеме) для clojure?

Все примеры взяты из книги SICP: http://sicpinclojure.com/?q=sicp/1-3-3-procedures-general-methods

Это было мотивировано из серии видео MIT на LISP - http://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-001-structure-and-interpretation-of-computer-programs-spring-2005/video-lectures/2a-higher-order-procedures/

В схеме вы можете поставить 'define' внутри другого 'define':

(define (close-enough? v1 v2)
    (define tolerance 0.00001)
    (< (abs (- v1 v2)) tolerance ) )

В clojure существует оператор let, с той лишь разницей, что он вложен:

(defn close-enough? [v1 v2]
    (let [tolerance 0.00001]
         (< (Math/abs (- v1 v2) ) 
            tolerance) ) )

Но как насчет переписывания в clojure чего-то большего?:

(define (sqrt x)
  (define (fixed-point f first-guess)
    (define (close-enough? v1 v2)
      (define tolerance 0.00001)
      (< (abs (- v1 v2)) tolerance))
    (define (try guess)
      (let ((next (f guess)))
         (if (close-enough? guess next)
            next
            (try next))))
    (try first-guess))
  (fixed-point (lambda (y) (average y (/ x y)))
           1.0))

Это действительно работает, но выглядит очень нетрадиционно...

(defn sqrt [n]
  (let [precision       10e-6
        abs             #(if (< % 0) (- %) %)
        close-enough?   #(-> (- %1 %2) abs (< precision))
        averaged-func   #(/ (+ (/ n %) %) 2)
        fixed-point     (fn [f start]
                            (loop [old start
                                   new (f start)]
                                (if (close-enough? old new) 
                                    new
                                    (recur new (f new) ) ) ) )]

        (fixed-point averaged-func 1) ) )

 (sqrt 10)

ОБНОВЛЕНО Мар /8/2012

Спасибо за ответ!

По существу "letfn" не слишком отличается от "let" - вызываемые функции должны быть вложены в определение letfn (в отличие от Схемы, где функции используются в следующем sexp после его определений и только существующих в рамках функции верхнего уровня, в которой она определена).

Итак, еще один вопрос... Почему clojure не дает возможности делать какую-либо схему? Это какое-то решение для языкового дизайна? Что мне нравится в организации схемы:

  • 1) Инкапсуляция идей, чтобы я, как программист, имел представление о том, какие маленькие блоки используются более крупным блоком, особенно если я использую только маленькие блоки один раз в большом блоке (по какой-либо причине, даже если маленькие блоки полезны сами по себе).

  • 2) Это также останавливает загрязнение пространства имен небольшими процедурами, которые не являются полезными для конечного пользователя (я написал программы clojure, вернулся к ним через неделю и должен был повторно изучить мой код потому что он был в плоской структуре, и я чувствовал, что я смотрю на код наизнанку, а не сверху вниз).

  • 3) Интерфейс определения общего метода, поэтому я могу вытащить конкретный под-метод, де-отступ, который он тестирует, и вставить измененную версию обратно, не слишком много возиться.

Почему это не реализовано в clojure?

Ответ 1

стандартным способом записи вложенных, именованных процедур в clojure является использование letfn.

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

Ответ 2

Люди, критикующие его размещение этого во всей одной функции, не понимают контекста того, почему это было сделано таким образом. SICP, откуда и взялся пример, пытался проиллюстрировать концепцию модуля, но без добавления каких-либо других конструкций на базовый язык. Таким образом, "sqrt" - это модуль с одной функцией в своем интерфейсе, а остальные - локальные или частные функции внутри этого модуля. Я полагаю, что это было основано на схеме R5RS, и с тех пор, с тех пор мы добавили стандартную конструкцию модуля, я думаю (?). Но независимо от того, что он больше демонстрирует принцип скрытия реализации.

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

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

Ответ 3

letfn является стандартным способом.

Но так как Clojure является Lisp, вы можете создать (почти) любую семантику, которую хотите. Здесь доказательство понятия, определяющее define в терминах letfn.

(defmacro define [& form]
  (letfn [(define? [exp]
            (and (list? exp) (= (first exp) 'define)))
          (transform-define [[_ name args & exps]]
            `(~name ~args
               (letfn [[email protected](map transform-define (filter define? exps))]
                 [email protected](filter #(not (define? %)) exps))))]
    `(defn [email protected](transform-define `(define [email protected])))))

(define sqrt [x]
  (define average [a b] (/ (+ a b) 2))
  (define fixed-point [f first-guess]
    (define close-enough? [v1 v2]
      (let [tolerance 0.00001]
        (< (Math/abs (- v1 v2)) tolerance)))
    (define tryy [guess]
      (let [next (f guess)]
        (if (close-enough? guess next)
          next
          (tryy next))))
    (tryy first-guess))
  (fixed-point (fn [y] (average y (/ x y)))
               1.0))

(sqrt 10)   ; => 3.162277660168379

Для реального кода вы хотите изменить define, чтобы вести себя как R5RS: разрешить не-fn-значения, быть доступными в defn, defmacro, let, letfn и fn, и убедитесь, что внутренние определения находятся в начале охватывающего тела.

Примечание. Мне пришлось переименовать try в tryy. По-видимому, try представляет собой специальную нефункциональную, немакросоставную конструкцию, для которой переопределение тихо проваливается.