Is support for &[impl Trait] coming to allow use of empty slice?


#1

I was looking to use &[impl Trait] rather than struct names for some more flexible code but I keep getting an error whenever I have an empty slice since it doesn’t know a type.

trait Edible {}

fn foo(a: &[impl Edible]) -> &[impl Edible] {
    a
}

fn main() {
    let _x = foo(&[]);
}

Error Output

error[E0282]: type annotations needed
 --> src/main.rs:9:14
  |
9 |     let _x = foo(&[]);
  |         --   ^^^ cannot infer type for `impl Edible`
  |         |
  |         consider giving `_x` a type

error: aborting due to previous error

What I find bizarre though is if I switch it to be Option<&[impl Trait]> the None type raises this exact same error.


#2

impl Trait doesn’t mean any type, but a one very specific type that exists, but isn’t named directly.

The function foo could choose to call methods on the slice, and the results could depend on what type of slice it is exactly.

Similarly with None, the type matters. In simplest case None of Option<u64> takes more bytes in memory than None of Option<bool>.


#3

When using a slice as a signature an empty slice is just as valid as a slice containing items.

The fact that it wants to know the size of an empty slice should not be an issue. If occupying memory is the only issue I’d be happy to have the ability to tell it what’s an a acceptable space to occupy if that were possible with the dynamic functionality that impl Trait provides. But impl Trait isn’t allowed in the signature for let.

For my use case I have more than a dozen objects that impl Object and the only thing those structs contain internally is a Value struct which is a repr(C) object. So I know the size and internally all the objects are based on the same root code of one specific size. My size is known I just need (really want) Rust to accept it and allow my use case.


#4

How about slice.get(0)? It returns None for an empty slice, but how big is that value?


#5

Your comments suggest you think impl Trait is dynamic. It is not. In argument position, it is just a way to avoid writing an explicit type parameter. In return position, it is a way to avoid writing an explicit return type. These types must still exist, and must be statically determinable.

The usual solution to this is to specify the type explicitly in the call so that the compiler has enough information to nail down the types involved… But to do that you need an explicit type parameter.

So I’d say: don’t use impl Trait, write an explicit type parameter.


#6

I currently do. I have a CouldBeAnything kind of object which each object can be converted/cast into and then converted back with a kind of “try convert”. All this type casting for the same underlying structure that the C side of the code doesn’t care about seems like more work than necessary. It just seems like it could be so much better with a behavioural type cast (with many compatible) than a hard cast type (one compatible).


#7

impl Trait does mean an existencial type only for return type. For func’s arguments it describes an universal type (generic). And type annotation needed.

trait Edible {}
fn foo<T: Edible>(a: &[T]) -> &[impl Edible] {
    a
}
fn main() {
    let _x = foo(&[]);
}

#8

And what would it give you here? Rust still needs to know the concrete type.

Your only chance is to give the empty slice an explicit type.


#9

Yes, this isn’t SML, so values have a specific type, not a most-general-type.

There are simple demonstrations of this without impl Trait, like

fn main() {
    println!("{:?}", []);
}
error[E0282]: type annotations needed
 --> src/main.rs:2:5
  |
2 |     println!("{:?}", []);
  |     ^^^^^^^^^^^^^^^^^^^^^ cannot infer type for `_`

#10

In many other cases Rust figures out specific type of the slice, even if it’s empty.

mem::size_of_value(slice.get(0)) can be different for every empty slice, and Rust cares about such things.


#11

Rather than explicit you can use inference;

    struct St;
    impl Edible for St{}
    fn your_function() -> impl Edible { St{} }
    let vec = vec![your_function()];
    let empty = if false {
        vec.as_slice()
    } else {
        &[]
    };
    let _x = foo(empty);

#12

That might just work @jonh, thank you. :+1:


#13

My internet searches have lately kept bringing this form issue back up. I tried your example as follows and the type checker doesn’t permit it.

pub fn infer_impl_object(slice: &[impl Object]) -> &[impl Object] {
    let vec = vec![NilClass::new()];

    if false {
        vec.as_slice()
    } else {
      slice
    }   
}

Output:

error[E0308]: mismatched types                                                                                                              
   --> src/util.rs:122:9                                                                                                                    
    |                                                                                                                                       
122 |         slice                                                                                                                         
    |         ^^^^^ expected struct `class::nil_class::NilClass`, found type parameter                                                      
    |                                                                                                                                       
    = note: expected type `&[class::nil_class::NilClass]`                                                                                   
               found type `&[impl Object]`

The two paths of the if/else do not work since an item that implements impl Object is not the same as wanting something that implements object.

I still really want this feature in Rust.


#14

But in your example, it desugars to the following:

pub fn infer_impl_object<T: Object, C: Object>(slice: &[T]) -> &[C] {
    let vec = vec![NilClass::new()];

    if false {
        vec.as_slice()
    } else {
      slice
    }   
}

From this, we can see that we have three types:

T: Object
C: Object
NilClass: Object

Of which they are not all guaranteed to be the same, or even trait object, and therefore cannot be casted between eachother.


#15

This isn’t strictly true - this syyntax implies C is chosen by the caller, but it is not.

Still, your point stands. When you return a value from a function, that value must have a type. impl Object does not remove this requirement. It simply means you don’t have to write that type in the code.

As written, infer_impl_object would return one of two different types depending on runtime code execution. The compiler does not evaluate if false as unreachable because such optimizations happen much later, and this is part of type resolution. Your input type, and NilClass, are not necessarily the same, and if they are different then this won’t work. You must have a single type for every returned value. You can’t have 0, and you can’t have 2 or more types.

If you want to return “some value which implements Object, but not necessarily one we know at compile time”, use dyn Object. Specifically, Box<dyn Object> allows you to allocate any number of different types and construct a single type of box which could refer to any of them.

pub fn infer_impl_object(slice: &[impl Object]) -> &[Box<dyn Object>] {
    let vec = vec![Box::new(NilClass::new()) as Box<dyn Object>];

    if false {
        vec.as_slice()
    } else {
      slice.iter().map(|thing| Box::new(thing) as Box<dyn Object>)
    }   
}

Unfortunately your function has more problems than that - like the fact that you’re returning a reference to data that is owned by the function, and thus dropped at the end of the function. Even if this type checked, returning vec.as_slice() would fail.


In general, though, if you need to have one of a number of types implementing a trait, and the decision of which is made at runtime, you either need an enum with one variant per possible type, or Box<dyn Trait>.