Crate syllogism[][src]

Expand description

Utilities to allow for some specialization using stable Rust.

This crate provides two mechanisms for specialization:

  • Using the IsNot trait. This can be used when the data-type that you want to syllogism for is a concrete type.
  • Using the Specialize trait.

When using the IsNot trait, in order to syllogism for a data type T1 and handle another data type T2 in a generic way, T2 needs to implement IsNot<T1>.

When using the Specialize trait, in order to syllogism for a data type T1 and handle another data type T2 in a generic way, both T1 and T2 need to implement Specialize<T1>.

You may need many impl blocks for implementing the IsNot and the Specialize trait. In order to facilitate you to do so, you can use the define_compatibility macro.

The syllogism-macro crate defines some procedural macros that help with these implementations, also when not all compatible data types are defined in the same crate.

Specialization using the IsNot trait

Using the IsNot trait is straightforward if you want to syllogism for a concrete data type.

use syllogism::IsNot;

trait GenericTrait<T> {
    // ...
}

struct SpecialDataType { /* ... */ }

struct GenericDataType1 { /* ... */ }
impl IsNot<SpecialDataType> for GenericDataType1 {}

struct GenericDataType2 { /* ... */ }
impl IsNot<SpecialDataType> for GenericDataType2 {}

struct MyStruct {
    // ...
}

impl GenericTrait<SpecialDataType> for MyStruct {
    // The special implementation
}
  
// This can be used for the data types `GenericDataType1` and `GenericDataType2`.
impl<T> GenericTrait<T> for MyStruct
where T: IsNot<SpecialDataType> {
    // The generic implementation
}

You can also have specialization for more than one type:

use syllogism::IsNot;

// One special implementation:
impl GenericTrait<SpecialDataType1> for MyStruct {
    // ...
}

// Another special implementation:
impl GenericTrait<SpecialDataType2> for MyStruct {
    // ...
}

// The generic implementation.
impl<T> GenericTrait<T> for MyStruct
where T: IsNot<SpecialDataType1> + IsNot<SpecialDataType2>{
    // ...
}

Specialization using the Specialize trait

If the data type for which you want to syllogism is a type parameter, you cannot use the IsNot trait because the compiler cannot know if a type (even not in a dependent crate) will implement IsNot<Self>. Not implementing IsNot<Self> is just a convention, it is not compiler-enforced and the compiler cannot see this. To work around this, you can use the Specialize trait:

use syllogism::{Specialize, Distinction};

trait GenericTrait<T> {
    fn some_method(&self, param: T);
}
struct MyStruct<T> {
    // ...
}

struct SpecialDataType { /* ... */ }
impl Specialize<SpecialDataType> for SpecialDataType {
    fn specialize(self) -> Distinction<SpecialDataType, Self> {
         Distinction::Special(self)
    }
}

struct GenericDataType1 { /* ... */ }
impl Specialize<SpecialDataType> for GenericDataType1 {
    fn specialize(self) -> Distinction<SpecialDataType, Self> {
         Distinction::Generic(self)
    }
}
struct GenericDataType2 { /* ... */ }
impl Specialize<SpecialDataType> for GenericDataType2 {
    // Similar to the implementation of Specialize for `GenericDataType1`.
}

// Can be used for each `T` in `SpecialDataType`, `GenericDataType1` and `GenericDataType2`.
impl<T> GenericTrait<T> for MyStruct<T>
where T: Specialize<SpecialDataType> {
    fn some_method(&self, param: T) {
        match param.specialize() {
            Distinction::Special(special) => {
                // The special implementation.
            },
            Distinction::Generic(generic) => {
                // The generic implementation.
            }
        }
    }
}

Using the Specialize trait is limited to methods that take the parameter by value (as oposed to by reference).

Implementing IsNot and Serialize across crate boundaries

As discussed before, for any data types T1 and T2 that you want to syllogism for or that are used in the general case, you may want to implement

  • impl IsNot<T2> for T1 {} (1) and
  • impl IsNot<T1> for T2 {} (2).

Because of the orphan rules, (1) can only be implemented in the crate that defines T1 and (2) can only be implemented in the crate that defines T2. In order to loosen the coupling and dependency between the crates, for each crate, a trait is defined (“NotInCrateX”) and a blanket impl combines the crates together:

// In some crate

pub trait NotInCrate1 {}
pub trait NotInCrate2 {}

These macros are then imported in crate1 and crate2 and used as follows:

// In crate1
use syllogism::{IsNot};

struct T1 {
    // ...
}

impl<T> IsNot<T> for T1
where T: NotInCrate1
{}

impl NotInCrate2 for T1 {}

and

// In crate2
use syllogism::{IsNot};

struct T2 {
    // ...
}

impl<T> IsNot<T> for T2
where T: NotInCrate2
{}

impl NotInCrate1 for T2 {}

Now if there are many crates at play, you need more trait impls:

// In crate1:
use syllogism::{IsNot};

struct T1 {
    // ...
}

impl<T> IsNot<T> for T1
where T: NotInCrate1
{}

impl NotInCrate2 for T1 {}
impl NotInCrate3 for T1 {}
// ...

In order to help you with these impls, you can use the define_compatibility macro to define the NotInCrateX traits and to automatically generate macros for implementing NotInCrate2, NotInCrate3, … .

Macros

A macro to define traits and other macros that help to implement IsNot and Specialize across crates.

Enums

An enum that allows to distinguish between the special case and the generic case.

Traits

A trait to inform the compiler that two data types are distinct.

A trait allowing to distinguish between the special case and the generic case at run time.