Betere manier om meervoudig te schrijven als controles in een Clojure-functie?

Ik heb een Clojure-functie die er ongeveer zo uitziet als het volgende.

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

Dat werkt prima, maar het is een beetje moeilijk te lezen, dus ik vroeg me af of er een meer idiomatische Clojure-manier was om dit te schrijven?

Ik dacht aan het maken van sommige-eenvoudige-validatie , berekenen-dingen-gebruik en berekenen-meer-dingen-gebruik uitzonderingen gooien en een try gebruiken/catch-blok, maar dat leek op het gebruik van uitzonderingen voor de controlestroom die niet correct aanvoelden.

Ik kan de uitzonderingen niet aan deze functie laten ontsnappen, ook niet als ik het gebruik om een ​​reeks kaarten in kaart te brengen en ik wil nog steeds doorgaan met het verwerken van de rest.

Ik denk dat wat ik zoek zoiets is als dit?

(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

Bedankt!

2
toegevoegd de auteur glts, de bron

5 antwoord

Als een mislukte validatie een fout aangeeft, is een uitzondering (en een try-catch-blok) wellicht de beste manier om dit te verhelpen. Vooral als het geen "normale" gebeurtenis is (d.w.z. ongeldige cust-id, enz.).

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

resultaten:

(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

Dit gezegd hebbende, gebruikt u nu nul om "mislukte gegevensvalidatie" te betekenen, dus u kunt geen nul in uw gegevens hebben.


Uitzonderingen gebruiken voor ongeldige gegevens

Het gebruik van nul als speciale waarde voor kortsluitingsverwerking kan werken, maar het is misschien eenvoudiger om oude uitzonderingen te gebruiken, vooral voor zaken die duidelijk "slechte gegevens" zijn:

(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

Ik heb een beetje macro om dit proces te automatiseren genaamd met uitzondering-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

Hiermee wordt voorkomen dat een speciale betekenis wordt gegeven aan nul of een andere "sentinal" -waarde, en wordt Uitzondering gebruikt voor het normale doel van het wijzigen van de besturingsstroom in de aanwezigheid van fouten.

4
toegevoegd
Dit is wat ik in veel gevallen ben gaan doen. Het werkt uiteindelijk heel erg zoals Haskell's Misschien-keten. Als het ergens in de keten faalt, faalt het hele ding en keert het terug.
toegevoegd de auteur Carcigenicate, de bron
Fantastisch antwoord - duidelijk en direct. Bedankt hiervoor!
toegevoegd de auteur Juraj Martinka, de bron
Ik ging met het gebruik van uitzonderingen op het einde, bedankt voor de suggesties
toegevoegd de auteur GentlemanHal, de bron

Dat is een gebruikelijk probleem met Clojure-codebases. Eén benadering is om uw gegevens om te zetten in iets dat meer informatie biedt, namelijk als de bewerking slaagt. Er zijn een paar bibliotheken die je daarbij helpen.

Bijvoorbeeld met katten ( http://funcool.github.io/cats/latest/ ):

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

Of met resultaten - ik heb hierbij geholpen ( https://github.com/clanhr/result ) :

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

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.

De eenvoudigste manier om uw code te refacteren, zou alleen maar zijn om het te ontleden. Stel dat je alles van de negatieve tak van de eerste als in een aparte functie kunt plaatsen, en dat is het dan. Deze twee functies worden eenvoudiger te testen en te debuggen.

Wat mij betreft, het zou de beste keuze zijn, omdat het het probleem meteen zal oplossen.

Een geval met uitzonderingen is ook goed. Stel uw eigen uitzonderingsklassen niet uit, gooi gewoon een kaart met ex-info . Eenmaal gevangen, geeft een dergelijke uitzondering alle gegevens die ermee zijn gegooid:

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

om het te vangen:

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

Ten slotte kan er een reden zijn om monaden te gebruiken, maar houd er rekening mee dat het probleem te veel is.

1
toegevoegd

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

in plaats van te controleren op nils, controleert deze macro uw voorgedefinieerde voorwaarde. Bijvoorbeeld:

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

Ik heb Promenade gemaakt om precies dit soort scenario's te behandelen.

0
toegevoegd