Traits for raw function types are now impractical after coercions-rfc


#1

TLDR; It is not possible to implement traits for fn() types to be used without casting (which I consider impractical).

The long story:

First off a little background: I’m experimenting with Rust by implementing language bindings with Rust-looking APIs. I started making bindings with Lua, I know there are at least two other bindings of Lua already but I’m mainly doing it for personal experience to learn how far can I go in regards to sort of meta programming.

I knew about the experimental nature of Rust from the beginning and I was willing to try my best to cope with the moving changes of the pre-1.0.0 phase. I mostly think it did great for the language.

My whole point revolves around this basic API sketch, which may not be the best but I see no reason for it to be impossible: having a method that can to push any object which can be represented and used on the other language to it’s stack, like this:

struct VM {...}
impl VM {
    pub fn push<T: ToVM>(&mut self, value: T) { ToVM::push(self, value); }
}

trait ToVM { fn push(vm: &mut VM, value: Self); }

With that in mind I only need to impl ToVM for every type that can be pushed into the VM using its native API, namely foreign functions.

For all basic data types that’s perfectly reasonable, even for Vectors and HashMaps that’s doable.

But for functions and closures it was only until before the coercions rfc that it was doable.

For simplicity I’ll only talk about functions but it all applies to closures with a few adjustments.

When functions had denotable types it was ok, I could use lua’s API to push a lua closure, which is a C function with some associated data, the data would be the transmuted fn and the C function a wrapper which transmuted the data back into a fn and called it. Effectively pushing a Rust function into the lua’s stack which could be called within the lua context. (which is exactly the technique Selene uses on C++). Which won’t require the user to change their function just so it works with my implementation, nor require any casting because of a language limitation.

But regardless of the internals used to make that work, it was possible simply because the raw function types were usable.

The higher picture for me is that functions should not have unique types the same way ints don’t have unique types. There is the Int and Float traits but you also have the iXX and fXX types, the same should be with functions, Fn() traits and also usable fn() types. Because it should be possible to implement a trait for functions which only applies if you can work with their underlying type.

To me that’s a key issue for a system language.

What most intrigues me is what lead to making functions have their own types, I don’t know the implementation but I think it only makes it more complicated.

What I’m interested in knowing is:

  • Am I missing something, is my point not valid?
  • What’s the motivation for unique fn types?
  • Will there be any coercions to address this (or if that’s possible)?
  • I read somewhere that functions will change from pointer like to zero sized like, is that true? Wouldn’t that make it even worse?

#2

Generally, the best thing to do is to replace a trait implemented for fn(A) -> B with a trait implemented for all types F which implement Fn(A) -> B. This gets you the bonus that your trait works for anything that is callable, not just a fn pointer.

The motivation for unique fn types is that it fits very well with the closure model. Basically, if I define a function foo that is generic over some callable type F : Fn(...):

fn foo<F:Fn()>(f: F) { ... }

and then I call foo(bar) for some bare function bar:

fn bar() { ... }

Now when foo is monomorphized, it will be compiled with F=<bar>, where <bar> is the unique type of the bar fn item (hopefully we’ll eventually add synax for this, probably typeof bar). This means that the compiler can see exactly which fn is being called and hence it will compile a direct, statically linked call, just like you wanted. If we kept the older model, where the type of bar was fn(), then it would be more efficient to do foo(|| bar()) than to do bar(), because the former would be statically linked to the closure which, in turn, could be statically linked to bar, but the latter would be compiled with F=fn(), which is just a fn pointer.

The zero-size aspect is analogous. It ensures that an object like Box<Fn()>, when created with a bare fn like bar, involves no allocation. This means it is roughly as efficient as the more restriction fn() type (though calls still involve an additional indirect load to fetch the fn pointer from the vtable).

I find this motivation very compelling. It does mean that if you want to implement on fn pointers specifically, vs all callable things, you must use a cast.


#3

The function-pointer types fn(...) -> ... still exist, and acts a little bit like a trait object for function items.

A function item will coerce to a function pointer, e.g.

fn use_fn(f: fn(i32) -> i32) {
    println!("{}", f(1));
}

fn add_one(x: i32) -> i32 { x + 1 }

fn main() {
    use_fn(add_one);
}

works, and prints 2.


#4

This is fantastic. Does this apply automatically to closures without captures as well (i.e. written using closure syntax). I needed to know this earlier so that I would have understood that returning closures now is just as easy and cheap as it was before with the old &'static fn(A) -> B boxed stack closures.


#5

The main reason it doesn’t work to implement for all types F which implement Fn(A) -> B is that my trait should be implemented for a range of functions with varying number of arguments and the implementation depends on the number of arguments. And that falls apart because a type may implement two different Fn() traits, and hence the trait system itself won’t allow me to do these different implementations.

Another reasons is that the implementation is dependent on the underlying structure of the function, while it would be great to support anything that’s callable it’s not possible and I rather support any function which I know the underlying structure, if the user wants it to work with their custom callable type they should implement the trait because they’ll have access to the concrete type. That is because I have to transmute the function pointer to store it as raw data with a foreign API, and transmute it back.

Well I basically have the same problem with closures, the difference is that the closure has a lifetime and may have borrowed variables, so it is not safe to transmute it to two pointers, and then back transmute it back to reconstruct it. But that’s my implementation issue which I suspect I can solve with lifetime annotations.

The issue I have with the language here is that I can’t implement my trait for a closure type, I have to implement for a type F which implements FnMut() and that leads to the same problem as with functions.


#6

That’s the simplest case, it won’t coerce on the following, which is what I miss:

trait Test {}
impl Test for fn() {}

fn tester<T: Test>(_: T) {}

fn foo() {}

fn main() {
    tester(foo); // error, since in fact Test is not impl for typeof(foo) but for its 'common' type
    tester(foo as fn ());  // works but hinders usability for the user
}

#7

On Thu, Feb 05, 2015 at 05:00:51PM +0000, Jan Segre wrote: