From bd82d54a2df5924e8b11d0702c056b308c57fadc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 14 Mar 2026 01:02:28 +0000 Subject: [PATCH 1/2] feat: add filter, iter, exists, forall, find, choose, head, toArray, toList, ofList to PersistentVector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 14 new module-level functions to PersistentVector, further addressing issue #152 (aligning collection module functions with FSharp.Collections). New functions: - choose – filter and map in one pass - exists – short-circuiting 'any' check - filter – return elements satisfying predicate - find – first matching element (throws if not found) - tryFind – first matching element (returns option) - findIndex – index of first match (throws if not found) - tryFindIndex – index of first match (returns option) - forall – short-circuiting 'all' check - head – first element (throws if empty) - tryHead – first element (returns option) - iter – apply action to each element - iteri – apply action with index to each element - ofList – create from a list - toArray – convert to an array - toList – convert to a list Adds 26 new unit tests covering normal cases, empty vectors, and error paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/FSharpx.Collections/PersistentVector.fs | 119 ++++++++++++++ src/FSharpx.Collections/PersistentVector.fsi | 137 ++++++++++------ .../PersistentVectorTest.fs | 147 ++++++++++++++++++ 3 files changed, 358 insertions(+), 45 deletions(-) diff --git a/src/FSharpx.Collections/PersistentVector.fs b/src/FSharpx.Collections/PersistentVector.fs index 656b5be8..e09abf7c 100644 --- a/src/FSharpx.Collections/PersistentVector.fs +++ b/src/FSharpx.Collections/PersistentVector.fs @@ -592,6 +592,112 @@ module PersistentVector = | Some v' -> tryNth j v' | None -> None + let choose (f: 'T -> 'T1 option) (vector: PersistentVector<'T>) : PersistentVector<'T1> = + let mutable ret = TransientVector() + + for item in vector do + match f item with + | Some v -> ret <- ret.conj v + | None -> () + + ret.persistent() + + let exists (f: 'T -> bool) (vector: PersistentVector<'T>) = + let mutable found = false + let mutable i = 0 + + while not found && i < vector.Length do + if f vector.[i] then + found <- true + + i <- i + 1 + + found + + let filter (f: 'T -> bool) (vector: PersistentVector<'T>) : PersistentVector<'T> = + let mutable ret = TransientVector() + + for item in vector do + if f item then + ret <- ret.conj item + + ret.persistent() + + let findIndex (f: 'T -> bool) (vector: PersistentVector<'T>) = + let mutable result = -1 + let mutable i = 0 + + while result = -1 && i < vector.Length do + if f vector.[i] then + result <- i + + i <- i + 1 + + if result = -1 then + raise(System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection.")) + else + result + + let tryFindIndex (f: 'T -> bool) (vector: PersistentVector<'T>) = + let mutable result = -1 + let mutable i = 0 + + while result = -1 && i < vector.Length do + if f vector.[i] then + result <- i + + i <- i + 1 + + if result = -1 then None else Some result + + let find (f: 'T -> bool) (vector: PersistentVector<'T>) = + vector.[findIndex f vector] + + let tryFind (f: 'T -> bool) (vector: PersistentVector<'T>) = + match tryFindIndex f vector with + | Some i -> Some vector.[i] + | None -> None + + let forall (f: 'T -> bool) (vector: PersistentVector<'T>) = + let mutable allMatch = true + let mutable i = 0 + + while allMatch && i < vector.Length do + if not(f vector.[i]) then + allMatch <- false + + i <- i + 1 + + allMatch + + let inline head(vector: PersistentVector<'T>) = + if vector.IsEmpty then + invalidArg "vector" "The input vector was empty." + else + vector.[0] + + let inline tryHead(vector: PersistentVector<'T>) = + if vector.IsEmpty then None else Some vector.[0] + + let iter (f: 'T -> unit) (vector: PersistentVector<'T>) = + for item in vector do + f item + + let iteri (f: int -> 'T -> unit) (vector: PersistentVector<'T>) = + let mutable i = 0 + + for item in vector do + f i item + i <- i + 1 + + let ofList(items: 'T list) : PersistentVector<'T> = + let mutable ret = TransientVector() + + for item in items do + ret <- ret.conj item + + ret.persistent() + let ofSeq(items: 'T seq) = PersistentVector.ofSeq items @@ -601,6 +707,19 @@ module PersistentVector = let inline singleton(x: 'T) = empty |> conj x + let toArray(vector: PersistentVector<'T>) = + let arr = Array.zeroCreate vector.Length + let mutable i = 0 + + for item in vector do + arr.[i] <- item + i <- i + 1 + + arr + + let toList(vector: PersistentVector<'T>) : 'T list = + foldBack (fun item acc -> item :: acc) vector [] + let rangedIterator (startIndex: int) (endIndex: int) (vector: PersistentVector<'T>) = vector.rangedIterator(startIndex, endIndex) diff --git a/src/FSharpx.Collections/PersistentVector.fsi b/src/FSharpx.Collections/PersistentVector.fsi index 34f12296..bab80122 100644 --- a/src/FSharpx.Collections/PersistentVector.fsi +++ b/src/FSharpx.Collections/PersistentVector.fsi @@ -13,144 +13,191 @@ type PersistentVector<'T> = interface System.Collections.Generic.IReadOnlyCollection<'T> /// O(1). Returns a new vector with the element added at the end. - member Conj : 'T -> PersistentVector<'T> + member Conj: 'T -> PersistentVector<'T> /// O(1) for all practical purposes; really O(log32n). Returns a new vector without the last item. If the collection is empty it throws an exception. - member Initial : PersistentVector<'T> + member Initial: PersistentVector<'T> /// O(1) for all practical purposes; really O(log32n). Returns option vector without the last item. - member TryInitial : PersistentVector<'T> option + member TryInitial: PersistentVector<'T> option /// O(1). Returns true if the vector has no elements. - member IsEmpty : bool + member IsEmpty: bool /// O(1). Returns a new PersistentVector with no elements. - static member Empty : unit -> PersistentVector<'T> + static member Empty: unit -> PersistentVector<'T> /// O(1) for all practical purposes; really O(log32n). Returns vector element at the index. - member Item : int -> 'T with get + member Item: int -> 'T with get /// O(1). Returns the last element in the vector. If the vector is empty it throws an exception. - member Last : 'T + member Last: 'T /// O(1). Returns option last element in the vector. - member TryLast : 'T option + member TryLast: 'T option /// O(1). Returns the number of items in the vector. - member Length : int + member Length: int /// O(n). Returns random access list reversed. - member Rev : unit -> PersistentVector<'T> + member Rev: unit -> PersistentVector<'T> /// O(1) for all practical purposes; really O(log32n). Returns tuple last element and vector without last item - member Unconj : PersistentVector<'T> * 'T + member Unconj: PersistentVector<'T> * 'T /// O(1) for all practical purposes; really O(log32n). Returns option tuple last element and vector without last item - member TryUnconj : (PersistentVector<'T> * 'T) option + member TryUnconj: (PersistentVector<'T> * 'T) option /// O(1) for all practical purposes; really O(log32n). Returns a new vector that contains the given value at the index. - member Update : int * 'T -> PersistentVector<'T> + member Update: int * 'T -> PersistentVector<'T> /// O(1) for all practical purposes; really O(log32n). Returns option vector that contains the given value at the index. - member TryUpdate : int * 'T -> PersistentVector<'T> option + member TryUpdate: int * 'T -> PersistentVector<'T> option /// Defines functions which allow to access and manipulate PersistentVectors. [] module PersistentVector = //pattern discriminators (active pattern) - val (|Conj|Nil|) : PersistentVector<'T> -> Choice<(PersistentVector<'T> * 'T),unit> + val (|Conj|Nil|): PersistentVector<'T> -> Choice<(PersistentVector<'T> * 'T), unit> /// O(n). Returns a new vector with the elements of the second vector added at the end. - val append : PersistentVector<'T> -> PersistentVector<'T> -> PersistentVector<'T> + val append: PersistentVector<'T> -> PersistentVector<'T> -> PersistentVector<'T> /// O(1). Returns a new vector with the element added at the end. - val inline conj : 'T -> PersistentVector<'T> -> PersistentVector<'T> + val inline conj: 'T -> PersistentVector<'T> -> PersistentVector<'T> /// O(1). Returns vector of no elements. [] val empty<'T> : PersistentVector<'T> /// O(m,n). Returns a seq from a vector of vectors. - val inline flatten : PersistentVector> -> seq<'T> + val inline flatten: PersistentVector> -> seq<'T> /// O(n). Returns a state from the supplied state and a function operating from left to right. - val inline fold : ('State -> 'T -> 'State) -> 'State -> PersistentVector<'T> -> 'State + val inline fold: ('State -> 'T -> 'State) -> 'State -> PersistentVector<'T> -> 'State /// O(n). Returns a state from the supplied state and a function operating from right to left. - val inline foldBack : ('T -> 'State -> 'State) -> PersistentVector<'T> -> 'State -> 'State + val inline foldBack: ('T -> 'State -> 'State) -> PersistentVector<'T> -> 'State -> 'State /// O(n). Returns a vector of the supplied length using the supplied function operating on the index. - val init : int -> (int -> 'T) -> PersistentVector<'T> + val init: int -> (int -> 'T) -> PersistentVector<'T> /// O(1) for all practical purposes; really O(log32n). Returns a new vector without the last item. If the collection is empty it throws an exception. - val inline initial : PersistentVector<'T> -> PersistentVector<'T> + val inline initial: PersistentVector<'T> -> PersistentVector<'T> /// O(1) for all practical purposes; really O(log32n). Returns option vector without the last item. - val inline tryInitial : PersistentVector<'T> -> PersistentVector<'T> option + val inline tryInitial: PersistentVector<'T> -> PersistentVector<'T> option /// O(1). Returns true if the vector has no elements. - val inline isEmpty : PersistentVector<'T> -> bool + val inline isEmpty: PersistentVector<'T> -> bool /// O(1). Returns the last element in the vector. If the vector is empty it throws an exception. - val inline last : PersistentVector<'T> -> 'T + val inline last: PersistentVector<'T> -> 'T /// O(1). Returns option last element in the vector. - val inline tryLast : PersistentVector<'T> -> 'T option + val inline tryLast: PersistentVector<'T> -> 'T option /// O(1). Returns the number of items in the vector. - val inline length : PersistentVector<'T> -> int + val inline length: PersistentVector<'T> -> int /// O(n). Returns a vector whose elements are the results of applying the supplied function to each of the elements of a supplied vector. - val map : ('T -> 'T1) -> PersistentVector<'T> -> PersistentVector<'T1> + val map: ('T -> 'T1) -> PersistentVector<'T> -> PersistentVector<'T1> /// O(n). Returns a vector whose elements are the results of applying the supplied function to each of the indices and elements of a supplied vector. - val mapi : (int -> 'T -> 'T1) -> PersistentVector<'T> -> PersistentVector<'T1> + val mapi: (int -> 'T -> 'T1) -> PersistentVector<'T> -> PersistentVector<'T1> /// O(1) for all practical purposes; really O(log32n). Returns the value at the index. If the index is out of bounds it throws an exception. - val inline nth : int -> PersistentVector<'T> -> 'T + val inline nth: int -> PersistentVector<'T> -> 'T /// O(log32(m,n)). Returns the value at the outer index, inner index. If either index is out of bounds it throws an exception. - val inline nthNth : int -> int -> PersistentVector> -> 'T + val inline nthNth: int -> int -> PersistentVector> -> 'T /// O(1) for all practical purposes; really O(log32n). Returns option value at the index. - val inline tryNth : int -> PersistentVector<'T> -> 'T option + val inline tryNth: int -> PersistentVector<'T> -> 'T option /// O(log32(m,n)). Returns option value at the indices. - val inline tryNthNth : int -> int -> PersistentVector> -> 'T option + val inline tryNthNth: int -> int -> PersistentVector> -> 'T option + + /// O(n). Returns a new vector containing only elements for which the supplied function returns Some. + val choose: ('T -> 'T1 option) -> PersistentVector<'T> -> PersistentVector<'T1> + + /// O(n). Returns true if the given predicate returns true for some element in the vector. + val exists: ('T -> bool) -> PersistentVector<'T> -> bool + + /// O(n). Returns a new vector containing only the elements of the supplied vector for which the given predicate returns true. + val filter: ('T -> bool) -> PersistentVector<'T> -> PersistentVector<'T> + + /// O(n). Returns the index of the first element in the vector that satisfies the given predicate. Raises KeyNotFoundException if not found. + val findIndex: ('T -> bool) -> PersistentVector<'T> -> int + + /// O(n). Returns the first element in the vector that satisfies the given predicate. Raises KeyNotFoundException if not found. + val find: ('T -> bool) -> PersistentVector<'T> -> 'T + + /// O(n). Returns true if the given predicate returns true for all elements in the vector. + val forall: ('T -> bool) -> PersistentVector<'T> -> bool + + /// O(1) for all practical purposes; really O(log32n). Returns the first element in the vector. Raises ArgumentException if the vector is empty. + val inline head: PersistentVector<'T> -> 'T + + /// O(n). Applies the given function to each element of the vector. + val iter: ('T -> unit) -> PersistentVector<'T> -> unit + + /// O(n). Applies the given function to each element of the vector, passing the index as the first argument. + val iteri: (int -> 'T -> unit) -> PersistentVector<'T> -> unit + + /// O(n). Returns a new vector from the supplied list. + val ofList: 'T list -> PersistentVector<'T> /// O(n). Returns a vector of the seq. - val ofSeq : seq<'T> -> PersistentVector<'T> + val ofSeq: seq<'T> -> PersistentVector<'T> /// O(n). Returns vector reversed. - val inline rev : PersistentVector<'T> -> PersistentVector<'T> + val inline rev: PersistentVector<'T> -> PersistentVector<'T> /// O(1). Returns a new vector of one element. - val inline singleton : 'T -> PersistentVector<'T> + val inline singleton: 'T -> PersistentVector<'T> /// O(n). Views a subset of the given vector. startIndex is inclusive, endIndex is exclusive. /// `rangedIterator 0 count` is the same as toSeq - val rangedIterator : int -> int -> PersistentVector<'T> -> seq<'T> + val rangedIterator: int -> int -> PersistentVector<'T> -> seq<'T> + + /// O(n). Returns the elements of the vector as an array. + val toArray: PersistentVector<'T> -> 'T[] + + /// O(n). Returns the elements of the vector as a list. + val toList: PersistentVector<'T> -> 'T list /// O(n). Views the given vector as a sequence. - val inline toSeq : PersistentVector<'T> -> seq<'T> + val inline toSeq: PersistentVector<'T> -> seq<'T> + + /// O(n). Returns the index of the first element in the vector that satisfies the given predicate, or None if not found. + val tryFindIndex: ('T -> bool) -> PersistentVector<'T> -> int option + + /// O(n). Returns the first element in the vector that satisfies the given predicate, or None if not found. + val tryFind: ('T -> bool) -> PersistentVector<'T> -> 'T option + + /// O(1) for all practical purposes; really O(log32n). Returns option first element in the vector. + val inline tryHead: PersistentVector<'T> -> 'T option /// O(1) for all practical purposes; really O(log32n). Returns tuple last element and vector without last item - val inline unconj : PersistentVector<'T> -> PersistentVector<'T> * 'T + val inline unconj: PersistentVector<'T> -> PersistentVector<'T> * 'T /// O(1) for all practical purposes; really O(log32n). Returns option tuple last element and vector without last item - val inline tryUnconj : PersistentVector<'T> -> (PersistentVector<'T> * 'T) option + val inline tryUnconj: PersistentVector<'T> -> (PersistentVector<'T> * 'T) option /// O(1) for all practical purposes; really O(log32n). Returns a new vector that contains the given value at the index. - val inline update : int -> 'T -> PersistentVector<'T> -> PersistentVector<'T> + val inline update: int -> 'T -> PersistentVector<'T> -> PersistentVector<'T> /// O(log32(m,n)). Returns a new vector of vectors that contains the given value at the indices. - val inline updateNth : int -> int -> 'T -> PersistentVector> -> PersistentVector> + val inline updateNth: + int -> int -> 'T -> PersistentVector> -> PersistentVector> /// O(1) for all practical purposes; really O(log32n). Returns option vector that contains the given value at the index. - val inline tryUpdate : int -> 'T -> PersistentVector<'T> -> PersistentVector<'T> option + val inline tryUpdate: int -> 'T -> PersistentVector<'T> -> PersistentVector<'T> option /// O(log32(m,n)). Returns option vector that contains the given value at the indices. - val inline tryUpdateNth : int -> int -> 'T -> PersistentVector> -> PersistentVector> option + val inline tryUpdateNth: + int -> int -> 'T -> PersistentVector> -> PersistentVector> option /// O(n). Returns a vector of vectors of given length from the seq. Result may be a jagged vector. - val inline windowSeq : int -> seq<'T> -> PersistentVector> + val inline windowSeq: int -> seq<'T> -> PersistentVector> diff --git a/tests/FSharpx.Collections.Tests/PersistentVectorTest.fs b/tests/FSharpx.Collections.Tests/PersistentVectorTest.fs index 851fda98..65cb20d8 100644 --- a/tests/FSharpx.Collections.Tests/PersistentVectorTest.fs +++ b/tests/FSharpx.Collections.Tests/PersistentVectorTest.fs @@ -445,6 +445,153 @@ module PersistentVectorTests = (i - 1) + i * 2 |> Expect.equal "map" a.[i - 1] } + test "filter should return elements satisfying predicate" { + let v = PersistentVector.ofSeq [ 1; 2; 3; 4; 5; 6 ] + let result = PersistentVector.filter (fun x -> x % 2 = 0) v + Expect.equal "filter even" [ 2; 4; 6 ] (result |> Seq.toList) + } + + test "filter empty vector returns empty" { + let result = PersistentVector.filter (fun x -> x > 0) PersistentVector.empty + Expect.equal "filter empty" 0 (result |> PersistentVector.length) + } + + test "filter all excluded returns empty" { + let v = PersistentVector.ofSeq [ 1; 3; 5 ] + let result = PersistentVector.filter (fun x -> x % 2 = 0) v + Expect.equal "filter none" 0 (result |> PersistentVector.length) + } + + test "iter applies function to each element" { + let v = PersistentVector.ofSeq [ 1; 2; 3 ] + let mutable sum = 0 + PersistentVector.iter (fun x -> sum <- sum + x) v + Expect.equal "iter sum" 6 sum + } + + test "iter on empty vector does nothing" { + let mutable count = 0 + PersistentVector.iter (fun _ -> count <- count + 1) PersistentVector.empty + Expect.equal "iter empty" 0 count + } + + test "iteri passes correct indices" { + let v = PersistentVector.ofSeq [ 10; 20; 30 ] + let mutable idxSum = 0 + PersistentVector.iteri (fun i _ -> idxSum <- idxSum + i) v + Expect.equal "iteri indices" 3 idxSum // 0+1+2 + } + + test "exists returns true when element found" { + let v = PersistentVector.ofSeq [ 1; 2; 3 ] + Expect.isTrue "exists" (PersistentVector.exists (fun x -> x = 2) v) + } + + test "exists returns false when no element found" { + let v = PersistentVector.ofSeq [ 1; 2; 3 ] + Expect.isFalse "exists none" (PersistentVector.exists (fun x -> x = 9) v) + } + + test "exists on empty returns false" { + Expect.isFalse "exists empty" (PersistentVector.exists (fun _ -> true) PersistentVector.empty) + } + + test "forall returns true when all elements match" { + let v = PersistentVector.ofSeq [ 2; 4; 6 ] + Expect.isTrue "forall even" (PersistentVector.forall (fun x -> x % 2 = 0) v) + } + + test "forall returns false when some element does not match" { + let v = PersistentVector.ofSeq [ 2; 3; 6 ] + Expect.isFalse "forall not all even" (PersistentVector.forall (fun x -> x % 2 = 0) v) + } + + test "forall on empty returns true" { + Expect.isTrue "forall empty" (PersistentVector.forall (fun _ -> false) PersistentVector.empty) + } + + test "find returns first matching element" { + let v = PersistentVector.ofSeq [ 1; 2; 3; 4 ] + Expect.equal "find" 3 (PersistentVector.find (fun x -> x > 2) v) + } + + test "find throws when not found" { + let v = PersistentVector.ofSeq [ 1; 2; 3 ] + Expect.throws "find throws" (fun () -> PersistentVector.find (fun x -> x > 9) v |> ignore) + } + + test "tryFind returns Some for matching element" { + let v = PersistentVector.ofSeq [ 1; 2; 3 ] + Expect.equal "tryFind" (Some 2) (PersistentVector.tryFind (fun x -> x = 2) v) + } + + test "tryFind returns None when not found" { + let v = PersistentVector.ofSeq [ 1; 2; 3 ] + Expect.equal "tryFind none" None (PersistentVector.tryFind (fun x -> x = 9) v) + } + + test "findIndex returns index of first matching element" { + let v = PersistentVector.ofSeq [ 10; 20; 30; 20 ] + Expect.equal "findIndex" 1 (PersistentVector.findIndex (fun x -> x = 20) v) + } + + test "tryFindIndex returns Some index when found" { + let v = PersistentVector.ofSeq [ 10; 20; 30 ] + Expect.equal "tryFindIndex" (Some 2) (PersistentVector.tryFindIndex (fun x -> x = 30) v) + } + + test "tryFindIndex returns None when not found" { + let v = PersistentVector.ofSeq [ 1; 2; 3 ] + Expect.equal "tryFindIndex none" None (PersistentVector.tryFindIndex (fun x -> x = 99) v) + } + + test "choose returns mapped Some values" { + let v = PersistentVector.ofSeq [ 1; 2; 3; 4; 5 ] + + let result = + PersistentVector.choose (fun x -> if x % 2 = 0 then Some(x * 10) else None) v + + Expect.equal "choose" [ 20; 40 ] (result |> Seq.toList) + } + + test "head returns first element" { + let v = PersistentVector.ofSeq [ 7; 8; 9 ] + Expect.equal "head" 7 (PersistentVector.head v) + } + + test "head throws on empty" { Expect.throws "head empty" (fun () -> PersistentVector.head PersistentVector.empty |> ignore) } + + test "tryHead returns Some for non-empty" { + let v = PersistentVector.ofSeq [ 42; 43 ] + Expect.equal "tryHead" (Some 42) (PersistentVector.tryHead v) + } + + test "tryHead returns None for empty" { Expect.equal "tryHead empty" None (PersistentVector.tryHead PersistentVector.empty) } + + test "toArray returns all elements" { + let v = PersistentVector.ofSeq [ 1; 2; 3 ] + Expect.equal "toArray" [| 1; 2; 3 |] (PersistentVector.toArray v) + } + + test "toArray on empty returns empty array" { Expect.equal "toArray empty" [||] (PersistentVector.toArray PersistentVector.empty) } + + test "toList returns all elements as list" { + let v = PersistentVector.ofSeq [ 1; 2; 3 ] + Expect.equal "toList" [ 1; 2; 3 ] (PersistentVector.toList v) + } + + test "ofList creates vector from list" { + let v = PersistentVector.ofList [ 5; 6; 7 ] + Expect.equal "ofList length" 3 (PersistentVector.length v) + Expect.equal "ofList nth 0" 5 (PersistentVector.nth 0 v) + Expect.equal "ofList nth 2" 7 (PersistentVector.nth 2 v) + } + + test "ofList empty creates empty vector" { + let v = PersistentVector.ofList [] + Expect.equal "ofList empty" 0 (PersistentVector.length v) + } + test "vector should allow init" { let vector = PersistentVector.init 5 (fun x -> x * 2) let s = Seq.init 5 (fun x -> x * 2) From 0fa2b33627c3a60a8c1923cc640d051b0fc08b5a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 14 Mar 2026 01:07:03 +0000 Subject: [PATCH 2/2] ci: trigger checks