try_specialize

Struct Specialization

source
pub struct Specialization<T1, T2>(/* private fields */)
where
    T1: ?Sized,
    T2: ?Sized;
Expand description

A zero-sized marker struct that guarantees type equality between T1 and T2.

This struct provides a type-safe mechanism to specialize one type into another, enforcing that T1 and T2 are identical concrete types. It can only be instantiated when both types are the same.

Specialization trait refers to a lower-level API. Prefer to use TrySpecialize trait methods if applicable.

Library tests ensure that the specializations are fully optimized and become zero-cost with opt-level >= 1. Note that release profile uses opt-level = 3 by default.

Constructors cheat sheet:

Type boundsConstructor
T1: 'static + LifetimeFreetry_new
T2: 'static + LifetimeFreetry_new + rev
T1: 'static, T2: 'statictry_new_static
underlying type maybe impls LifetimeFreetry_new_if_lifetime_free_weak
unsafe, without lifetimes checktry_new_ignore_lifetimes

Specialization methods cheat sheet:

FromToMethod
T1T2specialize
&T1&T2specialize_ref
&mut T1&mut T2specialize_mut
T2T1rev + specialize
Wrapper<T1>Wrapper<T2>map + specialize
T1::AssociatedTypeT2::AssociatedTypemap + specialize

Implementations§

source§

impl<T1, T2> Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

source

pub fn try_new() -> Option<Self>
where T2: LifetimeFree,

Checks the types T1 and T2 for equality and returns the specialization provider if types are equal.

Note that this method requires source type to implement LifetimeFree. Use Specialization::try_new().rev() method to check the target type instead. LifetimeFree is not automatically derived and implemented only for a set of types without lifetimes.

For simple cases consider using TrySpecialize methods like try_specialize, try_specialize_ref, and try_specialize_mut instead.

You can use Specialization::try_new_static if both types are 'static.

§Examples

Same-type stringifiable type concatenation, that don’t allocate memory if one of the arguments is empty &str:

use core::fmt::Display;
use std::borrow::Cow;
use std::format;

use try_specialize::Specialization;

fn concat_same<'a, T>(first: &'a T, second: &'a T) -> Cow<'a, str>
where
    T: ?Sized + Display,
{
    if let Some(spec) = Specialization::<T, str>::try_new() {
        match (spec.specialize_ref(first), spec.specialize_ref(second)) {
            (first, "") => Cow::Borrowed(first),
            ("", second) => Cow::Borrowed(second),
            (first, second) => Cow::Owned([first, second].concat()),
        }
    } else {
        Cow::Owned(format!("{first}{second}"))
    }
}

assert!(matches!(concat_same("foo", "bar"), Cow::Owned(v) if v == "foobar"));
assert!(matches!(concat_same("foo", ""), Cow::Borrowed("foo")));
assert!(matches!(concat_same("", "bar"), Cow::Borrowed("bar")));
let foo = String::from("foo");
let bar = String::from("bar");
assert!(matches!(concat_same(&foo, &bar), Cow::Owned(v) if v == "foobar"));
assert!(matches!(concat_same(&123, &456), Cow::Owned(v) if v == "123456"));

Generate a placeholder with non-default value for some types:

use try_specialize::Specialization;

fn placeholder<T>() -> T
where
    T: Default,
{
    if let Some(spec) = Specialization::<T, u8>::try_new() {
        spec.rev().specialize(12)
    } else if let Some(spec) = Specialization::<T, u32>::try_new() {
        spec.rev().specialize(42)
    } else if let Some(spec) = Specialization::<T, f64>::try_new() {
        spec.rev().specialize(123.456)
    // ...
    } else {
        T::default()
    }
}

assert_eq!(placeholder::<&'static str>(), "");
assert_eq!(placeholder::<u8>(), 12);
assert_eq!(placeholder::<(u8, u8)>(), (0, 0));
assert_eq!(placeholder::<u32>(), 42);
assert_eq!(placeholder::<f64>(), 123.456);
assert_eq!(placeholder::<i128>(), 0);
source

pub fn try_new_static() -> Option<Self>
where T1: 'static, T2: 'static,

Checks the types T1 and T2 for equality and returns the specialization provider if types are equal.

Note that this method requires both types to have 'static lifetime, but don’t require any type to implement LifetimeFree.

For simple cases consider using TrySpecialize methods like try_specialize_static, try_specialize_ref_static, and try_specialize_mut_static instead.

You can use Specialization::try_new if the target type implements LifetimeFree trait or Specialization::try_new().rev() if the source type implements LifetimeFree trait.

§Examples

Collection reverse function with optimized specializations for Vec<T> and Box<[T]>:

use core::sync::atomic::{AtomicU32, Ordering as AtomicOrdering};
use std::collections::VecDeque;

use try_specialize::Specialization;

static DEBUG_SPEC_USED: AtomicU32 = AtomicU32::new(0);

fn reverse_collection<T>(value: T) -> T
where
    T: 'static + IntoIterator + FromIterator<T::Item>,
    T::IntoIter: DoubleEndedIterator,
{
    let spec1 = Specialization::<T, Vec<T::Item>>::try_new_static();
    let spec2 = Specialization::<T, Box<[T::Item]>>::try_new_static();

    if let Some(spec1) = spec1 {
        DEBUG_SPEC_USED.store(101, AtomicOrdering::Relaxed);
        let mut vec = spec1.specialize(value);
        vec.reverse();
        spec1.rev().specialize(vec)
    } else if let Some(spec2) = spec2 {
        DEBUG_SPEC_USED.store(202, AtomicOrdering::Relaxed);
        let mut boxed = spec2.specialize(value);
        boxed.reverse();
        spec2.rev().specialize(boxed)
    } else {
        DEBUG_SPEC_USED.store(303, AtomicOrdering::Relaxed);
        value.into_iter().rev().collect()
    }
}

assert_eq!(reverse_collection(vec![1, 2, 3]), vec![3, 2, 1]);
assert_eq!(DEBUG_SPEC_USED.load(AtomicOrdering::Relaxed), 101);

assert_eq!(
    reverse_collection(vec![1, 2, 3].into_boxed_slice()),
    vec![3, 2, 1].into_boxed_slice()
);
assert_eq!(DEBUG_SPEC_USED.load(AtomicOrdering::Relaxed), 202);

assert_eq!(
    reverse_collection(VecDeque::from([1, 2, 3])),
    VecDeque::from([3, 2, 1])
);
assert_eq!(DEBUG_SPEC_USED.load(AtomicOrdering::Relaxed), 303);

Same-type stringifiable type concatenation, that don’t allocate memory if one of the arguments is empty &'static str and avoid reallocations if one of the arguments is empty String:

use core::fmt::Display;
use std::borrow::Cow;

use try_specialize::Specialization;

fn concat_same<T>(first: T, second: T) -> Cow<'static, str>
where
    T: 'static + Display,
{
    if let Some(spec) = Specialization::<T, &'static str>::try_new_static() {
        match (spec.specialize(first), spec.specialize(second)) {
            (first, "") => Cow::Borrowed(first),
            ("", second) => Cow::Borrowed(second),
            (first, second) => Cow::Owned([first, second].concat()),
        }
    } else if let Some(spec) = Specialization::<T, String>::try_new_static() {
        let first = spec.specialize(first);
        let second = spec.specialize(second);
        match (first.is_empty(), second.is_empty()) {
            (false | true, true) => Cow::Owned(first),
            (true, false) => Cow::Owned(second),
            (false, false) => Cow::Owned(first + &second),
        }
    } else {
        Cow::Owned(format!("{first}{second}"))
    }
}

assert!(matches!(concat_same("foo", "bar"), Cow::Owned(v) if v == "foobar"));
assert!(matches!(concat_same("foo", ""), Cow::Borrowed("foo")));
assert!(matches!(concat_same("", "bar"), Cow::Borrowed("bar")));
let foo = String::from("foo");
let bar = String::from("bar");
assert!(matches!(concat_same(foo, bar), Cow::Owned(v) if v == "foobar"));
let empty = String::new();
let bar = String::from("bar");
assert!(matches!(concat_same(empty, bar), Cow::Owned(v) if v == "bar"));
let foo = String::from("foo");
let empty = String::new();
assert!(matches!(concat_same(foo, empty), Cow::Owned(v) if v == "foo"));
assert!(matches!(concat_same(123, 456), Cow::Owned(v) if v == "123456"));

Generate a placeholder with non-default value for some types:

use try_specialize::Specialization;

fn placeholder<T>() -> T
where
    T: 'static + Default,
{
    if let Some(spec) = Specialization::<T, &'static str>::try_new_static() {
        spec.rev().specialize("dummy string")
    } else if let Some(spec) = Specialization::<T, u32>::try_new() {
        spec.rev().specialize(42)
    } else if let Some(spec) = Specialization::<T, f64>::try_new() {
        spec.rev().specialize(123.456)
    // ...
    } else {
        T::default()
    }
}

assert_eq!(placeholder::<&'static str>(), "dummy string");
assert_eq!(placeholder::<(u8, u8)>(), (0, 0));
assert_eq!(placeholder::<u32>(), 42);
assert_eq!(placeholder::<f64>(), 123.456);
assert_eq!(placeholder::<i128>(), 0);
source

pub unsafe fn try_new_ignore_lifetimes() -> Option<Self>

Checks the types T1 and T2 for equality and returns the specialization provider if the types are equal ignoring lifetimes.

For simple cases consider using TrySpecialize methods like try_specialize_ignore_lifetimes, try_specialize_ref_ignore_lifetimes, and try_specialize_mut_ignore_lifetimes instead.

§Safety

This method doesn’t validate type lifetimes. Lifetimes equality should be validated separately.

§Examples

Specialized to third-party library item that can definitely be LifetimeFree.

mod third_party_lib {
    #[derive(Eq, PartialEq, Default, Debug)]
    pub struct Marker(pub u32);
}

use third_party_lib::Marker;
use try_specialize::Specialization;

fn inc_if_marker<T>(value: T) -> T {
    // SAFETY: `Marker` type has no lifetime parameters.
    if let Some(spec) = unsafe { Specialization::<T, Marker>::try_new_ignore_lifetimes() } {
        spec.rev().specialize(Marker(spec.specialize(value).0 + 1))
    } else {
        value
    }
}

assert_eq!(inc_if_marker(123), 123);
assert_eq!(inc_if_marker("str"), "str");
assert_eq!(inc_if_marker(Marker(123)), Marker(124));
source

pub const unsafe fn new_unchecked() -> Self

Construct a new Specialization<T1, T2> without any types equality checks.

§Safety

Calling this method for Specialization<T1, T2> with different types in T1 and T2 is undefined behavior.

source

pub const fn rev(&self) -> Specialization<T2, T1>

Reverses the specialization.

It can be used to convert the target type back to source type and also to create a specialization from a type which implements LifetimeFree, for example: Specialization::<u8, T>::new().rev().

For simple cases consider using TrySpecialize methods like try_specialize_from, try_specialize_from_ref, and try_specialize_from_mut instead.

§Examples

Synthetic example with custom behavior for u32 type:

use try_specialize::{LifetimeFree, Specialization};

fn inc_if_u32<T>(value: T) -> T
where
    T: LifetimeFree,
{
    if let Some(spec) = Specialization::<T, u32>::try_new() {
        spec.rev().specialize(spec.specialize(value) + 1)
    } else {
        value
    }
}

assert_eq!(inc_if_u32(123_u32), 124);
assert_eq!(inc_if_u32(123_i32), 123);
assert_eq!(inc_if_u32(123_u8), 123);

More realistic example can be found at examples/encode.rs.

source

pub const fn map<M>( &self, ) -> Specialization<<M as TypeFn<T1>>::Output, <M as TypeFn<T2>>::Output>
where M: TypeFn<T1> + TypeFn<T2>,

Maps the specialization types to other types using a specified TypeFn.

This can be useful to specialize third-party wrappers or the trait associated types if they are based on LifetimeFree types.

§Examples

Simplified custom vec-like type processing optimization example:

mod third_party_lib {
    pub struct CustomVec<T> {
        // ...
    }
}

use third_party_lib::CustomVec;
use try_specialize::{Specialization, TypeFn};

fn process<T>(data: CustomVec<T>) {
    struct MapIntoCustomVec;
    impl<T> TypeFn<T> for MapIntoCustomVec {
        type Output = CustomVec<T>;
    }

    if let Some(spec) = Specialization::<T, u8>::try_new() {
        let data: CustomVec<u8> = spec.map::<MapIntoCustomVec>().specialize(data);
        // optimized specialized implementation...
    } else {
        // default implementation...
    }
}

Simplified custom hashbrown::HashMap type processing optimization example with multiple type generics:

use hashbrown::HashMap;
use try_specialize::{Specialization, TypeFn};

fn process<K, V>(data: HashMap<K, V>)
where
    K: 'static,
    V: 'static,
{
    struct MapIntoHashMap;
    impl<K, V> TypeFn<(K, V)> for MapIntoHashMap {
        type Output = HashMap<K, V>;
    }

    if let Some(spec) = Specialization::<(K, V), (char, char)>::try_new_static() {
        let data: HashMap<char, char> = spec.map::<MapIntoHashMap>().specialize(data);
        // optimized specialized implementation...
    } else if let Some(spec) = Specialization::<(K, V), (String, String)>::try_new_static() {
        let data: HashMap<String, String> = spec.map::<MapIntoHashMap>().specialize(data);
        // optimized specialized implementation...
    } else {
        // default implementation...
    }
}

Custom data encoders and decoders with customizable per-type encoding and decoding errors and optimized byte array encoding and decoding. Full example code is available at examples/encode.rs.

// ...

impl<T> Encode for [T]
where
    T: Encode,
{
    type EncodeError = T::EncodeError;

    #[inline]
    fn encode_to<W>(&self, writer: &mut W) -> Result<(), Self::EncodeError>
    where
        W: ?Sized + Write,
    {
        if let Some(spec) = Specialization::<[T], [u8]>::try_new() {
            // Specialize self from `[T; N]` to `[u32; N]`
            let bytes: &[u8] = spec.specialize_ref(self);
            // Map type specialization to its associated error specialization.
            let spec_err = spec.rev().map::<MapToEncodeError>();
            writer
                .write_all(bytes)
                // Specialize error from `io::Error` to `Self::EncodeError`.
                .map_err(|err| spec_err.specialize(err))?;
        } else {
            for item in self {
                item.encode_to(writer)?;
            }
        }
        Ok(())
    }
}

// ...
source

pub fn specialize(&self, value: T1) -> T2
where T1: Sized, T2: Sized,

Infallibly specialize value with type T1 as T2.

This method can only be called for a Specialization<T1, T2> whose existing instance indicates that types T1 and T2 are equivalent.

For simple cases consider using TrySpecialize methods like try_specialize, try_specialize_from, and try_specialize_static instead.

source

pub const fn specialize_ref<'a>(&self, value: &'a T1) -> &'a T2

Infallibly specialize value with type &T1 as &T2.

This method can only be called for a Specialization<T1, T2> whose existing instance indicates that types T1 and T2 are equivalent.

For simple cases consider using TrySpecialize methods like try_specialize_ref, try_specialize_from_ref, and try_specialize_ref_static instead.

source

pub fn specialize_mut<'a>(&self, value: &'a mut T1) -> &'a mut T2

Infallibly specialize value with type &mut T1 as &mut T2.

This method can only be called for a Specialization<T1, T2> whose existing instance indicates that types T1 and T2 are equivalent.

For simple cases consider using TrySpecialize methods like try_specialize_mut, try_specialize_from_mut, and try_specialize_mut_static instead.

Trait Implementations§

source§

impl<T1, T2> Clone for Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

source§

fn clone(&self) -> Self

Returns a copy of the value. Read more
1.0.0 · source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
source§

impl<T1, T2> Debug for Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

source§

fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult

Formats the value using the given formatter. Read more
source§

impl<T1, T2> Hash for Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

source§

fn hash<H: Hasher>(&self, _: &mut H)

Feeds this value into the given Hasher. Read more
1.3.0 · source§

fn hash_slice<H>(data: &[Self], state: &mut H)
where H: Hasher, Self: Sized,

Feeds a slice of this type into the given Hasher. Read more
source§

impl<T1, T2> Ord for Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

source§

fn cmp(&self, _: &Self) -> Ordering

This method returns an Ordering between self and other. Read more
1.21.0 · source§

fn max(self, other: Self) -> Self
where Self: Sized,

Compares and returns the maximum of two values. Read more
1.21.0 · source§

fn min(self, other: Self) -> Self
where Self: Sized,

Compares and returns the minimum of two values. Read more
1.50.0 · source§

fn clamp(self, min: Self, max: Self) -> Self
where Self: Sized,

Restrict a value to a certain interval. Read more
source§

impl<T1, T2> PartialEq for Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

source§

fn eq(&self, _: &Self) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 · source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
source§

impl<T1, T2> PartialOrd for Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

source§

fn partial_cmp(&self, other: &Self) -> Option<Ordering>

This method returns an ordering between self and other values if one exists. Read more
1.0.0 · source§

fn lt(&self, other: &Rhs) -> bool

Tests less than (for self and other) and is used by the < operator. Read more
1.0.0 · source§

fn le(&self, other: &Rhs) -> bool

Tests less than or equal to (for self and other) and is used by the <= operator. Read more
1.0.0 · source§

fn gt(&self, other: &Rhs) -> bool

Tests greater than (for self and other) and is used by the > operator. Read more
1.0.0 · source§

fn ge(&self, other: &Rhs) -> bool

Tests greater than or equal to (for self and other) and is used by the >= operator. Read more
source§

impl<T1, T2> WeakSpecialization for Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

Available on crate features alloc and unreliable only.
source§

fn try_new_if_lifetime_free_weak() -> Option<Self>

Checks the types T1 and T2 for equality and returns the specialization provider if types implement LifetimeFree and the types are equal. Read more
source§

impl<T1, T2> Copy for Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

source§

impl<T1, T2> Eq for Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

Auto Trait Implementations§

§

impl<T1, T2> Freeze for Specialization<T1, T2>
where T1: ?Sized, T2: ?Sized,

§

impl<T1, T2> RefUnwindSafe for Specialization<T1, T2>
where T1: RefUnwindSafe + ?Sized, T2: RefUnwindSafe + ?Sized,

§

impl<T1, T2> Send for Specialization<T1, T2>
where T1: Send + ?Sized, T2: Send + ?Sized,

§

impl<T1, T2> Sync for Specialization<T1, T2>
where T1: Sync + ?Sized, T2: Sync + ?Sized,

§

impl<T1, T2> Unpin for Specialization<T1, T2>
where T1: Unpin + ?Sized, T2: Unpin + ?Sized,

§

impl<T1, T2> UnwindSafe for Specialization<T1, T2>
where T1: UnwindSafe + ?Sized, T2: UnwindSafe + ?Sized,

Blanket Implementations§

source§

impl<T> Any for T
where T: 'static + ?Sized,

source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
source§

impl<T> Borrow<T> for T
where T: ?Sized,

source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
source§

impl<T> CloneToUninit for T
where T: Clone,

source§

unsafe fn clone_to_uninit(&self, dst: *mut T)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dst. Read more
source§

impl<T> From<T> for T

source§

fn from(t: T) -> T

Returns the argument unchanged.

source§

impl<T, U> Into<U> for T
where U: From<T>,

source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

source§

impl<T> ToOwned for T
where T: Clone,

source§

type Owned = T

The resulting type after obtaining ownership.
source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

source§

type Error = Infallible

The type returned in the event of a conversion error.
source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
source§

impl<T> TrySpecialize for T
where T: ?Sized,

source§

fn try_specialize<T>(self) -> Result<T, Self>
where Self: Sized, T: LifetimeFree,

Attempts to specialize Self as T for types without lifetimes. Read more
source§

fn try_specialize_from<T>(other: T) -> Result<Self, T>
where Self: Sized, T: LifetimeFree,

Attempts to specialize T as Self for types without lifetimes. Read more
source§

fn try_specialize_static<T>(self) -> Result<T, Self>
where Self: 'static + Sized, T: 'static,

Attempts to specialize Self as T for static types. Read more
source§

fn try_specialize_ref<T>(&self) -> Option<&T>
where T: ?Sized + LifetimeFree,

Attempts to specialize &Self as &T for types without lifetimes. Read more
source§

fn try_specialize_from_ref<T>(other: &T) -> Option<&Self>
where T: ?Sized + LifetimeFree,

Attempts to specialize &T as &Self for types without lifetimes. Read more
source§

fn try_specialize_ref_static<T>(&self) -> Option<&T>
where Self: 'static, T: ?Sized + 'static,

Attempts to specialize &Self as &T for static types. Read more
source§

fn try_specialize_mut<T>(&mut self) -> Option<&mut T>
where T: ?Sized + LifetimeFree,

Attempts to specialize &mut Self as &mut T for types without lifetimes. Read more
source§

fn try_specialize_from_mut<T>(other: &mut T) -> Option<&mut Self>
where T: ?Sized + LifetimeFree,

Attempts to specialize &mut T as &mut Self for types without lifetimes. Read more
source§

fn try_specialize_mut_static<T>(&mut self) -> Option<&mut T>
where Self: 'static, T: ?Sized + 'static,

Attempts to specialize &mut Self as &mut T for static types. Read more
source§

unsafe fn try_specialize_ignore_lifetimes<T>(self) -> Result<T, Self>
where Self: Sized,

Attempts to specialize Self as T ignoring lifetimes. Read more
source§

unsafe fn try_specialize_ref_ignore_lifetimes<T>(&self) -> Option<&T>
where T: ?Sized,

Attempts to specialize &Self as &T ignoring lifetimes. Read more
source§

unsafe fn try_specialize_mut_ignore_lifetimes<T>(&mut self) -> Option<&mut T>
where T: ?Sized,

Attempts to specialize &mut Self as &mut T ignoring lifetimes. Read more
source§

impl<T> TrySpecializeWeak for T
where T: ?Sized,

source§

fn try_specialize_if_lifetime_free_weak<T>(self) -> Result<T, Self>
where Self: Sized,

Available on crate features alloc and unreliable only.
Attempts to specialize Self as T checking that underlying Self type implements LifetimeFree. Read more
source§

fn try_specialize_ref_if_lifetime_free_weak<T>(&self) -> Option<&T>
where T: ?Sized,

Available on crate features alloc and unreliable only.
Attempts to specialize &Self as &T checking that underlying Self type implements LifetimeFree. Read more
source§

fn try_specialize_mut_if_lifetime_free_weak<T>(&mut self) -> Option<&mut T>
where T: ?Sized,

Available on crate features alloc and unreliable only.
Attempts to specialize &mut Self as &mut T checking that underlying Self type implements LifetimeFree. Read more