Freedom-of-Form-Foundation / anatomy3d

A CAD tool for humanoid anatomy alterations. See the anatomy3d-blender repository for more recent work.
https://freedomofform.org/1856/3d-anatomy-project-scope-phase-1-focus-on-a-limb-joint/
GNU General Public License v2.0
7 stars 5 forks source link

Convenience tools for converting functions to ContinuousMaps. #34

Closed AdamNorberg closed 3 years ago

AdamNorberg commented 3 years ago

I couldn't find a way to make the syntax lower-friction than this, everything requires explicitly specifying <TIn, TOut> somewhere. I think I could make one that doesn't require writing out the generic types again as a static function of a class that is itself not generic, but it's not obvious that there's any reasonable name for such a class that is actually less annoying than the average (Func<TIn, TOut>) cast.

Lathreas commented 3 years ago

This looks like it could be pretty useful, thanks so much! I'll take a closer look soon™, but the functionality of it looks very nice already.

This might tie in with a change I'll do soon as well. Regarding the ContinuousMap itself, I'll likely make it more specialized than a simple Func (possibly renaming ContinuousMap as well), namely because I found that most maps are actually functions with a finite domain. I'll add some abstract functions for getting the domain size of each ContinuousMap, because that appears to be necessary for the engine to be able to properly interpolate between two height maps. So this way we have a proper division of functionality between ContinuousMap and Func.

AdamNorberg commented 3 years ago

I just updated this to also overload This and Compose to take a Func and automatically use FunctionBackedContinuousMap, allowing lambdas to be used, resulting in expressions like var map2 = map1.Then(x => x * 2) or something like that. Note that, as seen in my unit test, the type of x must be made explicit in the lambda when using Compose or else C# cannot infer the input type of the resulting map. This can also be resolved by fully specifying the types on the Compose call (which is what the compiler will ask you to do), but it's less typing to only specify the argument type for the lambda.

The lambda's return type can be inferred when its input type is known, and its input type can be inferred when only one input type is legal at that point in the program. With Then, that's the output type of the predecessor ContinuousMap. With Compose, it's unbound, because it's the new input type and therefore does not have constraints that C# can infer. C# does not reason backwards from what the return type for the lambda must be to what its input type must presumably be.

AdamNorberg commented 3 years ago

Func<TIn, TOut> is a delegate type, and it is not possible to inherit from a delegate type: https://docs.microsoft.com/en-us/dotnet/api/system.delegate?view=net-5.0

I could write the extension methods to Func<TIn, TOut> to implement Then and Compose on them as well. These would work only on fields with the literal type Func<...>, since nothing derives from it so implicit upcasts won't cause the extension method to be resolved and "convert method to compatible delegate" is only considered when the compiler is explicitly looking for a type conversion, not when it is attempting to resolve an apparent member reference (including methods). Similarly, we could declare our own delegate type, ContinuousMap, and declare the extension there; it would have the same constraints. If we convert ContinuousMap from a type to a delegate, I think this would make sense to do, on whatever type we settle on using.