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
.