From 3832736b5142b41476ca6d8a672177abdcd180f7 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 1 Nov 2025 23:26:37 +0100 Subject: [PATCH] Add `Array::functional_ops()` --- .../src/special_cases/special_cases.rs | 9 +- godot-core/src/builtin/collections/array.rs | 69 +++--- .../collections/array_functional_ops.rs | 213 ++++++++++++++++++ godot-core/src/builtin/collections/mod.rs | 2 + godot-core/src/builtin/mod.rs | 1 + godot-core/src/builtin/string/mod.rs | 14 +- .../src/builtin/string/string_macros.rs | 2 +- godot-ffi/src/toolbox.rs | 14 ++ .../builtin_tests/containers/array_test.rs | 95 +++++++- 9 files changed, 361 insertions(+), 58 deletions(-) create mode 100644 godot-core/src/builtin/collections/array_functional_ops.rs diff --git a/godot-codegen/src/special_cases/special_cases.rs b/godot-codegen/src/special_cases/special_cases.rs index 1d515177d..af9358e72 100644 --- a/godot-codegen/src/special_cases/special_cases.rs +++ b/godot-codegen/src/special_cases/special_cases.rs @@ -801,6 +801,7 @@ pub fn is_builtin_method_deleted(_class_name: &TyName, method: &JsonBuiltinMetho /// Returns some generic type – such as `GenericArray` representing `Array` – if method is marked as generic, `None` otherwise. /// /// Usually required to initialize the return value and cache its type (see also https://github.com/godot-rust/gdext/pull/1357). +#[rustfmt::skip] pub fn builtin_method_generic_ret( class_name: &TyName, method: &JsonBuiltinMethod, @@ -809,9 +810,11 @@ pub fn builtin_method_generic_ret( class_name.rust_ty.to_string().as_str(), method.name.as_str(), ) { - ("Array", "duplicate") | ("Array", "slice") => { - Some(FnReturn::with_generic_builtin(RustTy::GenericArray)) - } + | ("Array", "duplicate") + | ("Array", "slice") + | ("Array", "filter") + + => Some(FnReturn::with_generic_builtin(RustTy::GenericArray)), _ => None, } } diff --git a/godot-core/src/builtin/collections/array.rs b/godot-core/src/builtin/collections/array.rs index ba4df6481..561b93534 100644 --- a/godot-core/src/builtin/collections/array.rs +++ b/godot-core/src/builtin/collections/array.rs @@ -12,6 +12,7 @@ use std::{cmp, fmt}; use godot_ffi as sys; use sys::{ffi_methods, interface_fn, GodotFfi}; +use crate::builtin::iter::ArrayFunctionalOps; use crate::builtin::*; use crate::meta; use crate::meta::error::{ConvertError, FromGodotError, FromVariantError}; @@ -178,7 +179,7 @@ pub struct Array { } /// Guard that can only call immutable methods on the array. -struct ImmutableInnerArray<'a> { +pub(super) struct ImmutableInnerArray<'a> { inner: inner::InnerArray<'a>, } @@ -608,6 +609,13 @@ impl Array { } } + /// Access to Godot's functional-programming APIs based on callables. + /// + /// Exposes Godot array methods such as `filter()`, `map()`, `reduce()` and many more. See return type docs. + pub fn functional_ops(&self) -> ArrayFunctionalOps<'_, T> { + ArrayFunctionalOps::new(self) + } + /// Returns the minimum value contained in the array if all elements are of comparable types. /// /// If the elements can't be compared or the array is empty, `None` is returned. @@ -672,6 +680,8 @@ impl Array { /// /// Calling `bsearch` on an unsorted array results in unspecified behavior. Consider using `sort()` to ensure the sorting /// order is compatible with your callable's ordering. + /// + /// See also: [`bsearch_by()`][Self::bsearch_by], [`functional_ops().bsearch_custom()`][ArrayFunctionalOps::bsearch_custom]. pub fn bsearch(&self, value: impl AsArg) -> usize { meta::arg_into_ref!(value: T); @@ -682,13 +692,15 @@ impl Array { /// /// The comparator function should return an ordering that indicates whether its argument is `Less`, `Equal` or `Greater` the desired value. /// For example, for an ascending-ordered array, a simple predicate searching for a constant value would be `|elem| elem.cmp(&4)`. - /// See also [`slice::binary_search_by()`]. + /// This follows the design of [`slice::binary_search_by()`]. /// /// If the value is found, returns `Ok(index)` with its index. Otherwise, returns `Err(index)`, where `index` is the insertion index /// that would maintain sorting order. /// - /// Calling `bsearch_by` on an unsorted array results in unspecified behavior. Consider using [`sort_by()`] to ensure - /// the sorting order is compatible with your callable's ordering. + /// Calling `bsearch_by` on an unsorted array results in unspecified behavior. Consider using [`sort_unstable_by()`][Self::sort_unstable_by] + /// to ensure the sorting order is compatible with your callable's ordering. + /// + /// See also: [`bsearch()`][Self::bsearch], [`functional_ops().bsearch_custom()`][ArrayFunctionalOps::bsearch_custom]. pub fn bsearch_by(&self, mut func: F) -> Result where F: FnMut(&T) -> cmp::Ordering + 'static, @@ -712,7 +724,7 @@ impl Array { let debug_name = std::any::type_name::(); let index = Callable::with_scoped_fn(debug_name, godot_comparator, |pred| { - self.bsearch_custom(ignored_value, pred) + self.functional_ops().bsearch_custom(ignored_value, pred) }); if let Some(value_at_index) = self.get(index) { @@ -724,22 +736,9 @@ impl Array { Err(index) } - /// Finds the index of a value in a sorted array using binary search, with `Callable` custom predicate. - /// - /// The callable `pred` takes two elements `(a, b)` and should return if `a < b` (strictly less). - /// For a type-safe version, check out [`bsearch_by()`][Self::bsearch_by]. - /// - /// If the value is not present in the array, returns the insertion index that would maintain sorting order. - /// - /// Calling `bsearch_custom` on an unsorted array results in unspecified behavior. Consider using `sort_custom()` to ensure - /// the sorting order is compatible with your callable's ordering. + #[deprecated = "Moved to `functional_ops().bsearch_custom()`"] pub fn bsearch_custom(&self, value: impl AsArg, pred: &Callable) -> usize { - meta::arg_into_ref!(value: T); - - to_usize( - self.as_inner() - .bsearch_custom(&value.to_variant(), pred, true), - ) + self.functional_ops().bsearch_custom(value, pred) } /// Reverses the order of the elements in the array. @@ -753,9 +752,11 @@ impl Array { /// Sorts the array. /// /// The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). - /// This means that values considered equal may have their order changed when using `sort_unstable`. For most variant types, + /// This means that values considered equal may have their order changed when using `sort_unstable()`. For most variant types, /// this distinction should not matter though. /// + /// See also: [`sort_unstable_by()`][Self::sort_unstable_by], [`sort_unstable_custom()`][Self::sort_unstable_custom]. + /// /// _Godot equivalent: `Array.sort()`_ #[doc(alias = "sort")] pub fn sort_unstable(&mut self) { @@ -771,8 +772,10 @@ impl Array { /// elements themselves would be achieved with `|a, b| a.cmp(b)`. /// /// The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). - /// This means that values considered equal may have their order changed when using `sort_unstable_by`. For most variant types, + /// This means that values considered equal may have their order changed when using `sort_unstable_by()`. For most variant types, /// this distinction should not matter though. + /// + /// See also: [`sort_unstable()`][Self::sort_unstable], [`sort_unstable_custom()`][Self::sort_unstable_custom]. pub fn sort_unstable_by(&mut self, mut func: F) where F: FnMut(&T, &T) -> cmp::Ordering, @@ -795,14 +798,14 @@ impl Array { /// Sorts the array, using type-unsafe `Callable` comparator. /// - /// For a type-safe variant of this method, use [`sort_unstable_by()`][Self::sort_unstable_by]. - /// /// The callable expects two parameters `(lhs, rhs)` and should return a bool `lhs < rhs`. /// /// The sorting algorithm used is not [stable](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability). - /// This means that values considered equal may have their order changed when using `sort_unstable_custom`.For most variant types, + /// This means that values considered equal may have their order changed when using `sort_unstable_custom()`. For most variant types, /// this distinction should not matter though. /// + /// Type-safe alternatives: [`sort_unstable()`][Self::sort_unstable] , [`sort_unstable_by()`][Self::sort_unstable_by]. + /// /// _Godot equivalent: `Array.sort_custom()`_ #[doc(alias = "sort_custom")] pub fn sort_unstable_custom(&mut self, func: &Callable) { @@ -940,7 +943,7 @@ impl Array { inner::InnerArray::from_outer_typed(self) } - fn as_inner(&self) -> ImmutableInnerArray<'_> { + pub(super) fn as_inner(&self) -> ImmutableInnerArray<'_> { ImmutableInnerArray { // SAFETY: We can only read from the array. inner: unsafe { self.as_inner_mut() }, @@ -1634,21 +1637,25 @@ macro_rules! varray { /// This macro creates a [slice](https://doc.rust-lang.org/std/primitive.slice.html) of `Variant` values. /// /// # Examples -/// Variable number of arguments: +/// ## Variable number of arguments /// ```no_run /// # use godot::prelude::*; /// let slice: &[Variant] = vslice![42, "hello", true]; -/// /// let concat: GString = godot::global::str(slice); /// ``` -/// _(In practice, you might want to use [`godot_str!`][crate::global::godot_str] instead of `str()`.)_ +/// _In practice, you might want to use [`godot_str!`][crate::global::godot_str] instead of `str()`._ /// -/// Dynamic function call via reflection. NIL can still be passed inside `vslice!`, just use `Variant::nil()`. +/// ## Dynamic function call via reflection +/// NIL can still be passed inside `vslice!`, just use `Variant::nil()`. /// ```no_run /// # use godot::prelude::*; /// # fn some_object() -> Gd { unimplemented!() } /// let mut obj: Gd = some_object(); -/// obj.call("some_method", vslice![Vector2i::new(1, 2), Variant::nil()]); +/// +/// obj.call("some_method", vslice![ +/// Vector2i::new(1, 2), +/// Variant::nil(), +/// ]); /// ``` /// /// # See also diff --git a/godot-core/src/builtin/collections/array_functional_ops.rs b/godot-core/src/builtin/collections/array_functional_ops.rs new file mode 100644 index 000000000..12d062e2c --- /dev/null +++ b/godot-core/src/builtin/collections/array_functional_ops.rs @@ -0,0 +1,213 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use crate::builtin::{to_usize, Array, Callable, Variant, VariantArray}; +use crate::meta::{ArrayElement, AsArg}; +use crate::{meta, sys}; + +/// Immutable, functional-programming operations for `Array`, based on Godot callables. +/// +/// Returned by [`Array::functional_ops()`]. +/// +/// These methods exist to provide parity with Godot, e.g. when porting GDScript code to Rust. However, they come with several disadvantages +/// compared to Rust's [iterator adapters](https://doc.rust-lang.org/stable/core/iter/index.html#adapters): +/// - Not type-safe: callables are dynamically typed, so you need to double-check signatures. Godot may misinterpret returned values +/// (e.g. predicates apply to any "truthy" values, not just booleans). +/// - Slower: dispatching through callables is typically more costly than iterating over variants, especially since every call involves multiple +/// variant conversions, too. Combining multiple operations like `filter().map()` is very expensive due to intermediate allocations. +/// - Less composable/flexible: Godot's `map()` always returns an untyped array, even if the input is typed and unchanged by the mapping. +/// Rust's `collect()` on the other hand gives you control over the output type. Chaining iterators can apply multiple transformations lazily. +/// +/// In many cases, it is thus better to use [`Array::iter_shared()`] combined with iterator adapters. Check the individual method docs of +/// this struct for concrete alternatives. +pub struct ArrayFunctionalOps<'a, T: ArrayElement> { + array: &'a Array, +} + +impl<'a, T: ArrayElement> ArrayFunctionalOps<'a, T> { + pub(super) fn new(owner: &'a Array) -> Self { + Self { array: owner } + } + + /// Returns a new array containing only the elements for which the callable returns a truthy value. + /// + /// **Rust alternatives:** [`Iterator::filter()`]. + /// + /// The callable has signature `fn(T) -> bool`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4, 5]; + /// let even = array.functional_ops().filter(&Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// })); + /// assert_eq!(even, array![2, 4]); + /// ``` + #[must_use] + pub fn filter(&self, callable: &Callable) -> Array { + // SAFETY: filter() returns array of same type as self. + unsafe { self.array.as_inner().filter(callable) } + } + + /// Returns a new untyped array with each element transformed by the callable. + /// + /// **Rust alternatives:** [`Iterator::map()`]. + /// + /// The callable has signature `fn(T) -> Variant`. Since the transformation can change the element type, this method returns + /// a `VariantArray` (untyped array). + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1.1, 1.5, 1.9]; + /// let rounded = array.functional_ops().map(&Callable::from_fn("round", |args| { + /// args[0].to::().round() as i64 + /// })); + /// assert_eq!(rounded, varray![1, 2, 2]); + /// ``` + #[must_use] + pub fn map(&self, callable: &Callable) -> VariantArray { + // SAFETY: map() returns an untyped array. + unsafe { self.array.as_inner().map(callable) } + } + + /// Reduces the array to a single value by iteratively applying the callable. + /// + /// **Rust alternatives:** [`Iterator::fold()`] or [`Iterator::reduce()`]. + /// + /// The callable takes two arguments: the accumulator and the current element. + /// It returns the new accumulator value. The process starts with `initial` as the accumulator. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4]; + /// let sum = array.functional_ops().reduce( + /// &Callable::from_fn("sum", |args| { + /// args[0].to::() + args[1].to::() + /// }), + /// &0.to_variant() + /// ); + /// assert_eq!(sum, 10.to_variant()); + /// ``` + #[must_use] + pub fn reduce(&self, callable: &Callable, initial: &Variant) -> Variant { + self.array.as_inner().reduce(callable, initial) + } + + /// Returns `true` if the callable returns a truthy value for at least one element. + /// + /// **Rust alternatives:** [`Iterator::any()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4]; + /// let any_even = array.functional_ops().any(&Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// })); + /// assert!(any_even); + /// ``` + pub fn any(&self, callable: &Callable) -> bool { + self.array.as_inner().any(callable) + } + + /// Returns `true` if the callable returns a truthy value for all elements. + /// + /// **Rust alternatives:** [`Iterator::all()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![2, 4, 6]; + /// let all_even = array.functional_ops().all(&Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// })); + /// assert!(all_even); + /// ``` + pub fn all(&self, callable: &Callable) -> bool { + self.array.as_inner().all(callable) + } + + /// Finds the index of the first element matching a custom predicate. + /// + /// **Rust alternatives:** [`Iterator::position()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// Returns the index of the first element for which the callable returns a truthy value, starting from `from`. + /// If no element matches, returns `None`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4, 5]; + /// let is_even = Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// }); + /// assert_eq!(array.functional_ops().find_custom(&is_even, None), Some(1)); // value 2 + /// assert_eq!(array.functional_ops().find_custom(&is_even, Some(2)), Some(3)); // value 4 + /// ``` + #[cfg(since_api = "4.4")] + pub fn find_custom(&self, callable: &Callable, from: Option) -> Option { + let from = from.map(|i| i as i64).unwrap_or(0); + let found_index = self.array.as_inner().find_custom(callable, from); + + sys::found_to_option(found_index) + } + + /// Finds the index of the last element matching a custom predicate, searching backwards. + /// + /// **Rust alternatives:** [`Iterator::rposition()`]. + /// + /// The callable has signature `fn(element) -> bool`. + /// + /// Returns the index of the last element for which the callable returns a truthy value, searching backwards from `from`. + /// If no element matches, returns `None`. + /// + /// # Example + /// ```no_run + /// # use godot::prelude::*; + /// let array = array![1, 2, 3, 4, 5]; + /// let is_even = Callable::from_fn("is_even", |args| { + /// args[0].to::() % 2 == 0 + /// }); + /// assert_eq!(array.functional_ops().rfind_custom(&is_even, None), Some(3)); // value 4 + /// assert_eq!(array.functional_ops().rfind_custom(&is_even, Some(2)), Some(1)); // value 2 + /// ``` + #[cfg(since_api = "4.4")] + pub fn rfind_custom(&self, callable: &Callable, from: Option) -> Option { + let from = from.map(|i| i as i64).unwrap_or(-1); + let found_index = self.array.as_inner().rfind_custom(callable, from); + + sys::found_to_option(found_index) + } + + /// Finds the index of a value in a sorted array using binary search, with `Callable` custom predicate. + /// + /// The callable `pred` takes two elements `(a, b)` and should return if `a < b` (strictly less). + /// For a type-safe version, check out [`Array::bsearch_by()`]. + /// + /// If the value is not present in the array, returns the insertion index that would maintain sorting order. + /// + /// Calling `bsearch_custom()` on an unsorted array results in unspecified behavior. Consider using [`Array::sort_unstable_custom()`] + /// to ensure the sorting order is compatible with your callable's ordering. + pub fn bsearch_custom(&self, value: impl AsArg, pred: &Callable) -> usize { + meta::arg_into_ref!(value: T); + + to_usize( + self.array + .as_inner() + .bsearch_custom(&value.to_variant(), pred, true), + ) + } +} diff --git a/godot-core/src/builtin/collections/mod.rs b/godot-core/src/builtin/collections/mod.rs index d28046dc9..5c6fade32 100644 --- a/godot-core/src/builtin/collections/mod.rs +++ b/godot-core/src/builtin/collections/mod.rs @@ -6,6 +6,7 @@ */ mod array; +mod array_functional_ops; mod dictionary; mod extend_buffer; mod packed_array; @@ -21,6 +22,7 @@ pub(crate) mod containers { // Re-export in godot::builtin::iter. #[rustfmt::skip] // Individual lines. pub(crate) mod iterators { + pub use super::array_functional_ops::ArrayFunctionalOps; pub use super::array::Iter as ArrayIter; pub use super::dictionary::Iter as DictIter; pub use super::dictionary::Keys as DictKeys; diff --git a/godot-core/src/builtin/mod.rs b/godot-core/src/builtin/mod.rs index 61227c6d2..0a97797af 100644 --- a/godot-core/src/builtin/mod.rs +++ b/godot-core/src/builtin/mod.rs @@ -65,6 +65,7 @@ pub use __prelude_reexport::*; pub mod math; /// Iterator types for arrays and dictionaries. +// Might rename this to `collections` or so. pub mod iter { pub use super::collections::iterators::*; } diff --git a/godot-core/src/builtin/string/mod.rs b/godot-core/src/builtin/string/mod.rs index aaab61279..5c10f5141 100644 --- a/godot-core/src/builtin/string/mod.rs +++ b/godot-core/src/builtin/string/mod.rs @@ -66,6 +66,7 @@ pub enum Encoding { } // ---------------------------------------------------------------------------------------------------------------------------------------------- +// Utilities fn populated_or_none(s: GString) -> Option { if s.is_empty() { @@ -75,19 +76,6 @@ fn populated_or_none(s: GString) -> Option { } } -fn found_to_option(index: i64) -> Option { - if index == -1 { - None - } else { - // If this fails, then likely because we overlooked a negative value. - let index_usize = index - .try_into() - .unwrap_or_else(|_| panic!("unexpected index {index} returned from Godot function")); - - Some(index_usize) - } -} - // ---------------------------------------------------------------------------------------------------------------------------------------------- // Padding, alignment and precision support diff --git a/godot-core/src/builtin/string/string_macros.rs b/godot-core/src/builtin/string/string_macros.rs index 73035e004..f5fb5f403 100644 --- a/godot-core/src/builtin/string/string_macros.rs +++ b/godot-core/src/builtin/string/string_macros.rs @@ -399,7 +399,7 @@ macro_rules! impl_shared_string_api { } }; - super::found_to_option(godot_found) + sys::found_to_option(godot_found) } } diff --git a/godot-ffi/src/toolbox.rs b/godot-ffi/src/toolbox.rs index 0b8c11f2a..b6c6f613e 100644 --- a/godot-ffi/src/toolbox.rs +++ b/godot-ffi/src/toolbox.rs @@ -195,6 +195,20 @@ pub fn i64_to_ordering(value: i64) -> std::cmp::Ordering { } } +/// Converts a Godot "found" index `Option`, where -1 is mapped to `None`. +pub fn found_to_option(index: i64) -> Option { + if index == -1 { + None + } else { + // If this fails, then likely because we overlooked a negative value. + let index_usize = index + .try_into() + .unwrap_or_else(|_| panic!("unexpected index {index} returned from Godot function")); + + Some(index_usize) + } +} + /* pub fn unqualified_type_name() -> &'static str { let type_name = std::any::type_name::(); diff --git a/itest/rust/src/builtin_tests/containers/array_test.rs b/itest/rust/src/builtin_tests/containers/array_test.rs index 7e8e76fbe..94c55bc8e 100644 --- a/itest/rust/src/builtin_tests/containers/array_test.rs +++ b/itest/rust/src/builtin_tests/containers/array_test.rs @@ -554,18 +554,11 @@ fn array_bsearch_by() { } #[itest] -fn array_bsearch_custom() { +fn array_fops_bsearch_custom() { let a = array![5, 4, 2, 1]; let func = backwards_sort_callable(); - assert_eq!(a.bsearch_custom(1, &func), 3); - assert_eq!(a.bsearch_custom(3, &func), 2); -} - -fn backwards_sort_callable() -> Callable { - // No &[&Variant] explicit type in arguments. - Callable::from_fn("sort backwards", |args| { - args[0].to::() > args[1].to::() - }) + assert_eq!(a.functional_ops().bsearch_custom(1, &func), 3); + assert_eq!(a.functional_ops().bsearch_custom(3, &func), 2); } #[itest] @@ -696,6 +689,88 @@ fn array_inner_type() { assert_eq!(subarray.element_type(), primary.element_type()); } +#[itest] +fn array_fops_filter() { + let is_even = is_even_callable(); + + let array = array![1, 2, 3, 4, 5, 6]; + assert_eq!(array.functional_ops().filter(&is_even), array![2, 4, 6]); +} + +#[itest] +fn array_fops_map() { + let f = Callable::from_fn("round", |args| args[0].to::().round() as i64); + + let array = array![0.7, 1.0, 1.3, 1.6]; + let result = array.functional_ops().map(&f); + + assert_eq!(result, varray![1, 1, 1, 2]); +} + +#[itest] +fn array_fops_reduce() { + let f = Callable::from_fn("sum", |args| args[0].to::() + args[1].to::()); + + let array = array![1, 2, 3, 4]; + let result = array.functional_ops().reduce(&f, &0.to_variant()); + + assert_eq!(result.to::(), 10); +} + +#[itest] +fn array_fops_any() { + let is_even = is_even_callable(); + + assert!(array![1, 2, 3].functional_ops().any(&is_even)); + assert!(!array![1, 3, 5].functional_ops().any(&is_even)); +} + +#[itest] +fn array_fops_all() { + let is_even = is_even_callable(); + + assert!(!array![1, 2, 3].functional_ops().all(&is_even)); + assert!(array![2, 4, 6].functional_ops().all(&is_even)); +} + +#[itest] +#[cfg(since_api = "4.4")] +fn array_fops_find_custom() { + let is_even = is_even_callable(); + + let array = array![1, 2, 3, 4, 5]; + assert_eq!(array.functional_ops().find_custom(&is_even, None), Some(1)); + + let array = array![1, 3, 5]; + assert_eq!(array.functional_ops().find_custom(&is_even, None), None); +} + +#[itest] +#[cfg(since_api = "4.4")] +fn array_fops_rfind_custom() { + let is_even = is_even_callable(); + + let array = array![1, 2, 3, 4, 5]; + assert_eq!(array.functional_ops().rfind_custom(&is_even, None), Some(3)); + + let array = array![1, 3, 5]; + assert_eq!(array.functional_ops().rfind_custom(&is_even, None), None); +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Helper functions for creating callables. + +fn backwards_sort_callable() -> Callable { + // No &[&Variant] explicit type in arguments. + Callable::from_fn("sort backwards", |args| { + args[0].to::() > args[1].to::() + }) +} + +fn is_even_callable() -> Callable { + Callable::from_fn("is even", |args| args[0].to::() % 2 == 0) +} + // ---------------------------------------------------------------------------------------------------------------------------------------------- // Class definitions