1

I have written a function in Clojure that is supposed to take a logical expression and return an equivalent expression where all not statements act directly on variables, like so:

(not (and p q r))

becomes

(or (not p) (not q) (not r))

It uses De Morgan's laws to push the nots inwards, and if a not acts directly on another not statement, they cancel out. The code looks like this:

(defn transform [expr]
    (if
        (list? expr)
        (if
            (=
                'not
                (first expr)
            )
            (if
                (list? (nth expr 1))
                (if
                    (=
                        'not
                        (first (nth expr 1))
                    )
                    (transform (first (rest (first (rest expr)))))
                    (if
                        (=
                            'and
                            (first (nth expr 1))
                        )
                        (cons 
                            'or 
                            (map
                                transform
                                (map
                                    not-ify
                                    (rest (first (rest expr)))
                                )
                            )
                        )
                        (if
                            (=
                                'or
                                (first (nth expr 1))
                            )
                            (cons
                                'and 
                                (map
                                    transform
                                    (map
                                        not-ify
                                        (rest (first (rest expr)))
                                    )
                                )
                            )
                            expr
                        )
                    )
                )
                expr
            )
            expr
        )
        expr
    )
)

The problem lies in this part:

(map
    transform
    (map
        not-ify
        (rest (first (rest expr)))
    )
)

The first map statement uses a function not-ify (excuse the pun) to basically put a not before each statement. That part works. However, the output doesn't work with the map transform, although the map transform part works by itself. Let me show you:

If I write the following in the REPL:

(def expr '(not (and q (not (or p (and q (not r)))))))


(map
    not-ify
    (rest (first (rest expr)))
)

I get the output ((not q) (not (not (or p (and q (not r))))))

If I then take that output and run (map transform '((not q) (not (not (or p (and q (not r))))))), I get the output ((not q) (or p (and q (not r)))). So far so good.

However if I run it all at once, like so:

(map
    transform
    (map
        not-ify
        (rest (first (rest expr)))
    )
)

I get this output instead: ((not q) (not (not (or p (and q (not r)))))).

If run

(def test1
    (map
        not-ify
        (rest (first (rest expr)))
    )
)
(map transform test1)

I also get ((not q) (not (not (or p (and q (not r)))))).

However if I run

(def test2 '((not q) (not (not (or p (and q (not r)))))))
(map transform test2)

I once again get the correct result: ((not q) (or p (and q (not r)))).

My guess is that this is somehow related to the map not-ify output (test1) having the type LazySeq, while if I manually type the input (test2) it becomes a PersistentList. I've tried running (into (list)) on test1 to convert it to a PersistentList, as well as doRun and doAll, with no results. Can I somehow stop my map not-ify statement from returning a LazySeq?

Henrik
  • 423
  • 4
  • 11

2 Answers2

2

The short answer is to use seq? instead of list?

Here is how I would implement it:

(defn push-not-down [expr]
  (if (and (seq? expr) (seq? (second expr)))
    (let [[outer-op & [[inner-op & inner-args] :as outer-args] :as expr] expr]
      (if (= 'not outer-op)
        (condp = inner-op
          'and (cons 'or (map #(push-not-down (list 'not %)) inner-args))
          'or (cons 'and (map #(push-not-down (list 'not %)) inner-args))
          'not (first inner-args)
          expr)
        (if (#{'or 'and} outer-op)
          (cons outer-op (map push-not-down outer-args))
          expr)))
    expr))

(deftest push-not-down-test
  (testing "Not or and not and are transformed to and not and or not"
    (is (= '(or (not :a) (not :b))
           (push-not-down
             '(not (and :a :b)))))
    (is (= '(and (not :a) (not :b))
           (push-not-down
             '(not (or :a :b))))))
  (testing "Double nots cancel"
    (is (= :a
           (push-not-down
             '(not (not :a))))))
  (testing "The rules work together in complex combinations"
    (is (= '(and :a (and :b (not :c)))
           (push-not-down
             '(and (not (not :a)) (not (or (not :b) :c))))))
    (is (= '(or (or (and (not :a))))
           (push-not-down
             '(or (or (and (not :a))))))))
  (testing "Nested expressions that don't fit the rules are preserved"
    (is (= '(not (inc 1))
           (push-not-down
             '(not (inc 1)))))
    (is (= '(inc (or 2 1))
           (push-not-down
             '(inc (or 2 1)))))))

For forms and expressions, there is no significant difference between a list or a sequence. Alternatively if you did want to preserve listiness, you just need to be a bit more thorough in converting sequences to lists :)

Timothy Pratley
  • 10,586
  • 3
  • 34
  • 63
  • Thanks! This really helped push me over the hill. Your example looks great, unfortunately I don't really know Clojure very well; It just happens that my discrete maths course has computer assignments in Clojure, without much more than a tutorial of the basics, so half of your code looks like gibberish to me. :P – Henrik May 22 '16 at 00:40
1

For what it's worth, ...

First, let's define what a logical inverse becomes:

(def opposite {'and 'or, 'or 'and})

(defn inverse [expr]
  (let [default (list 'not expr)]
    (if (seq? expr)
      (let [[op & args] expr]
        (if (= op 'not)
          (first args)
          (cons (opposite op) (map inverse args))))
      default)))

Let's test it:

(map inverse ['p '(not p) '(and a b) '(and (not a) b)])
;((not p) p (or (not a) (not b)) (or a (not b)))

It short-circuits double negatives as well as doing the DeMorgan thing.

Now we can express the transformation:

(defn transform [expr]
  (if (seq? expr)
    (let [[op & args] expr]
      (if (= op 'not)
        (inverse (first args))
        (cons op (map transform args))))
    expr))

For example,

(transform '(not (and p q r)))
;(or (not p) (not q) (not r))
  • Where it finds a not, it returns the inverse of the argument.
  • Otherwise, it reconstructs the expression, transforming sub-expressions where it can, just as inverse does.

If we're not fussed about transforming sub-expressions, we can simplify to

(defn transform [expr]
  (or (and (seq? expr)
           (let [[op & args] expr]
             (if (= op 'not) (inverse (first args)))))
      expr))

  • I find pulling out the inverse manipulation easier to follow.
  • In the simple transform, I've played with and and or and one-armed if to avoid repeating the do-nothing case that weighs down OP's text.

How does this relate to @TimothyPratley's answer?

  • I shamelessly stole his observation on using seq? instead of list?.
  • His throws an exception on unrecognised operations. Mine just makes them nil.
  • I've factored the ands and ors into a single case.
Community
  • 1
  • 1
Thumbnail
  • 13,293
  • 2
  • 29
  • 37