Crate try_specialize

source
Expand description

The try-specialize crate provides limited, zero-cost specialization in generic context on stable Rust.

use try_specialize::TrySpecialize;

fn example_specialize_by_value<T>(value: T) -> Result<u32, T> {
    value.try_specialize()
}

fn example_specialize_by_ref<T: ?Sized>(value: &T) -> Option<&str> {
    value.try_specialize_ref()
}

§Introduction

While specialization in Rust can be a tempting solution in many use cases, I encourage you to reconsider using traits instead as a more idiomatic alternatives. The use of traits is the idiomatic way to achieve polymorphism in Rust, promoting better code clarity, reusability, and maintainability.

However the specialization in Rust can be suitable when you need to optimize performance by providing specialized optimized implementations for some types without altering the code logic. Also it can be useful in highly specific type-level programming related usecases like comparisons between types from different libraries.

For a simple use cases, I recommend checking out the castaway crate, which offers a much simpler API and is easier to work with. On nightly Rust, I recommend to use min_specialization feature instead. Note that Rust standard library can and already use min_specialization in stable for many optimizations. For a more detailed comparison, see the Alternative crates section below.

§About

This crate provides a comprehensive API for solving various specialization challenges to save you from using unsafe code or at least to take over some of the checks. This crate provides specialization from unconstrained types, to unconstrained types, between 'static types, between types references and mutable references, and more.

Library tests ensure that the specializations are performed at compile time, fully optimized and do not come with any run-time cost with opt-level >= 1. Note that release profile uses opt-level = 3 by default.

§Usage

Add this to your Cargo.toml:

[dependencies]
try-specialize = "0.1.0"

Then, in most cases, it will be enough to use TrySpecialize trait methods like TrySpecialize::try_specialize, TrySpecialize::try_specialize_ref and TrySpecialize::try_specialize_static. If you want to check the possibility of specialization in advance and then use it infallibly multiple times including using reversed or mapped specialization, check Specialization struct methods.

Note that even in expression position, unlike casting, subtyping, and coercion, specialization does not alter the underlying type or its data. It only qualifies the underlying types of generics. Specialization from a generic type T1 to another generic type T2 succeeds only when the underlying types of T1 and T2 are equal.

§Examples

Specialize type to any LifetimeFree type:

use try_specialize::TrySpecialize;

fn func<T>(value: T) {
    match value.try_specialize::<(u32, String)>() {
        Ok(value) => specialized_impl(value),
        Err(value) => default_impl(value),
    }
}

Specialize 'static type to any 'static type:

use try_specialize::TrySpecialize;

fn func<T>(value: T)
where
    T: 'static,
{
    match value.try_specialize_static::<(u32, &'static str)>() {
        Ok(value) => specialized_impl(value),
        Err(value) => default_impl(value),
    }
}

Specialize Sized or Unsized type reference to any LifetimeFree type reference:

use try_specialize::TrySpecialize;

fn func<T>(value: &T)
where
    T: ?Sized, // Relax the implicit `Sized` bound.
{
    match value.try_specialize_ref::<str>() {
        Some(value) => specialized_impl(value),
        None => default_impl(value),
    }
}

Specialize Sized or Unsized type mutable reference to any LifetimeFree type mutable reference:

use try_specialize::TrySpecialize;

fn func<T>(value: &mut T)
where
    T: ?Sized, // Relax the implicit `Sized` bound.
{
    match value.try_specialize_mut::<[u8]>() {
        Some(value) => specialized_impl(value),
        None => default_impl(value),
    }
}

Specialize a third-party library container with generic types:

use try_specialize::{Specialization, TypeFn};

fn func<K, V>(value: hashbrown::HashMap<K, V>) {
    struct MapIntoHashMap;
    impl<K, V> TypeFn<(K, V)> for MapIntoHashMap {
        type Output = hashbrown::HashMap<K, V>;
    }

    if let Some(spec) = Specialization::<(K, V), (u32, char)>::try_new() {
        let spec = spec.map::<MapIntoHashMap>();
        let value: hashbrown::HashMap<u32, char> = spec.specialize(value);
        specialized_impl(value);
    } else {
        default_impl(value);
    }
}

You can also check out a more comprehensive example that implements custom data encoders and decoders with customizable per-type encoding and decoding errors and optimized byte array encoding and decoding. The full example is available at at: examples/encode.rs. The part of the example related to the Encode implementation for a slice:

// ...

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(())
    }
}

// ...

Find values by type in generic composite types:

use try_specialize::{LifetimeFree, TrySpecialize};

pub trait ConsListLookup {
    fn find<T>(&self) -> Option<&T>
    where
        T: ?Sized + LifetimeFree;
}

impl ConsListLookup for () {
    #[inline]
    fn find<T>(&self) -> Option<&T>
    where
        T: ?Sized + LifetimeFree,
    {
        None
    }
}

impl<T1, T2> ConsListLookup for (T1, T2)
where
    T2: ConsListLookup,
{
    #[inline]
    fn find<T>(&self) -> Option<&T>
    where
        T: ?Sized + LifetimeFree,
    {
        self.0.try_specialize_ref().or_else(|| self.1.find::<T>())
    }
}

#[derive(Eq, PartialEq, Debug)]
struct StaticStr(&'static str);
// SAFETY: It is safe to implement `LifetimeFree` for structs with no
// parameters.
unsafe impl LifetimeFree for StaticStr {}

let input = (
    123_i32,
    (
        [1_u32, 2, 3, 4],
        (1_i32, (StaticStr("foo"), (('a', false), ()))),
    ),
);

assert_eq!(input.find::<u32>(), None);
assert_eq!(input.find::<i32>(), Some(&123_i32));
assert_eq!(input.find::<[u32; 4]>(), Some(&[1, 2, 3, 4]));
assert_eq!(input.find::<[u32]>(), None);
assert_eq!(input.find::<StaticStr>(), Some(&StaticStr("foo")));
assert_eq!(input.find::<char>(), None);
assert_eq!(input.find::<(char, bool)>(), Some(&('a', false)));

§Documentation

API Documentation

§Feature flags

  • alloc (implied by std, enabled by default): enables LifetimeFree implementations for alloc types, like Box, Arc, String, Vec, BTreeMap etc.
  • std (enabled by default): enables alloc feature and LifetimeFree implementations for std types, like OsStr, Path, PathBuf, Instant, HashMap etc.
  • unreliable: enables unreliable functions, methods and macros that rely on Rust standard library undocumented behavior. See unreliable module documentation for details.

§How it works

  • Type comparison when both types are 'static is performed using TypeId::of comparison.
  • Type comparison when one of types is LifetimeFree is performed by converting type wrapped as &dyn PhantomData<T> to &dyn PhantomData<T> + 'static and comparing their TypeId::of.
  • Specialization is performed using type comparison and transmute_copy when the equality of types is proved.
  • Unreliable trait implementation check is performed using an expected, but undocumented behavior of the Rust stdlib PartialEq implementation for Arc<T>. Arc::eq uses fast path comparing references before comparing data if T implements Eq.

§Alternative crates

  • castaway: A very similar crate and a great simpler alternative that can cover most usecases. Its macro uses Autoref-Based Specialization and automatically determines the appropriate type of specialization, making it much easier to use. However, if no specialization is applicable because of the same Autoref-Based Specialization, the compiler generates completely unclear errors, which makes it difficult to use it in complex cases. Uses unsafe code for type comparison and specialization.
  • coe-rs: Smaller and simpler, but supports only static types and don’t safely combine type equality check and specialization. Uses unsafe code for type specialization.
  • downcast-rs: Specialized on trait objects (dyn) downcasting. Can’t be used to specialize unconstrained types. Doesn’t use unsafe code.
  • syllogism and syllogism_macro: Requires to provide all possible types to macro that generate a lot of boilerplate code and can’t be used to specialize stdlib types because of orphan rules. Doesn’t use unsafe code.
  • specialize: Requires nightly. Adds a simple macro to inline nightly min_specialization usage into simple if let expressions.
  • specialized-dispatch: Requires nightly. Adds a macro to inline nightly min_specialization usage into a match-like macro.
  • spez: Specializes expression types, usingAutoref-Based Specialization. It won’t works in generic context but can be used in the code generated by macros.
  • impls: Determine if a type implements a trait. Can’t detect erased type bounds, so not applicable in generic context, but can be used in the code generated by macros.

§Comparison of libraries supporting specialization in generic context:

crate
try-specialize
crate
castaway
crate
coe-rs
crate
downcast-rs
crate
syllogism
min_spec...
nightly feature
crate
specialize
crate
spec...ch
Rust toolchainStableStableStableStableStableNightlyNightlyNightly
API complexityComplexSimpleSimpleModerateSimpleSimpleSimpleSimple
API difficultyDifficultEasyEasyModerateModerateEasyEasyModerate
Zero-cost (compile-time optimized)YESYESYESnoYESYESYESYES
Safely combines type eq check and specializationYESYESnoYESYESYESYESYES
Specialize value referencesYESYESYESN/AYESYESYESno
Specialize valuesYESYESnoN/AYESYESYESYES
Specialize values without consume on failureYESYESnoN/AYESYESnoYES
Limited non-static value specializationYESYESnoN/AYESYESYESYES
Full non-static value specializationnononoN/AYESnonono
Specialize trait objects (dyn)N/AN/AN/AYESN/AN/AN/AN/A
Compare types without instantiationYESnoYESnoYESYESYESno
Support std typesYESYESYESYESnoYESYESYES
Specialize from unconstrained typeYESYESnononoYESYESYES
Specialize to unconstrained typeYESnonononoYESYESYES
Check generic implements “erased” traitYES, but unreliablenonononoYESYESYES
Specialize to generic with added boundsnononononoYESYESYES
API based onTraitsMacrosTraitsMacros + TraitsTraitsLanguageMacrosMacros
Type comparison implementation based onTypeId
+ transmute
TypeId
+ transmute
TypeIdN/ATraitsLanguageNightly
feature
Nightly
feature
Type casting implementation based ontransmute_copyptr::readtransmutestd::any::AnyTraitsLanguageNightly
feature
Nightly
feature
Implementation free of unsafenononoYESYESYESYESYES

§Primitive example of the value specialization using different libraries

crate try_specialize:

use try_specialize::TrySpecialize;

fn spec<T>(value: T) -> Result<u32, T> {
    value.try_specialize()
}

assert_eq!(spec(42_u32), Ok(42));
assert_eq!(spec(42_i32), Err(42));
assert_eq!(spec("abc"), Err("abc"));

crate castaway:

use castaway::cast;

fn spec<T>(value: T) -> Result<u32, T> {
    cast!(value, _)
}

assert_eq!(spec(42_u32), Ok(42));
assert_eq!(spec(42_i32), Err(42));
assert_eq!(spec("abc"), Err("abc"));

crate coe-rs:

use coe::{is_same, Coerce};

// Library don't support non-reference.
// specialization. Using reference.
fn spec<T>(value: &T) -> Option<&u32>
where
    // Library don't support specialization of
    // unconstrained non-static types.
    T: 'static,
{
    is_same::<u32, T>().then(|| value.coerce())
}

fn main() {
    assert_eq!(spec(&42_u32), Some(&42));
    assert_eq!(spec(&42_i32), None);
    assert_eq!(spec(&"abc"), None);
}

crates downcast-rs:

use downcast_rs::{impl_downcast, DowncastSync};

trait Base: DowncastSync {}
impl_downcast!(sync Base);

// Library requires all specializable
// types to be defined in advance.
impl Base for u32 {}
impl Base for i32 {}
impl Base for &'static str {}

// Library support only trait objects (`dyn`).
fn spec(value: &dyn Base) -> Option<&u32> {
    value.downcast_ref::<u32>()
}

fn main() {
    assert_eq!(spec(&42_u32), Some(&42));
    assert_eq!(spec(&42_i32), None);
    assert_eq!(spec(&"abc"), None);
}

crate specialize:

// Requires nightly.
#![feature(min_specialization)]

use specialize::constrain;

// Library don't support non-consuming
// value specialization. Using reference.
fn spec<T: ?Sized>(value: &T) -> Option<&u32> {
    constrain!(ref value as u32)
}

assert_eq!(spec(&42_u32), Some(&42));
assert_eq!(spec(&42_i32), None);
assert_eq!(spec("abc"), None);

crate specialized-dispatch:

// Requires nightly.
#![feature(min_specialization)]

use specialized_dispatch::specialized_dispatch;

// The library don't support using generics.
// from outer item. Using `Option`.
fn spec<T>(value: T) -> Option<u32> {
    specialized_dispatch! {
        T -> Option<u32>,
        fn (value: u32) => Some(value),
        default fn <T>(_: T) => None,
        value,
    }
}

assert_eq!(spec(42_u32), Some(42));
assert_eq!(spec(42_i32), None);
assert_eq!(spec("abc"), None);

crates syllogism and syllogism_macro:

use syllogism::{Distinction, Specialize};
use syllogism_macro::impl_specialization;

// Library specialization can not be
// implemented for std types because of
// orphan rules. Using custom local types.
#[derive(Eq, PartialEq, Debug)]
struct U32(u32);
#[derive(Eq, PartialEq, Debug)]
struct I32(i32);
#[derive(Eq, PartialEq, Debug)]
struct Str<'a>(&'a str);

// Library requires all specializable
// types to be defined in one place.
impl_specialization!(
    type U32;
    type I32;
    type Str<'a>;
);

fn spec<T>(value: T) -> Result<U32, T>
where
    T: Specialize<U32>,
{
    match value.specialize() {
        Distinction::Special(value) => Ok(value),
        Distinction::Generic(value) => Err(value),
    }
}

assert_eq!(spec(U32(42)), Ok(U32(42)));
assert_eq!(spec(I32(42_i32)), Err(I32(42)));
assert_eq!(spec(Str("abc")), Err(Str("abc")));

min_specialization nightly feature:

// Requires nightly.
#![feature(min_specialization)]

// The artificial example looks a bit long.
// More real-world use cases are usually
// on the contrary more clear and understandable.
pub trait Spec: Sized {
    fn spec(self) -> Result<u32, Self>;
}

impl<T> Spec for T {
    default fn spec(self) -> Result<u32, Self> {
        Err(self)
    }
}

impl Spec for u32 {
    fn spec(self) -> Result<u32, Self> {
        Ok(self)
    }
}

assert_eq!(Spec::spec(42_u32), Ok(42));
assert_eq!(Spec::spec(42_i32), Err(42));
assert_eq!(Spec::spec("abc"), Err("abc"));

§License

Licensed under either of

at your option.

§Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Modules§

  • unreliablealloc and unreliable
    This module contains a set of functions, traits and macros which depend on undocumented stdlib behavior and should therefore be used with caution.

Macros§

  • Generates a function which returns true if the given type implements specified trait. Note that all the lifetimes are erased and not accounted for.

Structs§

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

Traits§

  • A marker trait for types that do not contain any lifetime parameters.
  • A trait for specializing one type to another at runtime.
  • A trait that defines a mapping between an input type and an output type.

Functions§