lisp:对向量进行分块

lisp:对向量进行分块,lisp,common-lisp,Lisp,Common Lisp,如前所述,我试图通过实现lisp来自学lisp 我基本上没有使用lisp的经验,所以在js中微不足道的工作对我来说是陌生的 例如,我正在研究一个..chunk方法的实现,它在js中采用一个数组和一个size变量,并按大小“chunk”数组: _.chunk(['a', 'b', 'c', 'd'], 2); // => [['a', 'b'], ['c', 'd']] _.chunk(['a', 'b', 'c', 'd'], 3); // => [['a', 'b', 'c'],

如前所述,我试图通过实现lisp来自学lisp

我基本上没有使用lisp的经验,所以在js中微不足道的工作对我来说是陌生的

例如,我正在研究一个
..chunk
方法的实现,它在js中采用一个数组和一个
size
变量,并按大小“chunk”数组:

_.chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]

_.chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']] 
作为一个对常见lisp数据类型完全陌生的人,我假设类似的类型是向量,而不是数组,对吗

其次,我用算法解决这个问题的方法是保留
length
变量和
pointer
变量,并获取数组/向量的子集,[pointer-to-pointer+size],而pointer+size为 不知道如何在lisp中实现这一点,这是我到目前为止的代码

(defun _.chunk (vector &optional (size 1 size-p))
   (if (or (not size-p) (eq size 1)) 
        vector
        ((let   (                    
                    (array_length (array-total-size array))
                    (pointer)
                )
            ???
        ))      
    )  
)

对于这个实现,我将首先编写一个惯用的通用Lisp版本的chunk,它可以在CL程序中使用(高效等),然后编写一个仅围绕这些函数的薄薄lodash层

例如,我将首先编写一个helper函数,以允许与分块向量共享存储。置换数组是指具有偏移量和不同大小的另一个数组。将块作为原始向量的唯一视图可能很有用,这样它们都共享相同的底层存储阵列。这不仅仅是一种内存优化:当改变块或原始向量时,行为是不同的,因为其中一个的任何变化在另一个中都是可见的。但据我所知,lodash是(曾经是?)一种纯函数式语言,因此,如果不进行变异,共享一些数据是有意义的。有些语言将这种间接数组称为“切片”


因此,我还将使
chunk vector
接受
:start
:end
参数,就像通常所做的一样,以及
sharedp
参数,该参数指定是否应与原始向量共享存储:

(defun chunk-vector (size vector &key start end sharedp)
  (check-type size (integer 1))
  (loop
     with slicer = (if sharedp #'slice #'subseq)
     and low = (or start 0)
     and high = (or end (length vector))
     for s from low below high by size
     for e from (+ low size) by size
     collect (funcall slicer vector s (min e high))))
注意:我假设
nil
end
的一个可能值,表示向量的结束,以反映
subseq
的工作方式。我对
start
也这样做,因为对于这些变量,可以使用nil值来表示“默认值”,而不会产生歧义。我还可以在lambda列表中定义默认值,就像tfb的回答中那样

以下是一些测试:

(chunk-vector 3 #(0 1 2 3 4 5 6 7 8 9) :sharedp t)
(#(0 1 2) #(3 4 5) #(6 7 8) #(9))

(chunk-vector 2 #(0 1 2 3 4 5 6 7 8 9))
(#(0 1) #(2 3) #(4 5) #(6 7) #(8 9))

(chunk-vector 1 #(0 1 2 3 4 5 6 7 8 9))
(#(0) #(1) #(2) #(3) #(4) #(5) #(6) #(7) #(8) #(9))
同样,您还可以定义
块列表
函数,并根据序列类型将lodash
chunck
函数分派到每个专用版本

这可以通过CLOS来完成,但因为这已经在另一个答案中演示过了,所以我将只定义单独的专用函数

下面是基于的
块列表的实现。
我首先尝试在一个函数中混合所有情况,但这变得不必要的复杂

下面是第一个无界块函数:

(defun chunk-list/unbounded (size list)
  (loop
     for front = list then next
     for next = (nthcdr size front)
     collect (ldiff front next)
     while next))
(defgeneric chunk (thing &key)
  ;; in real life we might want to specify some of the keyword
  ;; arguments at the GF level, but we won't
  )
(defmethod chunk ((thing vector) &key
                  (size 1) (start 0) (end (length thing)) (sharedp nil))
  (check-type size (integer 1))
  (let ((slicer (if sharedp #'slice #'subseq)))
    (loop for s from start below end by size
          for e from (+ start size) by size
          collect (funcall slicer thing s (min e end)))))
  • front
    最初定义为
    list
    ,然后是每个步骤的
    next
    的当前值
  • next
    是下一个块,使用
    size
    计算;这很好地处理了没有足够元素的列表,因为在这种情况下,
    nthcdr
    只返回剩余的元素
处理
end
参数需要更复杂的情况,为此,我们定义了有界版本,其中还有一个额外的
上限
计数器,它在迭代的每个步骤中减少
大小
。它表示要添加的剩余元素数,并与大小一起用于计算下一个块的大小
(最小大小上限)

(defun chunk-list/bounded (size list upper-limit)
  (loop
     for front = list then next
     for next = (nthcdr (min size upper-limit) front)
     collect (ldiff front next)
     do (decf upper-limit size)
     while (and next (plusp upper-limit))))
最后,
chunk list
根据
end
是否为零对两个版本进行调度;调用在此处内联(因为我们可以):

一些例子:

(chunk-list 3 '(1 2 3 4 5 6 7))
((1 2 3) (4 5 6) (7))

(chunk-list 29 '(1 2))
((1 2))

(chunk-list 2 (alexandria:iota 100 :start 0) :start 10 :end 20)
((10 11) (12 13) (14 15) (16 17) (18 19))

我建议使用
dotimes
对区块索引进行逐步切片迭代(因为您可以很容易地找到区块的总量)

这可能类似于以下内容:

(defun chunked (seq size)
  (let* ((total (length seq))
         (amount (ceiling total size))
         (res (make-array amount :fill-pointer 0)))
    (dotimes (i amount res)
      (vector-push (subseq seq (* i size) (min (* (1+ i) size) total))
                   res))))


CL-USER> (chunked "abcdefgh" 3)
;; #("abc" "def" "gh")

CL-USER> (chunked #*00101 2)
;; #(#*00 #*10 #*1)

CL-USER> (chunked (list :a :b :c :d :e) 1)
;; #((:A) (:B) (:C) (:D) (:E))

CL-USER> (chunked (list :a :b :c :d :e) 4)
;; #((:A :B :C :D) (:E))

这是coredump回答的附录,同时也引用了Kaz的评论。其中大部分是关于风格的,这始终是一个意见问题,我并不认为我的意见比他们的好:我只是觉得谈论选择很有趣,因为Lisp编程非常关注风格选择,因为与大多数其他语言相比,该语言非常灵活。然而,最后一节(“扩展”)可能很有趣

参数顺序 签名
(大小向量…
的问题在于
大小
不能是可选的。如果您希望它是,它不能是函数的第一个参数。我不知道这是否超过了部分应用程序库的简单实用性(但是,本着“做正确的事情”的精神,如果我编写了部分应用程序库,它将允许您指定它使用的参数,因此这不会是一个问题)

因此,如果
大小
需要是可选的,那么参数顺序必须是
(向量大小…

此外,由于coredump的答案使用关键字参数,我会将
size
设置为一个,因为您几乎不想混合关键字和可选参数。这样就产生了一个签名,
(vector&key size start-end-sharedp)
,然后我将实际函数写成

(defun chunk-vector (vector &key (size 1) (start 0) (end (length vector))
                            (sharedp nil))
  (check-type size (integer 1))
  (let ((slicer (if sharedp #'slice #'subseq)))
       (loop for s from start below end by size
             for e from (+ start size) by size
             collect (funcall slicer thing s (min e end)))))
这稍微改进了coredump的版本,默认了arglist中的参数,而不是更高版本

扩展
块向量
很明显,您可能需要对其他类型的内容进行分块,例如列表,而且很明显,对列表进行分块的算法与对向量进行分块的算法会有很大的不同,因为您确实不希望在列表上重复调用
subseq

这是w
(defgeneric chunk (thing &key)
  ;; in real life we might want to specify some of the keyword
  ;; arguments at the GF level, but we won't
  )
(defmethod chunk ((thing vector) &key
                  (size 1) (start 0) (end (length thing)) (sharedp nil))
  (check-type size (integer 1))
  (let ((slicer (if sharedp #'slice #'subseq)))
    (loop for s from start below end by size
          for e from (+ start size) by size
          collect (funcall slicer thing s (min e end)))))
(defmethod chunk ((thing list) &key
                  (size 1) (start 0) (end nil endp) (sharedp nil))
  ;; This does not implemenent SHAREDP: this could only be useful for
  ;; the last chunk, and since you don't know if you could share a
  ;; chunk until you have already walked the list it did not seem
  ;; worth it.  It may also be buggy in its handling of END.
  (declare (ignorable sharedp))
  (flet ((next (lt)
           (nthcdr size lt))
         (the-chunk (lt p)
           (loop for c below (if endp (min size (- end p)) size)
                 for e in lt
                 do (print c)
                 collect e)))
    (loop for tail on (nthcdr start thing) by #'next
          for pos upfrom start by size
          while (or (not endp) (< pos end))
          collect (the-chunk tail pos))))
(defun reshape-2d (column-count vector &optional padding-element)
  (let* ((row-count (ceiling (length vector) column-count))
         (array (make-array (list row-count column-count)
                            :initial-element padding-element)))
    (loop :for i :below (length vector)
          :do (setf (row-major-aref array i) (aref vector i)))
    array))
(defun chunkv (size vector)
  (let ((vectors (make-array (ceiling (length vector) size))))
    (loop :for i :below (length vector) :by size
          :for j :below (length vectors)
          :do (setf (aref vectors j) (subseq vector
                                             i
                                             (min (1- (length vector))
                                                  (+ i size)))))
    vectors))
(defun chunkl (size vector)
  (loop :for i :below (length vector) :by size
        :collect (subseq vector
                         i
                         (min (1- (length vector))
                              (+ i size)))))