git.fiddlerwoaroof.com
lens.lisp
0850311d
 (defpackage :data-lens.lenses
   (:shadow :set)
b99be6fa
   (:use :cl)
a2243813
   (:export :over :set :view :make-alist-lens :make-plist-lens :make-hash-table-lens
            :make-list-lens))
0850311d
 (in-package :data-lens.lenses)
 
cbd36776
 #+fw.dev
 (progn
   ;; maybe functor implementation
   (defclass maybe ()
     ())
   (defclass just (maybe)
     ((%v :initarg :value :reader value)))
   (defclass nothing (maybe)
     ())
 
   (defun just (value)
     (make-instance 'just :value value))
   (defun nothing (&optional value)
     (declare (ignore value))
     (make-instance 'nothing))
 
   (defgeneric maybe (default value)
     (:method (default (value just))
       (value value))
     (:method (default (value nothing))
       default))
 
   (defgeneric maybe-apply (function value)
     (:method (function (value just))
       (just (funcall function (value value))))
     (:method (function (value nothing))
       value))
 
   (defmethod print-object ((o just) s)
     (format s "#.(~s ~s)"
             'just
             (value o)))
 
   (defmethod print-object ((o nothing) s)
     (format s "#.(~s)"
             'nothing)))
 
 ;; identity functor, necessary for set and over
 (defclass identity- ()
   ((%v :initarg :value :reader unidentity)))
 
 (defun wrap-identity (v)
   (make-instance 'identity- :value v))
 
 (defmethod print-object ((o identity-) s)
   (format s "#.(~s ~s)"
           'wrap-identity
           (unidentity o)))
 
 ;; constant functor, necessary for view
 (defclass constant- ()
   ((%v :initarg :value :reader unconstant)))
 
 (defun wrap-constant (v)
   (make-instance 'constant- :value v))
 
 (defmethod print-object ((o constant-) s)
   (format s "#.(~s ~s)"
           'wrap-constant
           (unconstant o)))
 
 (defgeneric fmap (function data)
   (:method (function (data identity-))
     (wrap-identity
      (funcall function
               (unidentity data))))
   (:method (function (data constant-))
     data)
   (:method (function (data list))
     (mapcar function data))
   (:method (function (data vector))
     (map 'vector function data))
   #+fw.dev
   (:method (function (data maybe))
     (maybe-apply function data)))
 
0850311d
 (defun over (lens cb rec)
   "Given a lens, a callback and a record, apply the lens to the
 record, transform it by the callback and return copy of the record,
 updated to contain the result of the callback. This is the fundamental
 operation on a lens and SET and VIEW are implemented in terms of it.
 
 A lens is any function of the form (lambda (fun) (lambda (rec) ...))
 that obeys the lens laws (where == is some reasonable equality
 operator):
 
ad00caba
     (== (view lens (set lens value rec))
         value)
  
     (== (set lens (view lens rec) rec)
         rec)
  
     (== (set lens value2 (set lens value1 rec))
         (set lens value2 rec))
0850311d
 
cbd36776
 The inner lambda returns a functor that determines the policy to be
 applied to the focused part.  By default, this only uses IDENTITY- and
 CONSTANT- in order to implement the lens operations over, set and
 view.
 
0850311d
 If these conditions are met, (over (data-lens:<>1 lens1 lens2) ...) is
 equivalent to using lens2 to focus the part lens1 focuses: note that
 composition is \"backwards\" from what one might expect: this is
 because composition composes the wrapper lambdas and applies the
 lambda that actually pulls a value out of a record later."
cbd36776
   (unidentity
    (funcall (funcall lens (lambda (x) (wrap-identity (funcall cb x))))
             rec)))
0850311d
 
cbd36776
 (defun view (lens rec)
   "Given a lens and a rec, return the focused value"
   (unconstant
    (funcall (funcall lens (lambda (x) (wrap-constant x)))
             rec)))
0850311d
 
cbd36776
 (defun set (lens v rec)
0850311d
   "Given a lens, a value and a rec, immutably update the rec to
 contain the new value at the location focused by the lens."
cbd36776
   (unidentity
    (funcall (funcall lens (lambda (_) _ (wrap-identity v)))
             rec)))
0850311d
 
cbd36776
 #+fw.dev
 (progn
   ;; "fake" functors that don't assume a functor result to their
   ;; callback
   (defun over* (lens cb rec)
     (funcall (funcall lens cb)
              rec))
 
   (defun set* (lens value rec)
     (over lens
           (lambda (_)
             (declare (ignore _))
             value)
           rec))
 
   (defun view* (lens rec)
     (over lens
           (lambda (value)
             (return-from view*
               value))
           rec)))
 
 (defun make-alist-history-lens (key)
   "A lens for updating a alist, preserving previous values"
   (lambda (cb)
     (lambda (alist)
       (fmap (lambda (new)
               (cons (cons key new)
                     alist))
             (funcall cb (serapeum:assocdr key alist))))))
0850311d
 
a1ca234d
 (defun make-alist-lens (key)
cbd36776
   "A lens for updating a alist, discarding previous values"
a1ca234d
   (lambda (cb)
     (lambda (alist)
cbd36776
       (fmap (lambda (new)
               (remove-duplicates (cons (cons key new)
                                        alist)
                                  :key #'car
                                  :from-end t))
             (funcall cb (serapeum:assocdr key alist))))))
a1ca234d
 
a2243813
 (defun make-list-lens (index)
   "A lens for updating a sequence"
   (lambda (cb)
     (lambda (seq)
       (fmap (lambda (new)
               (let ((result (copy-seq seq)))
                 (prog1 result
                   (setf (elt result index) new))))
             (funcall cb (elt seq index))))))
 
a1ca234d
 (defun make-plist-lens (key)
cbd36776
   "A lens for updating a plist, preserving previous values"
a1ca234d
   (lambda (cb)
     (lambda (plist)
cbd36776
       (fmap (lambda (new)
               (list* key new
                      plist))
             (funcall cb (getf plist key))))))
a1ca234d
 
 (defun make-hash-table-lens (key)
cbd36776
   "A lens for updating a hash-table, discarding previous values"
a1ca234d
   (lambda (cb)
     (lambda (old-hash)
cbd36776
       (fmap (lambda (new)
1f0a4f7b
               (let ((new-hash (alexandria:copy-hash-table old-hash)))
                 (prog1 new-hash
                   (setf (gethash key new-hash)
                         new))))
cbd36776
             (funcall cb (gethash key old-hash))))))
a1ca234d
 
 ;; imagine a lens here that uses the MOP to immutably update a class...
 (defgeneric clone (obj &rest new-initargs &key)
   (:method :around (obj &rest new-initargs &key)
     (apply #'reinitialize-instance (call-next-method) new-initargs)))
 
 #+fw.demo
 (progn
   (defclass foo ()
     ((a :initarg :a :accessor a)))
   (defmethod clone ((obj foo) &key)
     (make-instance 'foo :a (a obj)))
 
cbd36776
   ;;; needs to be updated for functor-based lens
a1ca234d
   (defun a-lens (cb)
     (lambda (foo)
       (fw.lu:prog1-bind (new (clone foo))
         (setf (a new)
               (funcall cb (a foo))))))
   (view 'a-lens
         (over 'a-lens '1+
               (set 'a-lens 2
                    (make-instance 'foo :a 1)))) #|
   ==> 3 |#)
 
 
0850311d
 
4c31b0b9
 (defpackage :data-lens
   (:use :cl)
04f22d37
   (:import-from #:serapeum #:op #:defalias)
f909394c
   (:export #:regex-match #:include #:exclude #:pick #:key-transform
            #:combine #:derive #:cumsum #:over #:on #:shortcut #:defun-ct #:key
            #:extract-key #:element #:let-fn #:juxt #:transform-tail #:slice
            #:compress-runs #:combine-matching-lists #:sorted #:applicable-when
            #:of-length #:of-min-length #:of-max-length #:transform-head
a1ca234d
            #:maximizing #:zipping #:applying #:splice-elt #:transform-elt #:denest
            #:op #:defalias #:<> #:<>1))
4c31b0b9
 (in-package :data-lens)
 
0850311d
 
f7f7b3e6
 (declaim 
72c5d331
  (inline data-lens:over data-lens:transform-tail
          data-lens:applicable-when data-lens:of-min-length
          data-lens:on data-lens:over data-lens:slice
          data-lens:compress-runs data-lens:combine-matching-lists
          data-lens:juxt data-lens:element data-lens:sorted))
f7f7b3e6
 
0850311d
 ;;; TODO: consider making this wrap defalias?
4c31b0b9
 (defmacro shortcut (name function &body bound-args)
   `(eval-when (:load-toplevel :compile-toplevel :execute)
      (setf (fdefinition ',name)
            (,function ,@bound-args))))
 
 (defmacro defun-ct (name (&rest args) &body body)
   `(eval-when (:load-toplevel :compile-toplevel :execute)
      (defun ,name ,args
        ,@body)))
 
289289e7
 (defmacro let-fn ((&rest bindings) &body body)
   (let ((binding-forms (mapcar (lambda (form)
                                  `(,(car form) ,(cadr form)
f909394c
                                    (funcall ,@(cddr form) ,@(cadr form))))
289289e7
                                bindings)))
     `(labels ,binding-forms
        ,@body)))
 
 (defgeneric extract-key (map key)
   (:method ((map hash-table) key)
     (gethash key map))
   (:method ((map list) key)
     (typecase (car map)
       (cons (cdr (assoc key map :test 'equal)))
       (t (loop for (a-key . value) on map by #'cddr
f909394c
                when (equal key a-key) do
                  (return (car value)))))))
289289e7
 
 (defun-ct deduplicate (&optional (test 'eql))
   (lambda (it)
     (remove-duplicates it :test test)))
 
 (defun cons-new (&key (test 'eql) (key 'identity))
   (lambda (acc next)
     (if (and acc
              (funcall test
                       (funcall key (car acc))
                       (funcall key next)))
         acc
         (cons next acc))))
 
48ff3c11
 (defun matching-list-reducer (test acc next)
   (if (and acc
            (funcall test (caar acc) (car next)))
       (cons (cons (caar acc)
                   (append (cdar acc)
                           (cdr next)))
             (cdr acc))
       (cons next acc)))
 
289289e7
 (defun combine-matching-lists (&key (test 'eql) &allow-other-keys)
   (lambda (acc next)
48ff3c11
     (matching-list-reducer test acc next)))
289289e7
 
 (defun-ct compress-runs (&key (collector 'cons-new) (test 'eql) (key 'identity))
   (lambda (it)
     (nreverse
      (reduce (funcall collector :test test :key key)
              it
              :initial-value ()))))
 
f7f7b3e6
 (defun-ct of-length (len)
   (lambda (it)
     (= (length it)
        len)))
 
 (defun-ct of-min-length (len)
   (lambda (it)
     (>= (length it)
         len)))
 
 (defun-ct of-max-length (len)
   (lambda (it)
     (>= (length it)
         len)))
 
 (defun-ct applicable-when (fun test)
   (lambda (it)
     (if (funcall test it)
         (funcall fun it)
         it)))
 
289289e7
 (defun-ct sorted (comparator &rest r &key key)
   (declare (ignore key))
   (lambda (it)
48ff3c11
     (apply #'stable-sort (copy-seq it) comparator r)))
289289e7
 
 (defun-ct element (num)
   (lambda (it)
     (elt it num)))
 
 (defun-ct key (key)
   (lambda (map)
     (declare (dynamic-extent map))
     (extract-key map key)))
 
4c31b0b9
 (defun-ct regex-match (regex)
   (lambda (data)
     (cl-ppcre:scan-to-strings regex data)))
 
 (defun-ct include (pred)
   (lambda (seq)
     (remove-if-not pred seq)))
 
 (defun-ct exclude (pred)
   (lambda (seq)
     (remove-if pred seq)))
 
 (defun-ct pick (selector)
   (lambda (seq)
     (map 'list selector seq)))
 
7187d8e4
 (defun slice (start &optional end)
   (lambda (it)
     (subseq it start end)))
 
14b91cf5
 (defun-ct update (thing fun &rest args)
   (apply fun thing args))
 
 (define-modify-macro updatef (fun &rest args)
   update)
 
f7f7b3e6
 (defun-ct transform-head (fun)
   (lambda (it)
14b91cf5
     (typecase it
       (list (list* (funcall fun (car it))
                    (cdr it)))
       (vector (let ((result (copy-seq it)))
                 (prog1 result
                   (updatef (elt result 0) fun)))))))
f7f7b3e6
 
7187d8e4
 (defun-ct transform-tail (fun)
   (lambda (it)
14b91cf5
     (typecase it
       (list (list* (car it)
                    (funcall fun (cdr it))))
       (vector (let ((result (copy-seq it)))
                 (prog1 result
                   (updatef (subseq result 1)
                            fun)))))))
7187d8e4
 
a1ca234d
 (defun-ct splice-elt (elt fun)
bd1882ec
   (lambda (it)
     (append (subseq it 0 elt)
             (funcall fun (nth elt it))
             (subseq it (1+ elt)))))
 
a1ca234d
 (defun-ct transform-elt (elt fun)
   (lambda (it)
     (append (subseq it 0 elt)
             (list (funcall fun (nth elt it)))
             (subseq it (1+ elt)))))
 
4c31b0b9
 (defun-ct key-transform (fun key-get key-set)
   (lambda (it)
     (let ((key-val (funcall key-get it)))
       (funcall key-set
                (funcall fun key-val)))))
 
14b91cf5
 (defun-ct juxt (fun1 &rest r)
f7f7b3e6
   (lambda (&rest args)
     (list* (apply fun1 args)
            (when r
              (mapcar (lambda (f)
                        (apply f args))
                      r)))))
4c31b0b9
 
18dbfba4
 (defun =>> (fun1 fun2)
   (lambda (i)
     (prog1 (funcall fun1 i)
       (funcall fun2))))
 
4c31b0b9
 (defun-ct derive (diff-fun &key (key #'identity))
14b91cf5
   (lambda (seq)
     (typecase seq
       (list (cons (cons nil (car seq))
                   (mapcar (lambda (next cur)
                             (cons (funcall diff-fun
                                            (funcall key next)
                                            (funcall key  cur))
                                   next))
                           (cdr seq)
                           seq)))
       (vector (coerce (loop for cur = nil then next
                             for next across seq
                             if cur
                               collect (cons (funcall diff-fun
                                                      (funcall key next)
                                                      (funcall key cur))
                                             cur)
                             else collect (cons nil next))
                       'vector)))))
f7f7b3e6
 
 (defun-ct cumsum
     (&key (add-fun #'+) (key #'identity) (combine (lambda (x y) y x)) (zero 0))
4c31b0b9
   (lambda (seq)
     (nreverse
      (reduce (lambda (accum next)
                (let ((key-val (funcall key next))
                      (old-val (if accum
                                   (funcall key (car accum))
                                   zero)))
                  (cons (funcall combine
                                 (funcall add-fun old-val key-val)
                                 next)
                        accum)))
              seq
              :initial-value ()))))
 
 (defun-ct over (fun &key (result-type 'list))
   (lambda (seq)
     (map result-type fun seq)))
 
14b91cf5
 (defun-ct denest (&key (result-type 'list))
   (lambda (seq)
     (apply #'concatenate result-type
            seq)))
 
bd1882ec
 (defmacro applying (fun &rest args)
   (alexandria:with-gensyms (seq)
     `(lambda (,seq)
        (apply ,fun ,@args ,seq))))
 
4c31b0b9
 (defun-ct on (fun key)
   (lambda (it)
     (funcall fun (funcall key it))))
f7f7b3e6
 
 (defun filler (length1 length2 fill-value)
   (if (< length1 length2)
       (make-sequence 'vector (- length2 length1) :initial-element fill-value)
       #()))
 
 (defun-ct zipping (result-type &key (fill-value nil fill-value-p))
   (lambda (seq1 seq2)
     (let ((length1 (when fill-value-p (length seq1)))
           (length2 (when fill-value-p (length seq2))))
       (let ((seq1 (if fill-value-p
                       (concatenate result-type
                                    seq1
                                    (filler length1 length2 fill-value))
                       seq1))
             (seq2 (if fill-value-p
                       (concatenate result-type
                                    seq2
                                    (filler length2 length1 fill-value))
                       seq2)))
         (map result-type #'list
              seq1 seq2)))))
 
 (defun-ct maximizing (relation measure)
   (lambda (it)
     (let ((it-length (length it)))
       (when (> it-length 0)
         (values-list
72a785dc
          (reduce (lambda (|arg1764| |arg1765|)
                    (destructuring-bind (cur-max max-idx) |arg1764|
                      (destructuring-bind (next next-idx) |arg1765|
                        (if (funcall relation (funcall measure cur-max) (funcall measure next))
                            (list next next-idx)
                            (list cur-max max-idx)))))
f7f7b3e6
                  (funcall (zipping 'vector)
                           it
                           (alexandria:iota it-length))))))))
04f22d37
 
1f0a4f7b
 #+nil
04f22d37
 (defmacro <> (arity &rest funs)
   (let ((arg-syms (loop repeat arity collect (gensym))))
     `(lambda (,@arg-syms)
        (declare (dynamic-extent ,@arg-syms))
        ,(fw.lu:rollup-list (mapcar (lambda (x)
                                      (etypecase x
                                        (list `(funcall ,x))
                                        (symbol (list x))))
                                    funs)
                            arg-syms))))
 
 (defmacro <>1 (&rest funs)
1f0a4f7b
   `(alexandria:compose ,@funs))