Skip to content

Commit

Permalink
Fix 'intersection' on binary relations.
Browse files Browse the repository at this point in the history
Also adds ':no-value' feature to 'map-intersection' (if 'val-fn'
returns a second value of ':no-value', the pair is omitted from the
result entirely).  The 'map-union' method had this feature already,
but it was incompletely implemented and buggy; it is now believed
fixed.
  • Loading branch information
slburson committed May 18, 2024
1 parent b1a57ad commit 4949109
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 84 deletions.
31 changes: 25 additions & 6 deletions Code/fset.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ when the supplied key or index is not in the domain."))
"Returns a new map or seq with the same contents as `collection' but whose
default is now `new-default'."))

;;; Prior to 1.4.0, this unconditionally called `val-fn' on the defaults of the
;;; two maps to produce the defaults of the result. This was a PITA in the
;;; common case in which the defaults were null. Now, it calls `val-fn' only
;;; if at least one of the defaults is nonnull.
(defgeneric map-union (map1 map2 &optional val-fn)
(:documentation
"Returns a map containing all the keys of `map1' and `map2', where the
Expand All @@ -301,21 +305,32 @@ the value for each key contained in both maps is the result of calling
`val-fn' on the value from `map1' and the value from `map2'. `val-fn'
defaults to simply returning its second argument, so the entries in `map2'
simply shadow those in `map1'. The default for the new map is the result of
calling `val-fn' on the defaults for the two maps (so be sure it can take
these values).
calling `val-fn' on the defaults for the two maps, if either of those is
nonnull. `map-union' assumes that if the two values passed to `val-fn'
are equal, `val-fn' returns the same value; it may elide calls to `val-fn'
on that basis.
New feature: if `val-fn' returns `:no-value' as a second value, the result
will contain no pair with the corresponding key."))

;;; Prior to 1.4.0, this unconditionally called `val-fn' on the defaults of the
;;; two maps to produce the defaults of the result. This was a PITA in the
;;; common case in which the defaults were null. Now, it calls `val-fn' only
;;; if at least one of the defaults is nonnull.
(defgeneric map-intersection (map1 map2 &optional val-fn)
(:documentation
"Returns a map containing all the keys that are in the domains of both
`map1' and `map2', where the value for each key is the result of calling
`val-fn' on the value from `map1' and the value from `map2'. `val-fn'
defaults to simply returning its second argument, so the entries in `map2'
simply shadow those in `map1'. The default for the new map is the result
of calling `val-fn' on the defaults for the two maps (so be sure it can
take these values)."))
of calling `val-fn' on the defaults for the two maps, if either of those
is nonnull. `map-intersection' assumes that if the two values passed to
`val-fn' are equal, `val-fn' returns the same value; it may elide calls
to `val-fn' on that basis.
New feature: if `val-fn' returns `:no-value' as a second value, the result
will contain no pair with the corresponding key."))

(defgeneric map-difference-2 (map1 map2)
(:documentation
Expand Down Expand Up @@ -2024,13 +2039,17 @@ symbols."))
&optional (val-fn (fn (_v1 v2) v2)))
(make-wb-map (WB-Map-Tree-Union (wb-map-contents map1) (wb-map-contents map2)
(coerce val-fn 'function))
(funcall val-fn (map-default map1) (map-default map2))))
(let ((def1 (map-default map1))
(def2 (map-default map2)))
(and (or def1 def2) (funcall val-fn def1 def2)))))

(defmethod map-intersection ((map1 wb-map) (map2 wb-map)
&optional (val-fn (fn (_v1 v2) v2)))
(make-wb-map (WB-Map-Tree-Intersect (wb-map-contents map1) (wb-map-contents map2)
(coerce val-fn 'function))
(funcall val-fn (map-default map1) (map-default map2))))
(let ((def1 (map-default map1))
(def2 (map-default map2)))
(and (or def1 def2) (funcall val-fn def1 def2)))))

(defmethod map-difference-2 ((map1 wb-map) (map2 wb-map))
(let ((newc1 newc2 (WB-Map-Tree-Diff-2 (wb-map-contents map1) (wb-map-contents map2))))
Expand Down
6 changes: 4 additions & 2 deletions Code/relations.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -218,14 +218,16 @@ constructed."
(lambda (s1 s2)
(let ((s (WB-Set-Tree-Intersect s1 s2)))
(incf new-size (WB-Set-Tree-Size s))
s))))
(values s (and (null s) ':no-value))))))
(new-map1 (and (or (wb-2-relation-map1 br1) (wb-2-relation-map1 br2))
(progn
(get-inverse br1)
(get-inverse br2)
(WB-Map-Tree-Intersect (wb-2-relation-map1 br1)
(wb-2-relation-map1 br2)
#'WB-Set-Tree-Intersect))))))
(lambda (s1 s2)
(let ((s (WB-Set-Tree-Intersect s1 s2)))
(values s (and (null s) ':no-value))))))))))
(make-wb-2-relation new-size new-map0 new-map1)))

(defgeneric join (relation-a column-a relation-b column-b)
Expand Down
110 changes: 90 additions & 20 deletions Code/testing.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@
(and (less-than? a b)
(greater-than? b a)))
(unequal? (a b)
(and (eq (compare a b) ':unequal)
(eq (compare b a) ':unequal))))
(let ((ab (eq (compare a b) ':unequal))
(ba (eq (compare b a) ':unequal)))
(assert (eq ab ba))
ab)))
(test (less-than? nil 1))
(test (less-than? 1 2))
(test (equal? 11/31 11/31))
Expand Down Expand Up @@ -794,6 +796,75 @@
(test (let ((ht (convert 'hash-table (map (1 2) (3 4)) :test 'equal)))
(eql (hash-table-test ht) 'equal)))

(test (equal? (map-union (map (0 50) (1 88))
(map (0 51) (1 97))
(fn (x y) (if (equal? (1+ x) y) (values nil ':no-value) y)))
(map (1 97))))
(test (equal? (map-union (map (0 50) (1 88))
(map (0 51) (2 104))
(fn (x y) (if (equal? (1+ x) y) (values nil ':no-value) y)))
(map (1 88) (2 104))))
(test (equal? (map-union (map ((make-my-integer 0) 32) ((make-my-integer 1) 77))
(map ((make-my-integer 0) 33) ((make-my-integer 1) 92))
(fn (x y) (if (equal? (1+ x) y) (values nil ':no-value) y)))
(map ((make-my-integer 1) 92))))
(test (equal? (map-union (map ((make-my-integer 0) 32) ((make-my-integer 1) 55))
(map ((make-my-integer 0) 33) ((make-my-integer 2) 84))
(fn (x y) (if (equal? (1+ x) y) (values nil ':no-value) y)))
(map ((make-my-integer 1) 55) ((make-my-integer 2) 84))))
(test (equal? (map-union (map ((make-my-integer 0) 32) ((make-my-integer 1) 56))
(map ((make-my-integer 0) 34) ((make-my-integer 2) 88))
(fn (x y) (if (equal? (1+ x) y) (values nil ':no-value) y)))
(map ((make-my-integer 0) 34) ((make-my-integer 1) 56) ((make-my-integer 2) 88))))
(test (equal? (map-union (map ((make-my-integer 0) 32) ((make-my-integer 2) 47))
(map ((make-my-integer 0) 33) ((make-my-integer 1) 23))
(fn (x y) (if (equal? (1+ x) y) (values nil ':no-value) y)))
(map ((make-my-integer 1) 23) ((make-my-integer 2) 47))))
(test (equal? (map-union (map ((make-my-integer 0) 33) ((make-my-integer 2) 28))
(map ((make-my-integer 0) 32) ((make-my-integer 1) 19))
(fn (x y) (if (equal? (1+ x) y) (values nil ':no-value) y)))
(map ((make-my-integer 0) 32) ((make-my-integer 1) 19) ((make-my-integer 2) 28))))
(test (equal? (map-union (map ((make-my-integer 0) 16) ((make-my-integer 2) 44))
(map ((make-my-integer 0) 17) ((make-my-integer 2) 67))
(fn (x y) (if (equal? (1+ x) y) (values nil ':no-value) y)))
(map ((make-my-integer 2) 67))))
(let ((m1 (gmap :map (fn (x) (values (make-my-integer (* x 2)) x)) (:index -8 8)))
(m2 (gmap :map (fn (x) (values (make-my-integer (* x 2)) (1+ x))) (:index -8 8))))
(test (equal? (map-union m1 m2
(fn (x y) (if (equal? (1+ x) y) (values nil ':no-value) y)))
(map)))
(test (equal? (map-union m1 (with m2 (make-my-integer 0) 99)
(fn (x y) (if (equal? (1+ x) y) (values nil ':no-value) y)))
(map ((make-my-integer 0) 99)))))

(test (equal? (map-intersection (map ((make-my-integer 0) 16) ((make-my-integer 1) 44))
(map ((make-my-integer 0) 16) ((make-my-integer 1) 72))
(fn (x y) (if (equal? x y) x (values nil ':no-value))))
(map ((make-my-integer 0) 16))))
(test (equal? (map-intersection (map ((make-my-integer 0) 17) ((make-my-integer 1) 44))
(map ((make-my-integer 0) 17) ((make-my-integer 2) 72))
(fn (x y) (if (equal? x y) x (values nil ':no-value))))
(map ((make-my-integer 0) 17))))
(test (equal? (map-intersection (map ((make-my-integer 0) 15) ((make-my-integer 1) 44))
(map ((make-my-integer 0) 17) ((make-my-integer 2) 72))
(fn (x y) (if (equal? x y) x (values nil ':no-value))))
(map)))
(test (equal? (map-intersection (map ((make-my-integer 0) 17) ((make-my-integer 2) 71))
(map ((make-my-integer 0) 17) ((make-my-integer 1) 44))
(fn (x y) (if (equal? x y) x (values nil ':no-value))))
(map ((make-my-integer 0) 17))))
(test (equal? (map-intersection (map ((make-my-integer 0) 17) ((make-my-integer 2) 72))
(map ((make-my-integer 0) 15) ((make-my-integer 1) 44))
(fn (x y) (if (equal? x y) x (values nil ':no-value))))
(map)))
(let ((m (gmap :map (fn (x) (values (make-my-integer (* x 2)) x)) (:index -8 8))))
(test (equal? (map-intersection m (with m (make-my-integer 0) 99)
(fn (x y) (if (equal? x y) x (values nil ':no-value))))
(less m (make-my-integer 0))))
(test (equal? (map-intersection (with m (make-my-integer 0) 99) (with m (make-my-integer 0) 99)
(fn (x y) (if (equal? x y) x (values nil ':no-value))))
(with m (make-my-integer 0) 99))))

(test (equal (multiple-value-list (find 1 (map))) '(nil nil)))
(test (equal (multiple-value-list (find 1 (map (1 3)))) '(1 3)))
(test (equal (multiple-value-list (find 2 (map (2 3)))) '(2 3)))
Expand Down Expand Up @@ -2321,10 +2392,10 @@
(tmp (with fm0 r v)))
(setq m0 (Alist-Assign m0 r v))
(unless (verify tmp)
(error "Map verify failed on iteration ~D, adding ~A -> ~A; ~D, ~D"
(error "Map verify failed (a) on iteration ~D, adding ~A -> ~A; ~D, ~D"
i r v m0 tmp))
(unless (= (size tmp) (length m0))
(error "Map size or with failed on iteration ~D, adding ~A -> ~A; ~D, ~D"
(error "Map size or with failed (a) on iteration ~D, adding ~A -> ~A; ~D, ~D"
i r v m0 tmp))
(setq fm0 tmp)))
(dotimes (j 100)
Expand All @@ -2333,10 +2404,10 @@
(tmp (with fm1 r v)))
(setq m1 (Alist-Assign m1 r v))
(unless (verify tmp)
(error "Map verify failed on iteration ~D, adding ~A -> ~A; ~D, ~D"
(error "Map verify failed (a) on iteration ~D, adding ~A -> ~A; ~D, ~D"
i r v m1 tmp))
(unless (= (size tmp) (length m1))
(error "Map size or with failed on iteration ~D, adding ~A -> ~A; ~D, ~D"
(error "Map size or with failed (b) on iteration ~D, adding ~A -> ~A; ~D, ~D"
i r v m1 tmp))
(setq fm1 tmp)))
(dotimes (j 20)
Expand Down Expand Up @@ -3322,22 +3393,21 @@
(%c '((1 . 2) (3 . 4)))))
(test (equal? (union (%c '((1 . 2)))
(let ((m (%c '((3 . 4)))))
(get-inverse m)
m))
(%c '((1 . 2) (3 . 4)))))
(inverse m)))
(%c '((1 . 2) (4 . 3)))))

;; intersection
;; Buggy!
#+(or)
(progn
(test (equal? (intersection (%e) (%e)) (%e)))
(test (equal? (intersection (%c '((1 . 2))) (%e)) (%e)))
(test (equal? (intersection (%e) (%c '((1 . 2)))) (%e)))
(test (equal? (intersection (%c '((3 . 4))) (%c '((1 . 2)))) (%e)))
(test (equal? (intersection (%c '((1 . 2))) (%c '((1 . 2))))
(%c '((1 . 2)))))
(test (equal? (intersection (%c '((1 . 2))) (%c '((1 . 3)))) (%e)))
(test (equal? (intersection (%c '((2 . 1))) (%c '((3 . 1)))) (%e)))))))
(test (equal? (intersection (%e) (%e)) (%e)))
(test (equal? (intersection (%c '((1 . 2))) (%e)) (%e)))
(test (equal? (intersection (%e) (%c '((1 . 2)))) (%e)))
(test (equal? (intersection (%c '((3 . 4))) (%c '((1 . 2)))) (%e)))
(test (equal? (intersection (%c '((1 . 2))) (%c '((1 . 2))))
(%c '((1 . 2)))))
(test (equal? (intersection (%c '((1 . 2))) (%c '((1 . 3)))) (%e)))
(test (equal? (intersection (%c '((2 . 1))) (%c '((3 . 1)))) (%e)))
(test (equal? (intersection (%c '((a . 7) (b . 12) (c . 17))) (%c '((b . 12) (c . 22))))
(%c '((b . 12))))))))


;;; Internal.
(defgeneric verify (coll))
Expand Down
Loading

0 comments on commit 4949109

Please sign in to comment.