Help with implementing From trait for wrapping a std f64 function in an enum

Background: I'm working on a simple expression evaluator and would like to allow the standard floating point functions to be called (e.g. f64::abs) by user-provided expressions. I'm not quite sure of the best way to structure such a thing, so I wrote a wrapper enum for the different types of function signatures. The evaluator would lookup the wrapper for a named function in a HashMap and then pass a vector of values to the wrapper, which then calls the target function with the appropriate arguments. (I am open to better ways to approach this...).

Anyways, as I was trying to simplify the construction of the wrapper, I thought I'd impl From<fn(f64) -> f64> so I could just do f64::abs.into(). But, no joy... The compiler seems to be angry about something that I can't quite understand:

the trait std::convert::From<fn(f64) -> f64 {std::f64::<impl f64>::abs}> is not implemented for FunctionWrapper

The odd thing (to me) is that if I cast the function to fn(f64) -> f64, the compiler will accept that...

Here's a snippet of code and the errors that are returned:


enum FunctionWrapper {
    Float1(fn(f64) -> f64),
}

impl From<fn(f64) -> f64> for FunctionWrapper {
    fn from(f: fn(f64) -> f64) -> Self {
        FunctionWrapper::Float1(f)
    }
}

fn main() {
    // Why does this work?
    let fw_worky: FunctionWrapper = (f64::abs as fn(f64) -> f64).into();
    // But, this doesn't?
    let fw_no_worky: FunctionWrapper = f64::abs.into();
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `FunctionWrapper: std::convert::From<fn(f64) -> f64 {std::f64::<impl f64>::abs}>` is not satisfied
  --> src/main.rs:14:49
   |
14 |     let fw_no_worky: FunctionWrapper = f64::abs.into();
   |                                                 ^^^^ the trait `std::convert::From<fn(f64) -> f64 {std::f64::<impl f64>::abs}>` is not implemented for `FunctionWrapper`
   |
   = help: the following implementations were found:
             <FunctionWrapper as std::convert::From<fn(f64) -> f64>>
   = note: required because of the requirements on the impl of `std::convert::Into<FunctionWrapper>` for `fn(f64) -> f64 {std::f64::<impl f64>::abs}`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

Functions in Rust all have their own special unnamable Function type. This has the advantage that if you pass a function to a method with a generic Fn trait parameter, the compiler will monomorphize the code for that specific function as argument and avoid a dynamic function call. (Not too huge a deal IMO, but I guess it makes some abstractions more “zero cost”.)

Now, to make function pointers more ergonomic there is a coercion rule that makes it possible to convert functions (and non-capturing closures, too) into function pointers. fn(f64) -> f(64) is such a function pointer type.

Usually coercion happen automatically when necessary, but unfortunately they don't play nicely with generics, in particular with traits and method resolution. When you pass the function to into it will do method resolution on the special anonymous type for that particular function it doesn't find any matching method because that function type does not implement the Into trait (or any other trait with a medhod of named into that's in scope). One way to explicitly force a coercion before you pass the function is using as as you did, that's why it made your code compile. Another option is to call your enum constructor directly FunctionWrapper::Float1(f64::abs). You could make this more concise with use FunctionWrapper::*. In this explicit constructor call it is obvious to the compiler that a function pointer is expected and it will do the coercion for you.

1 Like

Do you know if this is a known bug or accepted limitation? It seems a little odd that the compiler would be looking for a trait impl using the unnamable type. Wouldn't it be impossible to create such an impl? Should it not perform the coercion to a type that could be named?

Yes, this is what I had done in the first place. I then started playing code golf...

Thanks for the reply!

It's a lot more likely that the compiler simply avoids such a trait search.

And which type should it be: fn(), dyn Fn, dyn FnMut, dyn FnOnce? Each of them can have trait implementations independently from others, in theory.

1 Like

I'm able to create an impl for From where the type is dyn Fn (and presumably for the other function traits). But, I don't see how to create an impl for the .. concrete(?) .. fn() type.

I mean, should the concrete unnameable type coerce to function pointer, as you've done manually, or to the trait object?

You can implement for unnameable types generically:

impl<F: Fn(f64) -> f64> Trait for F { ... }

Your OP does create the impl for the concrete fn(f64) -> f64 type, just not for the unnameable fn(f64) -> f64 {particular function} type.

In general, it's not possible to create an impl for the fn(f64) -> f64 {particular function} type, as that type is unnameable. As @cuviper wrote, we could create a generic impl for T: Fn(f64) -> f64, and that would include fn(f64) -> f64 { particular function }. But generically implementing From isn't possible without specialization due to the existing impl<T> From<T> for T impl, and since you're storing the function, I don't think such a generic impl would be extremely useful in any case? Maybe if you decide to store a Box<dyn Fn(f64) -> f64 instead, but even then you'd still only get exactly one impl this generic.

As @Cerberuser stated, if the compiler were to coerce from fn(f64) -> f64 { particular function } into fn(f64) -> f64, that would have to be an explicit choice. And if it knows you want an fn(f64) -> f64, it will make that coercion. It's just that when you have the situation

need: T impls Into<FunctionWrapper>
have: T coerces from fn(f64) -> f64 { particular function }

The choices of T as fn(f64) -> f64 is not obvious. If you only have the single impl impl From<fn(f64) -> f64> for FunctionWrapper {, then it is the only choice. But rust tries, in all ways, to be forward compatible for adding new impls!

So, assuming you add an impl

impl From<dyn Fn(f64) -> f64> for FunctionWrapper {

what should the compiler choose for T now? It could be either fn(f64) -> f64, and thus use the From<fn(f64) -> f64> impl, or it coul dbe dyn Fn(f64) -> f64, and thus choose the From<dyn Fn(f64) -> f64 impl. It's ambiguous.

Because adding a new trait implementation could lead to this ambiguous situation, and new trait implementations are supposed to be backwards compatible, the current situation is also ambiguous. In an effort to not have new future impls (such as From<dyn Fn(f64) -> f64>) not break the situation, it pre-breaks it so you have to explicitly decide.

Another language could easily choose to have this coercion to fn(f64) -> f64 happen automatically - and I know other languages which will do this kind of thing, for example, Scala (not this exact example, but similar things). But Rust makes the choice not to for the sake of maintainability, and future-proofing code to the greatest extent.

I think there’s multiple ways to change the compiler to make this possible.

One could argue that certain coercions are more canonical than others and they could get special treatment for method resolution and trait resolution. In a way we already have special treatment for some coercions for method resolutions. The autoref and autoderef steps are more than just coercion, but for references, dereferencing and unsized coercion are valid coercions. For method resolution it is already the case that new trait implementations (of a different trait with the same method name), adding methods to existing traits, adding new traits (with glob imports, like use path::to::import::*) and addign new inherent methods to types can all lead to breakage introducing ambiguities or even changing behavior silently. Adding some more coercions to method resolutions and also type checking around calling generic functions with type constraints (i.e. what I called trait resolution above) could be done carefully and AFAICT actually without introducing any new ambiguities or breakage that would not come from a single place due to orphan rules and choosing the allowed coercions in a way that always only permits a single direction to continue coercing into. I believe that a reasonable list of permitted coercions would include: functions and non-capturing closures to fn-pointers, deref coercion, unsized coercion of non deref-able types.

Another way, perhaps easier would be to introduce a trait that allows a generic implementation to check if a type is coercible to some other type. So you could do impl<F: CoercibleTo<fn(f64) -> f64>> From<F> for FunctionWrapper or something like that. Comparing to the above approach this has the advantage of supporting every kind of coercion and the disadvantages that this doesn’t offer a way to help with trait implementations for an imported trait due to orphan rules, and it’s opt-in only.

An approach more specific to function pointers would be to introduce a trait that’s automatically implemented for types that coerce to function pointers, or even more specific only implemented for function and non-capturing-closure types, or perhaps only for function types.

Another approach I could imagine is a trait for zero-sized types (probably needs to be opt-in but because changing the size of a type is not supposed to be a breaking change, but could be automatically implemented for functions and for closures that only capture variables that implement this zero-sized-trait). Let’s call it ZeroSized. Then one could allow the type dyn WhateverTrait + ZeroSized to be a thing and a sized type, at least for traits WhateverTrait that have no blanket implementations, and that type would only be a pointer to the vtable, or perhaps even the whole vtable by-value for small vtables and one could introduce coercions to fn-pointers for both dyn (Fn(...) -> ...) + ZeroSized and types F with F: (Fn(...) -> ...) + ZeroSized. Then you could write generic methods with parameter F: (Fn(...) -> ...) + ZeroSized and coerce values of that type to fn-pointers.


That last approach hints at a way to go about this today: Use Box<dyn Fn(f64) -> f64> in your FunctionWrapper instead of fn(f64) -> f64 and do impl<F: Fn(f64) -> f64> From<F> for FunctionWrapper. Now doing f64::abs.into() would create a Box without allocation since f64::abs has its own anonymous zero-sized type (see, now there’s some advantage to this feature). The only disadvantage: the Box is twice as big as a function pointer and calling the function in a Box has one extra indirection through the vtable, so it is slightly slower, most likely negligible compared to the HashMap and vector-passing plans you have. But you gain the potential advantage that you also support closures with captured data now.

If you’re going with Box, you could also simplify your type to something like Box<dyn Fn(&[f64]) -> Result(f64, ...)> and won’t need an enum anymore. Something like

enum EvaluationError{WrongNumberOfArgument, /* ... */}
use EvaluationError::*;

struct FunctionWrapper(Box<dyn Fn(&[f64]) -> Result<f64, EvaluationError>>);

impl<F: Fn(f64) -> f64 + 'static> From<F> for FunctionWrapper {
    fn from(f: F) -> Self {
        FunctionWrapper(Box::new(move |args| match args {
            &[x] => Ok(f(x)),
            _ => Err(WrongNumberOfArgument),
            // alternatively return an Option and leave error generation
            // to your evaluation function, i.e. change `FunctionWrapper` to 
            // `Box<dyn Fn(&[f64] -> Option<f64>>`
        }))
    }
}

fn main() {
    // This still works, but now it will result in allocation for the Box
    let fw_worky: FunctionWrapper = (f64::abs as fn(f64) -> f64).into();
    // Now this also works and it will work without allocation (I’m pretty sure)
    let fw_also_worky: FunctionWrapper = f64::abs.into();
}

The closure in the From impl is zero-sized itself when the passed F is zero-sized, hence the Box will not have to allocate when passed a function like f64::abs.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.