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