【问题标题】:Simple flow control and variable binding in ClojureClojure 中的简单流控制和变量绑定
【发布时间】:2020-08-10 01:30:40
【问题描述】:

我正在学习 Clojure 并正在研究一个简单的文件解析脚本。

我有一个格式如下的文件:

pattern1
pattern2
pattern3
pattern1
pattern2
...

每行都有一些我提取的值(数字)。

如果我用 Java 写这个,我会做类似的事情:

Map<String, Integer> parse(String line) {
    //using Optional in this toy example, but could be an empty map or something else to
    //signal if first one was actually matched and the values are there
    Optional<Map<String, Integer>> firstMatched = matchFirst(line);
    if (firstMatched.isPresent()) {
        return firstMatched.get();
    }
    //...do the same for 2 remaining patterns
    //...
}

现在什么是优雅或惯用的方式来做类似的事情在Clojure

我想我可以使用cond,但由于测试表达式中没有绑定,我将不得不解析该行两次:

(defn parse
  [line]
  (cond
    (re-find #"pattern-1-regex" line) (re-find...)
    (re-find #"pattern-2-regex" line) (re-find...

我也可以使用if-let,但这将是很多嵌套,因为有 3 个不同的选项。想象一下 7 种不同的模式会是什么样子。

有什么建议吗?显然,Java 解决方案是一种命令式的解决方案,我可以随时“返回”,那么处理这个简单分支的 Clojure/FP 方式是什么。

【问题讨论】:

  • condp 会起作用吗?

标签: if-statement clojure functional-programming imperative


【解决方案1】:

我会使用一些简单的函数来返回第一个匹配的模式,过滤模式 seq:

(defn first-match [patterns]
  (fn [line]
    (some #(re-find % line) patterns)))

这个返回函数,它将返回第一个匹配,测试行:

user> (def mat (first-match [#"(asd)" #"(fgh)" #"aaa(.+?)aaa"]))
#'user/mat

user> (mat "aaaxxxaaa")
;;=> ["aaaxxxaaa" "xxx"]

user> (mat "nomatch")
;;=> nil

否则你可以使用一些简单的宏。可能是这样的:

(defmacro when-cond [& conds]
  (when (seq conds)
    `(if-let [x# ~(first conds)]
       x#
       (when-cond ~@(rest conds)))))

user> 
(let [line "somethingaaa"]
  (when-cond
    (re-find #"something" line)
    (re-find #"abc(.*?)def" line)))
;;=> "something"

对于前面的例子,它会扩展成这样的东西(示意图)

(if-let [x__8043__auto__ (re-find #"something" line)]
  x__8043__auto__
  (if-let [x__8044__auto__ (re-find #"abc(.*?)def" line)]
    x__8044__auto__
    nil))

更多示例:

user> 
(let [line "nomatch"]
  (when-cond
    (re-find #"something" line)
    (re-find #"abc(.*?)def" line)))
;;=> nil

user> 
(let [line "abcxxxxxxdef"]
  (when-cond
    (re-find #"something" line)
    (re-find #"abc(.*?)def" line)))
;;=> ["abcxxxxxxdef" "xxxxxx"]

【讨论】:

  • 不错!返回第一个匹配项的函数是非常简单的方法,我觉得我没有想到这一点:) 并且带有宏的那个看起来很整洁,仍在学习,所以我需要更多地剖析它,即理解宏正确。非常感谢!
【解决方案2】:

给定一些示例数据:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test)
  (:require
    [clojure.string :as str]
    [tupelo.string :as ts]
    [tupelo.parse :as parse]))

(def data-str "
  fred123 1 2 3
  fred456   4 5    6
  wilma12  1.2
  wilma34 3.4
  barney1 1
  barney2 2
  ")

然后您可以为每种类型的数据定义解析函数:

(defn fred-parser
  [line]
  (let [tokens        (str/split line #"\p{Blank}+")
        root          (first tokens)
        details       (rest tokens)
        parsed-root   (re-find #"fred\n*" root)
        parsed-params (mapv parse/parse-long details)
        result        {:root parsed-root :params parsed-params}]
    result))

(defn wilma-parser
  [line]
  (let [tokens        (str/split line #"\p{Blank}+")
        root          (first tokens)
        details       (rest tokens)
        parsed-root   (re-find #"wilma\n*" root)
        parsed-params (mapv parse/parse-double details)
        result        {:root parsed-root :params parsed-params}]
    result))

我会制作一个从模式到解析函数的映射:

(def pattern->parser
  {#"fred\d*"  fred-parser
   #"wilma\d*" wilma-parser
   })

还有一些函数可以为每一行(清理过的)数据找到正确的解析器:

(defn parse-line
  [line]
  (let [patterns          (keys pattern->parser)
        patterns-matching (filterv ; keep pattern if matches
                            (fn [pat]
                              (ts/contains-match? line pat))
                            patterns)
        num-matches       (count patterns-matching)]
    (cond
      (< 1 num-matches) (throw (ex-info "Too many matching patterns!" {:line line :num-matches num-matches}))
      (zero? num-matches) (prn :no-match-found line)
      :else (let [parser      (get pattern->parser (only patterns-matching))
                  parsed-line (parser line)]
              parsed-line))))

(defn parse-file
  [data]
  (let
    [lines       (filterv #(not (str/blank? %)) ; remove blank lines
                   (mapv str/trim ; remove leading/trailing whitespace
                     (str/split-lines data))) ; split into lines
     parsed-data (mapv parse-line lines)]
    parsed-data))

和一个单元测试来展示它的实际效果:

(dotest
  (is= (parse-file data-str)
    [{:root "fred", :params [1 2 3]}
     {:root "fred", :params [4 5 6]}
     {:root "wilma", :params [1.2]}
     {:root "wilma", :params [3.4]}
     nil
     nil])
  )

请注意,不匹配的行返回 nil。您可能希望抛出问题异常,或者至少过滤掉 nil 值。现在你只会得到一个打印的错误消息:

-------------------------------
   Clojure 1.10.1    Java 14
-------------------------------

Testing tst.demo.core
:no-match-found "barney1 1"
:no-match-found "barney2 2"

Ran 2 tests containing 1 assertions.
0 failures, 0 errors.

更多文档 herehere

【讨论】:

  • 太棒了!我从这两个答案中学到了很多东西。尽管我对 Clojure 完全陌生,但它很容易阅读(我想当您查看干净的代码时,其他语言通常也是如此)。谢谢艾伦!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多