Функциональная альтернатива «let»

#clojure

#clojure

Вопрос:

Я обнаружил, что пишу много clojure таким образом:

 (defn my-fun [input]
  (let [result1 (some-complicated-procedure input) 
        result2 (some-other-procedure result1)]
    (do-something-with-results result1 result2)))
  

Это let утверждение кажется очень… обязательно. Что мне не нравится. В принципе, я мог бы написать такую же функцию, как эта:

 (defn my-fun [input]
    (do-something-with-results (some-complicated-procedure input) 
                               (some-other-procedure (some-complicated-procedure input)))))
  

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

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

 (defn some-complicated-procedure [input] (lots (of (nested (operations input))))) 
(defn my-fun [input]
    (do-something-with-results (some-complicated-procedure input) 
                               (some-other-procedure (some-complicated-procedure input)))))

  

Например, это опасно, потому что рефакторинг сложный:

 (defn my-fun [input]
    (do-something-with-results (lots (of (nested (operations (mistake input))))) ; oops made a change here that wasn't applied to the other nested calls
                               (some-other-procedure (lots (of (nested (operations input))))))))
  

Учитывая эти компромиссы, я чувствую, что у меня нет никаких альтернатив написанию длинных императивных let утверждений, но когда я это делаю, я не могу избавиться от ощущения, что я не пишу идиоматический clojure. Есть ли способ, которым я могу решить проблемы с вычислениями и чистотой кода, поднятые выше, и написать идиоматический clojure? Являются let ли императивные выражения идиоматическими?

Комментарии:

1. Может быть полезен более конкретный пример. Я считаю, что ответ будет зависеть от того, что some-complicated-procedure и some-other-procedure и do-something-with-results делают. Но, как упоминалось ниже @andy_fingerhut и @Charles Duffy, это идиоматично с точки зрения let .

2. Да, let это идиоматично. Если это помогает вам лучше спать по ночам, подумайте о (let [a (f x), b (g a y)] (h a b)) синтаксическом сахаре для ((fn [a] ((fn [b] (h a b)) (g a y))) (f x)) , если мне удалось правильно расставить скобки, потому что это, по сути, так и есть. Другими словами, ваш инстинкт использования вложенных функций для привязки результатов к именам — это то, что let формализуется в более удобном для чтения формате.

Ответ №1:

Описываемые let вами операторы могут напоминать вам императивный код, но в них нет ничего императивного. В Haskell также есть аналогичные инструкции для привязки имен к значениям внутри тел.

Комментарии:

1. Я бы сказал, что в них есть что-то обязательное, поскольку они имеют четко определенный порядок операций, в отличие от оценки связанных имен только по мере необходимости. С другой стороны, они, безусловно, идиоматичны.

Ответ №2:

Если в вашей ситуации действительно нужен молоток побольше, есть несколько молотков побольше, которые вы можете использовать или взять для вдохновения. Следующие две библиотеки предлагают некоторую форму привязки (похожую на let ) с локализованным запоминанием результатов, чтобы выполнять только необходимые шаги и повторно использовать их результаты, если это необходимо снова: Plumatic Plumbing, в частности, графическая часть; и многообразие Зака Телмана, let-flow форма которого, кроме того, организует асинхронные шаги для ожиданиянеобходимые входные данные должны стать доступными и выполняться параллельно, когда это возможно. Даже если вы решите сохранить свой текущий курс, их документы хорошо читаются, а сам код Manifold является образовательным.

Ответ №3:

Недавно у меня возник тот же вопрос, когда я посмотрел на этот код, который я написал

 (let [user-symbols (map :symbol states)
      duplicates (for [[id freq] (frequencies user-symbols) :when (> freq 1)] id)]
  (do-something-with duplicates))
  

Вы заметите, что map и for ленивы и не будут выполняться до do-something-with тех пор, пока не будут выполнены. Также возможно, что не все (или даже не все) states будут отображены или вычислены частоты. Это зависит от того, какие do-something-with на самом деле запросы последовательности возвращаются for . Это очень функциональное и идиоматическое функциональное программирование.

Ответ №4:

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

 (defn with-state [res-key f state]
  (assoc state res-key (f state)))

user> (with-state :res (comp inc :init) {:init 10})
;;=> {:init 10, :res 11}
  

итак, вы можете перейти к чему-то вроде этого:

 (->> {:init 100}
     (with-state :inc'd (comp inc :init))
     (with-state :inc-doubled (comp (partial * 2) :inc'd))
     (with-state :inc-doubled-squared (comp #(* % %) :inc-doubled))
     (with-state :summarized (fn [st] (apply   (vals st)))))

;;=> {:init 100,
;;    :inc'd 101,
;;    :inc-doubled 202,
;;    :inc-doubled-squared 40804,
;;    :summarized 41207}
  

Ответ №5:

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

 (defmacro my-let [bindings body]
  (if (empty? bindings)
    body
    `((fn [~(first bindings)]
        (my-let ~(rest (rest bindings)) ~body))
      ~(second bindings))))
  

Вот пример его вызова:

 (my-let [a 3
         b (  a 1)]
        (* a b))
;; => 12
  

И вот macroexpand-all вызванное выше выражение, которое показывает, как мы реализуем my-let использование анонимных функций:

 (clojure.walk/macroexpand-all '(my-let [a 3
                                        b (  a 1)]
                                       (* a b)))
;; => ((fn* ([a] ((fn* ([b] (* a b))) (  a 1)))) 3)
  

Обратите внимание, что расширение не зависит от let и что связанные символы становятся именами параметров в анонимных функциях.

Ответ №6:

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

Однако вы можете захотеть надрать шины моей маленькой библиотеки tl; dr, которая позволяет вам писать код, например

 (compute 
     (  a b c)
 where
     a (f b)
     c (  100 b))