Agar Clojure funktsiyasidagi tekshiruvlar bir necha marta yozilsa yaxshi bo'ladi?

Menda quyidagi kabi ko'rinadigan Clojure funksiyasi mavjud.

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

Qanday ishlaydi, ammo o'qish uchun bir oz qiyin, shuning uchun men bu haqda yozib yuborishning odatiy bo'lmagan Clojure uslubi bormi deb o'ylayman?

some-simple-validation , hisob-ish-ishlatish va hisob-more-stuff-using /tutish bloki, lekin o'zlarini to'g'ri his qilmagan boshqarish oqimi uchun istisnolardan foydalanishni his qilardi.

Men istisnolardan bu funksiyadan qochib ketishimga imkon berolmayman, chunki uni xaritalar xaritasini xaritada ko'rsatish uchun foydalanmoqdaman va qolgan ishni davom ettirishni xohlayman.

O'ylaymanki, men nima qilayapman?

(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

Rahmat!

2
qo'shib qo'ydi muallif glts, manba

5 javoblar

Muvaffaqiyatsiz tekshirish xatolikning holatini bildirsa, istisno (va sinab ko'ring-blok) uni qo'llashning eng yaxshi usuli bo'lishi mumkin. Ayniqsa, u "oddiy" (masalan, noto'g'ri cust-id va boshqalar) sodir bo'lmasa.

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

natijalari:

(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

Buni gapirib, "ma'lumotlar tekshirish xatosi" degan ma'noni anglatadi, shuning uchun sizda nil kodi mavjud emas.


Yaroqsiz ma'lumot uchun istisnolardan foydalanish

Qisqa elektron ishlov berish uchun maxsus qiymat sifatida nil dan foydalanish mumkin, ammo odatdagi "yomon ma'lumot" bo'lgan holatlar uchun, odatdagi istisnolardan foydalanish osonroq bo'lishi mumkin:

(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

bu jarayonni avtomatlashtirish uchun kichik so'l »degan so'z bor with-exception-default :

(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

Bu esa nil yoki boshqa har qanday "sentinal" qiymat uchun maxsus ma'no berishni oldini oladi va xatolik mavjudligida boshqarish oqimini o'zgartirishning normal maqsadi uchun Exception dan foydalanadi.

4
qo'shib qo'ydi
Ko'p hollarda men buni boshlaganman. Bu Haskellning ehtimol zanjiriga o'xshash ishlarni bajaradi. Zanjir bo'ylab bir joyga etib borgach, hamma narsa muvaffaqiyatsiz bo'ladi va nilni qaytaradi.
qo'shib qo'ydi muallif Carcigenicate, manba
Ajoyib javob - aniq va bevosita. Buning uchun rahmat!
qo'shib qo'ydi muallif Juraj Martinka, manba
Men oxirida istisnolardan foydalanib, takliflar uchun minnatdorman
qo'shib qo'ydi muallif GentlemanHal, manba

Bu Clojure kodlari bazasidagi odatiy muammo. Bitta yondashuv, sizning ma'lumotlaringizni operatsiya muvaffaqiyatli bajarilgan bo'lsa, qo'shimcha ma'lumot beruvchi narsaga aylantirishdir. Sizga yordam beradigan bir nechta kutubxona mavjud.

Misol uchun, mushuklar ( http://funcool.github.io/cats/latest/ ):

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

Yoki natija bilan - men unga yordam berdim ( https://github.com/clanhr/result ) :

(result/enforce-let [r1 notgood
                     r2 foo])
    (println "notgoof will be returned"))
2
qo'shib qo'ydi

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.

Kodni qayta tuzishning eng oson usuli faqat uni buzishdir. Aytgin, har qanday narsani agar ning salbiy qismidan alohida funktsiyasiga qo'yishingiz mumkin, va bu shunday. Ushbu ikki funktsiya sinov va xatolarni tuzatish uchun oson bo'ladi.

Menga kelsak, bu eng yaxshi tanlovdir, chunki bu muammoni darhol hal qiladi.

Istisno holatlari ham yaxshi. O'zingizning istisno sinflaringizni ixtira qilmang, faqat ex-info dan foydalangan holda xaritani tashlang. Bunday holatda qo'lga kiritilgan barcha ma'lumotlar qaytariladi:

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

uni qo'lga olish uchun:

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

Va nihoyat, monadlarni ishlatish mumkin bo'lgan holatlar bo'lishi mumkin, ammo muammolarni chalkashtirib yuborishdan xabardor bo'ling.

1
qo'shib qo'ydi

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

nillarni tekshirish o'rniga, bu so'l sizning oldindan belgilangan holatni tekshiradi. Misol uchun:

(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
qo'shib qo'ydi

Ushbu turdagi senaristlarni boshqarish uchun Promenad ni yaratdim.

0
qo'shib qo'ydi