Лучший способ записи нескольких, если чеки в функции Clojure?

У меня есть функция Clojure, которая выглядит примерно так:

(defn calculate-stuff [data]
  (if (some-simple-validation data)
    (create-error data)
    (let [foo (calculate-stuff-using data)]
      (if (failed? foo)
        (create-error foo)
        (let [bar (calculate-more-stuff-using foo)]
          (if (failed? bar)
            (create-error bar)
            (calculate-response bar)))))))

Что работает отлично, но немного трудно читать, поэтому мне было интересно, есть ли более идиоматический способ написания Clojure?

Я подумал о том, чтобы сделать исключения some-simple-validation , calculate-stuff-using и вычислять больше-stuff-using и использовать попытку/catch, но это было похоже на использование исключений для потока управления, который не чувствовал себя правильно.

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

Я предполагаю, что я за чем-то вроде этого?

(defn calculate-stuff [data]
  (let-with-checking-function
    [valid-data (some-simple-validation data)
     foo (calculate-stuff-using valid-data)
     bar (calculate-more-stuff-using foo)]
    failed?)                    ; this function is used to check each variable
      (create-error %)          ; % is the variable that failed
      (calculate-response bar)) ; all variables are OK

Благодаря!

2
См. Также stackoverflow.com/q/7491360 .
добавлено автор glts, источник

5 ответы

Если неудачная проверка указывает на условие ошибки, исключение (и блок try-catch) может быть лучшим способом его обработки. Особенно, если это не «нормальное» событие (т. Е. Недопустимый cust-id и т. Д.).

For more "normal" but still "invalid" cases, you might use some-> (pronounced "some-thread") to quietly squelch "bad" cases. Just have your validators return nil for bad data, and some-> will abort the processing chain:

(defn proc-num [n]
  (when (number? n)
    (println :proc-num n)
    n))

(defn proc-int [n]
  (when (int? n)
    (println :proc-int n)
    n))

(defn proc-odd [n]
  (when (odd? n)
    (println :proc-odd n)
    n))

(defn proc-ten [n]
  (when (< 10 n)
    (println :proc-10 n)
    n))

(defn process [arg]
  (when (nil? arg)
    (throw (ex-info "Cannot have nil data" {:arg arg})))
  (some-> arg
    proc-num
    proc-int
    proc-odd
    proc-ten))

Результаты:

(process :a) => nil

(process "foo") => nil

:proc-num 12
:proc-int 12
(process 12) => nil

:proc-num 13
:proc-int 13
:proc-odd 13
:proc-10 13
(process 13) => 13

(throws? (process nil)) => true

Сказав это, вы теперь используете nil для обозначения «сбоя проверки данных», поэтому вы не можете иметь nil в своих данных.


Использование исключений для недопустимых данных

Использование nil в качестве специального значения для обработки короткого замыкания может работать, но может быть проще использовать простые исключения, особенно для случаев, которые явно «плохие данные»:

(defn parse-with-default [str-val default-val]
  (try
    (Long/parseLong str-val)
    (catch Exception e
      default-val))) ; default value

(parse-with-default "66-Six" 42) => 42

У меня есть небольшой макрос для автоматизации этого процесса с-исключением-умолчанию :

(defn proc-num [n]
  (when-not (number? n)
    (throw (IllegalArgumentException. "Not a number")))
  n)

(defn proc-int [n]
  (when-not (int? n)
    (throw (IllegalArgumentException. "Not int")))
  n)

(defn proc-odd [n]
  (when-not (odd? n)
    (throw (IllegalArgumentException. "Not odd")))
  n)

(defn proc-ten [n]
  (when-not (< 10 n)
    (throw (IllegalArgumentException. "Not big enough")))
  n)

(defn process [arg]
  (with-exception-default 42  ; <= default value to return if anything fails
    (-> arg
      proc-num
      proc-int
      proc-odd
      proc-ten)))

(process nil)    => 42
(process :a)     => 42
(process "foo")  => 42
(process 12)     => 42

(process 13)     => 13

Это позволяет избежать особого значения nil или любого другого «отпущенного» значения и использует Exception для нормальной работы по изменению потока управления при наличии ошибок.

4
добавлено
Это то, что я начал делать во многих случаях. Он в конечном итоге работает так же, как Haskell's Maybe chaining. Как только он терпит неудачу где-то вдоль цепи, все это терпит неудачу и возвращает нуль.
добавлено автор Carcigenicate, источник
Фантастический ответ - ясный и прямой. Спасибо за это!
добавлено автор Juraj Martinka, источник
Я пошел с использованием исключений в конце, спасибо за предложения
добавлено автор GentlemanHal, источник

Это обычная проблема на базе кода Clojure. Один из подходов состоит в том, чтобы обернуть ваши данные во что-то, что дает больше информации, а именно, если операция преуспела. Есть несколько библиотек, которые помогут вам в этом.

Например, с кошками ( http://funcool.github.io/cats/latest/ ):

(m/mlet [a (maybe/just 1)
         b (maybe/just (inc a))]
  (m/return (* a b)))

Или с результатами - я помог на этом ( https://github.com/clanhr/result ) :

(result/enforce-let [r1 notgood
                     r2 foo])
    (println "notgoof will be returned"))
2
добавлено

One of examples from other answers uses some-> macro that has a flaw: every failure should print a message into console and return nil. That is not good because a nil value also may indicate good results, especially for empty collections. Needless to say that you also need not only to print an error, but to handle it somehow or log it somewhere.

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

Что касается меня, это был бы лучший выбор, потому что он немедленно решает проблему.

Случай с исключениями также хорош. Не изобретайте свои собственные классы исключений, просто выбросьте карту, используя ex-info . После обнаружения такое исключение возвращает все данные, полученные с ним:

(if (some-checks data)
  (some-positive-code data)
  (throw (ex-into "Some useful message" {:type :error 
                                         :data data})))

поймать его:

(try
  (some-validation data)
(catch Exception e
  (let [err-data (ex-data e)]
    ; ...)))

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

1
добавлено

I faced the same issue. My solution was to copy the some->> macro and adjust it a little bit:

(defmacro run-until->> [stop? expr & forms]
     (let [g (gensym)
           steps (map (fn [step] `(if (~stop? ~g) ~g (->> ~g ~step)))
               forms)]
        `(let [~g ~expr
               [email protected](interleave (repeat g) (butlast steps))]
             ~(if (empty? steps)
                g
                (last steps)))))

вместо проверки на нуль этот макрос будет проверять ваше предопределенное условие. Например:

(defn validate-data [[status data]]
    (if (< (:a data) 10)
       [:validated data]
       [:failed data]))

(defn calculate-1 [[status data]]
     [:calculate-1 (assoc data :b 2)])

(defn calculate-2 [[status data]]
    (if (:b data)
       [:calculate-2 (update data :b inc)]
       [:failed data]))

(deftest test
    (let [initial-data [:init {:a 1}]]
       (is (= [:calculate-2 {:a 1, :b 3}] 
              (run-until->> #(= :failed (first %))
                            initial-data
                            (validate-data)
                            (calculate-1)
                            (calculate-2))))

       (is (= [:failed {:a 1}] 
              (run-until->> #(= :failed (first %))
                            initial-data
                            (validate-data)
                            (calculate-2))))))
0
добавлено

Я создал Променад для обработки именно такого рода сценариев.

0
добавлено
pro.jvm
pro.jvm
3 503 участник(ов)

Сообщество разработчиков Java Scala Kotlin Groovy Clojure Чат для нач-их: @javastart Наш сайт: projvm.com projvm.ru Наш канал: @proJVM Вакансии: @jvmjobs Конфы: @jvmconf

Clojure — русскоговорящее сообщество
Clojure — русскоговорящее сообщество
433 участник(ов)

Общаемся на темы, посвященный Clojure. Решаем проблемы, обмениваемся опытом и делимся новостями. Вакансии и поиск работы: @clojure_jobs Вам могут быть интересны: @javascript_ru, @nodejs_ru, @ruby_ru, @devops_ru, @devops_jobs