The types of impossible trait objects can be described

A user in the Discord was confused by a compiler error this code generated:

fn f(cb: &dyn Fn() -> dyn Send) {
    (cb)();
}
error[E0618]: expected function, found `&dyn Fn() -> (dyn Send + 'static)`
 --> src/lib.rs:2:5
  |
1 | fn f(cb: &dyn Fn() -> dyn Send) {
  |      -- `&dyn Fn() -> (dyn Send + 'static)` defined here
2 |     (cb)();
  |     ^^^^--
  |     |
  |     call expression requires function

I'd say this error should explain that the trait object isn't callable because the return type is unsized, and preferably before we even try to call it. I would normally file an issue on the bug tracker, but the minimal example came with a few surprises.

It turns out this compiles -

trait Assoc {}
trait Trait {
    type Type: Assoc;
}
type Impossible = dyn Trait<Type = ()>;

Instead, rustc only complains once we try to use it: the dyn Trait<Type = ()> doesn't implement Trait. Why do trait objects behave like this?

It's probably because type is only an alias, and it doesn't create an instance of a type, so the type doesn't need to be checked at that point.

This compiles too:

type MaybePossible<X> = dyn Trait<Type = X>;

Note that it doesn't require MaybePossible<X: Assoc>:

= note: #[warn(type_alias_bounds)] on by default
help: the bound will not be checked when the type alias is used, and should be removed

so I guess it's a feature, not a bug? :slight_smile:

Though I'd rather a type that definitely wasn't usable errored out, it's beside the point. The same type is allowed in argument position:

fn f(v: &dyn Trait<Type = ()>) {}

If you then try to use that v, the error will point at your use site, as in the original example. I feel that the "best" solution to the confusing error is an explanation of the cb variable's type being nonsense, and I wanted to check why it was allowed at all to figure out how to solve it.

2 Likes

You can use any type without ever instantiating it. (E.g., see PhantomData.) Types are useful for more than just describing data, they can also describe relationships between data.

How might these impossible trait objects be used at the type level? I'm struggling to think of a case where they'd be preferable to a Marked<()> that justifies the ergonomics problem

I just been playing around with this a bit and, well..

..turns out, something like fn() -> dyn Send is a type that rustc accepts and that implements FnOnce, violating the implicit FnOnce::Output: Sized constraint.

yeah, I know, only marginally related to the problem that you’re describing in this thread, but I thought you might be interested since this thread lead me to finding this :wink:

5 Likes

Yeah, it sorta spirals, doesn't it :smiley: I tried not to bring up too many points in this post in case it brought the discussion off-track, but afaict there are a few issues that ought to be solved

I was only pointing out that, in general, there is no reason to emit an error just because a type is created which is impossible to use. A type isn't nonsense just because you can't use it (see the never type (!) for example.)

In this case, there could easily be some value in allowing an uncallable functions to be defined, for instance, to help macro authors operate within generic contexts.

2 Likes

But the ! type can be used:

#![feature(never_type)]

fn foo(never: !) -> ! {
    never.clone()
}

This function can't be executed at runtime, but it does compile. That's because ! implements Clone, therefore it can be cloned. Likewise, anything that implements FnOnce can be called. A function that can't be called doesn't make sense, because then it isn't a function, right?

That depends on your definitions. The never type is useful even if "it doesn't make sense" as a type with a value, and we don't get a compile error just because we've used it. So there's no inherent reason an uncallable function should be an error, because you can just simply never call it. Just like you will never call foo(never: !).

Anything that implements FnOnce can be called at most once. Nothing guarantees you can call it more than zero times. (Because it could take a ! argument, for example.)

Even if it takes an ! argument, it can be called:

fn foo(never: !, f: impl FnOnce(!)) {
    f(never);
}

I haven't tried it out, but I'm pretty sure that it works.

My definition is, a function is something that implements any of the Fn* traits. This implies that it can be called. How often it can be called doesn't matter in this context.

That doesn't show that f can be called, since foo can't be called either. More formally, you can imagine a "calling convention" for functions of the form fn(!, T) that is "the function pointer is always <*const ()>::dangling(), and to call the function you execute unreachable". In this case, when visiting foo's body you will emit the code { unreachable }, or perhaps you will look at the arguments of foo, notice that never: ! is present, and not emit any function at all.

You can call this "calling the function" since f(never) is not a compile error, but that is only a compile-time notion; there is clearly no actual function calling going on here.

I'm aware that ! can't be instantiated (in safe rust). But that's not the point. ! can't exist at runtime, but it does exist at the type level. So the type system considers f a callable function, which I showed in my previous comment.

I think there's some misunderstanding about the problem at hand. To get back on topic, @Plecra showed that you can have a Fn trait object returning an unsized type. This type is accepted by the type system, but trying to call it produces an error, indicating that the Fn trait object doesn't implement the Fn trait. This trait object is impossible to create (in safe rust). I guess that's why you compared it to the ! type. The problem is that ! can be used at the type level, since it implements useful traits (such as Clone), whereas the Fn trait object doesn't. It doesn't even implement the Fn trait, which is quite confusing.

I'm pretty sure there are other counterexamples, where dyn Trait doesn't implement Trait, possibly around object safety or auto traits, but I can't think of any off-hand, perhaps someone else knows. I think @steffahn 's issue shows pretty clearly that this is a bug, and a soundness bug at that (which is prevented on stable only because the FnOnce<Args> syntax is unstable). FnOnce clearly requires in its definition that its argument should be Sized, so dyn (FnOnce() -> dyn Trait) should not be a well formed type. If/when feature(unsized_locals) lands, this constraint can be lifted, and I would expect that dyn (FnOnce() -> dyn Trait) would become a valid (unsized) type.

It's true that there's nothing wrong with uninhabited types in general, but there definitely is something wrong with a type that violates its own trait bounds, because this can be used to deduce false statements in the type system, resulting in unsoundness. If FnOnce did not require Output: Sized but its call_once function did, then it would be reasonable to have the types FnOnce(A) -> B with B: !Sized be empty types, although I don't think the type system can soundly know this (in the sense of deducing ! from a member f: F: FnOnce(A) -> B).

1 Like