Search code examples
if-statementclojurefunctional-programmingimperative

Simple flow control and variable binding in Clojure


I'm learning Clojure and working on a simple file parsing script.

I have a file in the form of:

pattern1
pattern2
pattern3
pattern1
pattern2
...

where each line has a few values (numbers) that I extract.

If I was to write this in Java for example, I would do something similar to:

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
    //...
}

Now what would be a an elegant or idiomatic way to do something similar in Clojure?

I guess I can use cond, but since there's no binding in the test expression, I'll have to parse the line twice:

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

I could also use if-let, but that will be a lot of nesting since there are 3 different options. Imagine with 7 different pattern how that would look like.

Any suggestions? Obviously Java solution is an imperative one and I can do a "return" whenever I want, so what would be the Clojure/FP way of dealing with this simple branching.


Solution

  • i would go with some simple function to return the first matched pattern, filtering over the patterns seq:

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

    this one returns the function, that would return the first match, testing the line:

    user> (def mat (first-match [#"(asd)" #"(fgh)" #"aaa(.+?)aaa"]))
    #'user/mat
    
    user> (mat "aaaxxxaaa")
    ;;=> ["aaaxxxaaa" "xxx"]
    
    user> (mat "nomatch")
    ;;=> nil
    

    otherwise you could use some simple macro for that. maybe like this:

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

    for the preceeding example that would expand to something like this (schematically)

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

    more examples:

    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"]