美文网首页
Clojure学习笔记(二)——序列

Clojure学习笔记(二)——序列

作者: 简单一点点 | 来源:发表于2022-10-29 18:38 被阅读0次

    最近简单学习下Clojure,网上的资料也不是很多,自己写个小教程。

    在Clojure中,很多数据结构都可以通过同一个抽象概念来访问:序列(Seq)。

    可被视为序列的容器,被称为可序化的(seq-able,发音“SEEKabull”)。各种各样可序化的容器包括:

    • 所有的Clojure容器
    • 所有的Java容器
    • Java数组和字符串
    • 正则表达式的匹配结果
    • 目录结构
    • 输入/输出流
    • XML树

    一切皆为序列

    每一种聚合的数据结构,在Clojure中都能被视为序列。序列具有三大核心能力:

    • 你能够得到序列的第一个元素。
    (first aseq)
    
    • 你能够获取第一个元素后面的一切东西,换句话说,就是序列的剩余部分。
    (rest aseq)
    
    • 你可以通过向现有序列的前端添加元素,来创建一个新的序列。这就是所谓的cons。
    (cons elem aseq)
    

    seq函数会返回一个序列,该序列源自任何一个可序化的其他容器。

    (seq coll)
    

    如果coll是空的或者是nil,则seq返回nil。next函数也会返回一个序列,该序列由除第一个元素以外的其他所有元素组成。

    (next aseq)
    

    (next aseq)等价于 (seq (rest aseq))。

    映射表和集合的遍历顺序是稳定的,但这个顺序取决于具体的实现细节。

    sorted-set会依据自然顺序对值进行排序。

    (sorted-set& elements)
    

    sorted-map来创建一个有序的映射表。

    (sorted-map& elements)
    

    conj 会向容器添加一个或是多个元素,into 则会把容器中的所有元素添加至另一个容器。添加数据时,conj和into都会根据底层数据结构的特点选取最高效的插入点。

    • 对于列表而言,conj和into会在其前端进行添加。

    • 而对于向量,conj和into则会把元素添加至末尾。

    绝大多数Clojure序列都是惰性的:只有当确实需要时,它们才真正的把元素生成出来。因此,Clojure序列函数能够处理那些无法驻留在内存中的超大序列。

    Clojure序列是不可变的:它们永远都不会发生变化。所以我们可以很容易的就做出推断:Clojure序列在并发访问时是安全的。

    序列库

    Clojure的序列库包含以下四种函数:

    • 创建序列的函数。
    • 过滤序列的函数。
    • 序列谓词。
    • 序列转换函数。

    创建序列

    ranage会生成一个从start开始到end结束的序列,每次的增量为step。

    (range start? end step?)
    

    范围包含了start,但并不包含end。如果你没有指定的话,start默认为0,step默认为1。

    user=> (range 10)
    (0 1 2 3 4 5 6 7 8 9)
    user=> (range 10 20)
    (10 11 12 13 14 15 16 17 18 19)
    user=> (range 1 10 2)
    (1 3 5 7 9)
    

    repeat函数会重复n次元素x。当只传入一个参数时,repeat会返回一个惰性的无限序列。

    (repeat n x)
    
    user=> (repeat 5 1)
    (1 1 1 1 1)
    user=> (repeat 6 "x")
    ("x" "x" "x" "x" "x" "x")
    

    iterate起始于值x,并持续地对每个值应用函数f,以计算下一个值,直至永远。

    (iterate f x)
    

    take会返回一个包含了容器中前n项元素的惰性序列,这就提供了一种在无限序列上创建有限视图的途径。

    (take n sequence)
    

    cycle函数接受一个容器,并无限的对其进行循环。

    (cycle coll)
    

    interleave函数接受多个容器作为参数,并产生一个新的容器,这个新容器会从每个参数容器中交错地提取元素,直至其中某个容器元素被耗尽。

    (interleave& colls)
    

    当其中的某个容器被耗尽时,interleave就会终止。所以,你可以把有限容器与无限容器混合到一块儿。

    与interleave密切相关的interpose函数,把输入序列中的每个元素用分隔符隔开,并作为新的序列返回。

    (interpose separator coll)
    

    interpose和(apply str ...)的结合,正好可以用来产生输出字符串。

    作为一种惯用法,(apply str ...)是如此的常用,以至于Clojure甚至专门把它封装为clojure.string/join。

    (join separator sequence)
    

    对应每种Clojure中的容器类型,都有一个可以接受任意数量参数的函数,用来创建该类型的容器。

    (list_& elements)
    (vector_& elements)
    (hash-set_& elements)
    (hash-map key-1 val-1 ...)
    

    过滤序列

    filter接受一个谓词和一个容器作为参数,并返回一个序列,这个序列的所有元素都经谓词判定为真。

    (filter pred coll)
    
    user=> (filter even? '(1, 2, 3, 4, 5))
    (2 4)
    

    你还可以使用take-while从序列中截取开头的一段,其每个元素都被谓词判定为真。

    (take-while pred coll)
    

    drop-while 从序列的起始位置开始,逐个丢弃元素,直至谓词判定为真,然后返回序列剩余的部分。

    (drop-while pred coll)
    

    split-at和split-with能把一个容器一分为二。

    (split-at index coll)
    (split-with pred coll)
    

    split-at接受一个索引作为参数,而split-with则接受一个谓词。

    user=> (split-at 5 (range 10))
    [(0 1 2 3 4) (5 6 7 8 9)]
    user=> (split-with #(<= % 10) (range 0 20 2))
    [(0 2 4 6 8 10) (12 14 16 18)]
    

    所有take-、split-和drop-打头的函数,返回的都是惰性序列。

    序列谓词

    序列谓词会要求其他谓词应如何判定序列中的每一个元素。例如,every?要求其他谓词对序列中的每个元素都必须判定为真。

    (every? pred coll)
    
    user=> (every? odd? [1 3 5])
    true
    user=> (every? odd? [1 2 5])
    false
    

    只要有一个元素被谓词判定为非假,some就会返回这个值,如果没有任何元素符合,则some返回nil。

    (some pred coll)
    
    user=> (some even? [1 2 3])
    true
    user=> (some even? [1 3 5])
    nil
    

    注意,some没有以问号结尾。尽管总被当作谓词使用,但some并非谓词。因为 some 返回的是第一个符合项的值,而非 true。

    其他谓词从名称就能很明显的表现出其行为。

    (not-every? pred coll)
    (not-any? pred coll)
    

    序列转换

    转换函数用于对序列中的值进行转换。最简单的转换是映射函数map。

    (map f coll)
    

    map接受一个源容器coll和一个函数f作为参数,并返回一个新的序列。该序列的所有元素,都是通过对coll中的每个元素调用f得到的。

    还可以传入多个容器给map。在这种情况下,f必须是一个多参函数。map会从每个容器分别取出一个值,作为参数来调用f,直到数量最少的那个容器被耗尽为止。

    另一个常用的转换是归纳函数reduce。

    (reduce f coll)
    

    其中f是一个接受两个参数的函数。reduce首先用coll的前两个元素作为参数来调用f,然后用得到的结果和第三个元素作为参数,继续调用f,以此类推。

    user=> (reduce + (range 1 11))
    55
    

    你可以使用sort或sort-by对容器进行排序。

    (sort comp? coll)
    (sort-by a-fn comp? coll)
    

    sort 会依据元素的自然顺序对容器进行排序,sort-by 则会对每个元素调用 a-fn,再依据得到的结果序列来进行排序。

    user=> (sort [42 1 6 11])
    (1 6 11 42)
    user=> (sort-by #(.toString %) [42 1 6 11])
    (1 11 42 6)
    

    如果不打算按照自然顺序排序,你可以为sort或sort-by指定一个可选的比较函数comp。

    user=> (sort > [42 1 6 11])
    (42 11 6 1)
    

    惰性和无限序列

    大多数Clojure序列都是惰性的,换句话说,直到真的需要时,元素才会被计算出来。使用惰性序列有很多好处。

    • 你可以推迟那些实际上并不需要的昂贵计算。
    • 你可以处理超出内存允许范围的庞大数据集。
    • 你可以将I/O推迟至确实需要时才进行。

    当你在REPL中查看很大的序列时,你可以使用take来阻止REPL对整个序列进行求值。

    在另外一些情况下,你遇到的问题可能正好相反。doall迫使Clojure遍历序列中的元素,并把这些元素作为结果返回。

    (doall coll)
    

    你还可以使用dorun。

    (dorun coll)
    

    dorun 同样会遍历容器中的元素,但它不会把穿过的元素保留在内存中。这样的结果是,dorun可以遍历那些超过了内存容许范围的超大容器。

    调用特定于结构的函数

    Clojure包含了一些特定目标的函数,面向列表、向量、映射表、结构和集合。

    列表函数

    Clojure支持peek和pop这两个名称比较传统的函数,分别用于取出列表的第一个元素,和其余的那些元素。

    (peek coll)
    (pop coll)
    
    user=> (peek '(1 2 3))
    1
    user=> (pop '(1 2 3))
    (2 3)
    

    peek等同于first,但pop则与rest不同。如果是空序列,pop会抛出一个异常。

    user=> (rest ())
    ()
    user=> (pop ())
    Execution error (IllegalStateException) at user/eval53 (REPL:1).
    Can't pop empty list
    

    向量函数

    向量也支持peek和pop,但它们是从向量的末尾开始处理元素的。

    user=> (peek [1 2 3])
    3
    user=> (pop [1 2 3])
    [1 2]
    

    get返回索引位置的值,倘若索引超出了向量边界,则返回nil。

    user=> (get [:a :b :c] 1)
    :b
    

    向量自身也是函数。它接受一个索引作为参数并返回对应的值,或是当索引超过边界时,抛出一个异常。

    user=> ([:a :b :c] 1)
    :b
    

    assoc会在指定的索引位置,关联一个新的值。

    user=> (assoc [0 1 2 3 4] 2 :two)
    [0 1 :two 3 4]
    

    subvec会返回向量的一个子向量。

    (subvec avec start end?)
    

    若未指定end,则默认为向量的末尾。

    当然,你也可以通过组合drop和take来模拟subvec。不同之处在于,take和drop非常通用,可用于任何序列。令一方面,对于向量而言,subvec要更快一些。每当你看到subvec这样特定于结构的函数,同时其功能又与序列库中的某个函数产生了重复时,那最有可能的理由就是性能。

    映射表

    Clojure提供了几个用于从映射表中读取键和值的函数。keys将所有的键作为序列返回,vals则将所有的值作为序列返回。

    (keys map)
    (vals map)
    

    get会返回键对应的值,或者是返回nil。

    (get map key value-if-not-found?)
    

    有一种比用 get 更简单的方法。因为映射表同时也是一个函数,以它自己的键作为参数。

    关键字同样也是函数。他接受一个容器作为参数,并在这个容器中查找其自身。

    如果在映射表中查找一个键并返回了 nil,你无法确认究竟是键对应的值为nil,还是这个键在映射表中根本就不存在。contains?函数就可以解决这个问题,它只是单纯的检测某个键是否存住。

    (contains? map key)
    

    另外一种方法是调用get,它可以传入可选的第三个参数,如果键未能找到,那么就会返回这个参数的值。

    Clojure还提供了几个会构建新映射表的函数。

    • assoc返回新增了一个键值对的映射表。
    • dissoc返回移除了某些键的映射表。
    • select-keys返回一个映射表,仅保留了参数传入的那些键。
    • merge可以合并映射表。如果多个映射表包含了同一个键,那么最右边的获胜。

    最有趣的映射表构建函数是merge-with。

    (merge-with merge-fn & maps)
    

    merge-with与merge很类似,除了当两个或以上的映射表中有相同的键时,你能指定一个你自己的函数,来决定如何合并这个键对应的值。

    集合函数

    clojure.set函数执行由集合论而来的操作。

    • union返回的集合,包含了所有输入集合中的元素。
    • intersection返回的集合,其所有元素都曾同时出现于多个输入集合中。
    • difference 返回的集合,其所有元素都出现于第一个输入集合,但却未出现于第二个中。
    • select返回所有元素都能与给定谓词相匹配的一个集合。

    rename函数可以用来给键(数据库的列)重新命名,基于一个映射表,把原来的名称改成新的。

    (rename relation rename-map)
    

    select 函数返回经谓词计算结果为真的那些映射表,这里的谓词与 SQL 语言中SELECT语句的WHERE部分非常类似。

    (select pred relation)
    

    project函数返回的那些映射表中,仅包含与参数匹配的键。

    (project relation keys)
    

    project与指定了列的子集的SQL SELECT语句非常类似。

    相关文章

      网友评论

          本文标题:Clojure学习笔记(二)——序列

          本文链接:https://www.haomeiwen.com/subject/koqctdtx.html