2016-08-29 11 views
3

考慮這個相當嵌套的clojure代碼,它描述了傳遞給命令行工具的edn-config文件的驗證過程。cli-tool的配置文件的驗證過程:避免嵌套的if

(defn -main [& [config-path]] 
    (if config-path 
    (if-let [content (read-file config-path)] 
     (if-let [raw-data (read-edn content)] 
     (if-let [parsed (parse raw-data)] 
      (start-processing parsed) 
      (error "parsing-error")) 
     (error "invalid-edn-format")) 
     (error "file-not-found")) 
    (error "no argument")) 

注:調用的函數是虛設

這需要較少的嵌套&少勢在必行的方式來做到這一點。你有什麼改進建議嗎? (1)Argument-Check,(2)File-Read,(3)Parse-EDN,(4)Parse-EDN,(4)Parse-EDN,(4)Parse-EDN,數據。他們在處理「錯誤」的方式上有所不同:對於1和4,我使用clojure.spec,因此:clojure.spec/invalid在失敗時返回。其他人(2和3)會在出現問題時拋出異常。 這使得在這裏抽象特別困難。

回答

1

使用Either monad。

起初,你需要修改你的函數返回Either

(require '[cats.monad.either :as either]) 

(defn assert-config-path-e [config-path] 
    (if config-path 
    (either/right config-path) 
    (either/left "no argument"))) 

(defn read-file-e [config-path] 
    ;; interpret return value as error 
    (if-let [content (read-file config-path)] 
    (either/right content) 
    (either/left "file-not-found"))) 

(defn read-edn-e [content] 
    (try 
    (read-edn content) 
    (catch ... 
     ;; interpret thrown exception as error 
     (either/left "invalid-edn-format")))) 

(defn parse-e [raw-data] 
    (if-let [parsed (parse raw-data)] 
    (either/right parsed) 
    (either/left "parsing-error"))) 

Right值表示成功的計算,Left - 已經發生的錯誤。

後,結合使用do -notation/mlet的所有東西:

(require '[cats.core :as cats]) 

(defn -main [& [config-path]] 
    (cats/mlet [_ (assert-config-path-e config-path) 
       content (read-file-e config-path) 
       raw-data (read-edn-e content) 
       parsed (parse-e raw-data)] 
    (cats/return (start-processing parsed))) 
+0

這看起來像一個乾淨的東西。兩個問題:你的代碼在任何時候都沒有聲明,應該怎樣處理錯誤字符串(例如打印它們)。以及:爲什麼貓?它可以用clojure.algo.monads完成嗎?感謝您的努力! –

+0

@AntonHarald我在示例中使用過貓,因爲我們在生產中已經使用了很長一段時間。我沒有完全明白錯誤字符串的問題:如果在管道中發生錯誤,將返回「left」值,通常包含錯誤對象(在此實現中它是描述錯誤的字符串)。這個功能可以使用'algo.monads'來實現,但'algo.monads'庫沒有'Either' monad的實現,所以你需要自己實現它。 – OlegTheCat

0

我會考慮的邏輯的各部分寫入獨立的功能,使他們能夠處理驗證和使用報告錯誤的一致方法(在這種情況下的例外):

(defn validate-config-path [config-path] 
    (if config-path 
    config-path 
    (throw ...))) 

(defn read-raw-config [config-path] 
    (try 
    (read-file config-path) 
    (catch ... 
     (throw ...)))) 

(defn read-edn-config [raw-config] 
    (try 
    ... 
    (catch ... 
     (throw ...)))) 

(defn read-parsed-config [edn-config] 
    (try 
    ... 
    (catch ... 
     (throw ...)))) 

(defn -main [& [config-path]] 
    (try 
    (-> config-path 
     validate-config-path 
     read-edn-config 
     read-parsed-config) 
    (catch Exception e 
     (error (.getMessage e))))) 

通過這種方法,你可以很容易地分別測試每個部分(如將其封裝在單獨的函數中)並在其他地方重新使用。您還集中處理如何在一個位置處理顯示錯誤的用戶,以便您可以更改向用戶顯示編程錯誤的方式。

1

使用delay推遲評估,但允許將值綁定到本地let變量。然後使用and評估表達式,但會在nil上短路,這正是您想要的。 cond然後會給出具體的錯誤消息,並評估你的最後一步如果沒有錯誤。

(defn -main [& [config-path]] 
    (let [content (delay (read-file config-path)) 
     raw-data (delay (read-edn @content)) 
     parsed (delay (parse @raw-data))] 
    (and config-path 
     @content 
     @raw-data 
     @parsed) 
    (cond 
     (nil? config-path) (error "no argument") 
     (nil? @content) (error "file-not-found") 
     (nil? @raw-data) (error "invalid-edn-format") 
     (nil? @parsed)  (error "parsing-error") 
     :else (start-processing @parsed)))) 

它具有以下優點:

  • 這是奉承
  • 容易理解
  • 葉單獨調用的函數,而不是強迫他們做的事情(也許,一旦你過去delay?)它們不在它們的定義之內,保持功能的純度
  • 不涉及宏(不是壞事,但使用核心函數是一個加號)