Clojure 中的简单流控制和变量绑定
Simple flow control and variable binding in Clojure
我正在学习 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 解决方案是必要的,我可以随时执行 "return",那么处理这个问题的 Clojure/FP 方式是什么 简单的分支。
我会使用一些简单的函数来 return 第一个匹配的模式,过滤模式 seq:
(defn first-match [patterns]
(fn [line]
(some #(re-find % line) patterns)))
这个 return 函数,它将 return 第一个匹配项,测试行:
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"]
给定一些示例数据:
(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])
)
请注意,不匹配的行 return 为零。您可能希望针对问题抛出异常,或者至少过滤掉 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.
我正在学习 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 解决方案是必要的,我可以随时执行 "return",那么处理这个问题的 Clojure/FP 方式是什么 简单的分支。
我会使用一些简单的函数来 return 第一个匹配的模式,过滤模式 seq:
(defn first-match [patterns]
(fn [line]
(some #(re-find % line) patterns)))
这个 return 函数,它将 return 第一个匹配项,测试行:
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"]
给定一些示例数据:
(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])
)
请注意,不匹配的行 return 为零。您可能希望针对问题抛出异常,或者至少过滤掉 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.