Type inference for fluent API
Asked Answered
G

4

12

I have the following extension methods:

public static IFoo Foo(this IFluentApi api, Action action);

public static IFoo<TResult> Foo<TResult>(
    this IFluentApi api, Func<TResult> func);

public static IBar Bar(this IFoo foo);

public static void FooBar(this IBar bar, Action action);

public static void FooBar<TResult>( // <- this one cannot work as desired 
    this IBar bar, Action<TResult> action);

The generic interfaces are always derived from their corresponding non-generic interface.

Unfortunately, to make this work:

api.Foo(x => ReturnLong())
   .Bar()
   .FooBar(x => ...); // x should be of type long

I need to also implement the following extension method:

public static IBar<TResult> Bar<TResult> (this IFoo<TResult> foo);

and change the last of the above extension methods to:

public static void FooBar<TResult>(
    this IBar<TResult> bar, Action<TResult> action);

As I actually not only have Bar() between Foo() and FooBar() but a VERY long chain of methods I would have huge additional implementation costs.

Is there any way to avoid this problem and "magically" forward the TResult generic parameter?

Edit:

Without losing type inference!

Gaseous answered 24/7, 2013 at 18:55 Comment(8)
Is there any reason in your fluent API to have the non-generic methods? If not, just ditch them in favour of the generic ones entirely.Melody
Why do you not have a public static IBar<T> Bar (this IFoo<T> foo);?Lithology
@Chris: Unfortunately yes, what should I return in Foo(Action)?Gaseous
@supercat: Not required by the users, so why implement it? Unfortunately, if I want to make use of type inference, I have to implement it...question is now: how to overcome this flaw?Gaseous
I wouldn't call it a "flaw" at all. I think your best bet is to either implement all the methods, or create a wrapping generic builder where the TResult is defined once at a class level. (doing so I suppose would mean ditching the extension method usage though, and still require you to implement the methods) I say just bite the bullet, do it proper and avoid "magic". EDIT: Sorry, I think I fully understand the problem now. You need to chain the generic information across non-generic calls. Yeah, try making a separate builder class.Melody
@Chris: Thank you for your answer, however, there are also many Bar()-like methods not written by me, but by extension developers. I don't want them to need to implement two methods.Gaseous
The types in the question aren't enforcing type safety, if "x should be of type long". The following will typecheck: Action<string> stringAction = Console.WriteLine; api.Foo(x => ReturnLong()).Bar().FooBar(stringAction);Prochronism
@Gaseous If you ditch the non-generic interfaces you would use the Unit type as the T. The Unit type is the type with only one value, which can be implemented as public struct Unit {}. You'll also notice that the compiler can't discern between overloads of Action and Func<> or between Action<T> and Func<T,>, so you'll want to rename one of the Foos.Prochronism
S
4

Assuming you're able to go from an IFoo<TResult> to an IFoo and your chain of methods does not care about TResult you may be able to save some of the implementing by changing the usage to something like:

api.Foo(x => ReturnLong())
   .Bars(foo=>foo.Bar1() //where foo is an IFoo
                 .Bar2()
                 .Bar3()
                 ...
    )
   .FooBar(x => ...);
Sidero answered 24/7, 2013 at 20:10 Comment(1)
One could do that, however, there is a lot of noise. Although there are many different Bar()-like methods, most of the time the user is going to call exactly one and this would result in .Bars(foo => foo.Bar()) for nothing :/ Thanks for the attempt though -> upvoted.Gaseous
P
2

Get rid of the IFoo interface that doesn't have a type parameter, and add a type parameter to IBar to remember the type from IFoo. Without this, incorrect programs will type-check.

public interface IFluentApi {}

public interface IFoo<T> {}

public interface IBar<T> {}

public struct Unit {}

public static class Extenders
{
    public static IFoo<Unit> Foo(this IFluentApi api, Action action) {return null;}

    public static IFoo<T> Foo<T>(this IFluentApi api, Func<T> func) {return null;}

    public static IBar<T> Bar<T>(this IFoo<T> foo) {return null;}

    public static void FooBar<T>(this IBar<T> bar, Action action) {}

    public static void FooBar<T>(this IBar<T> bar, Action<T> action) {}

    public static void CheckType<T>(this T value) {}
}

public class Examples
{
    public void Example()
    {

        IFluentApi api = null;

        api.Foo(() => "Value")
           .Bar()
           .FooBar(x => x.CheckType<string>()); // x is a string


        api.Foo(() => {})
           .Bar()
           .FooBar(x => x.CheckType<Unit>() ); // x is a Unit

        // The following (correctly) fails to type check
        Action<string> stringAction = Console.WriteLine;
        api.Foo(() => (long) 7)
           .Bar()
           .FooBar(stringAction); // x should be of type long
    }
}
Prochronism answered 9/8, 2013 at 19:59 Comment(0)
S
1

The fluent interface within C# is reliant upon types (explicitly or implicitly) being passed through each .. As you've described, if you lose type information you cannot get it back.

Your only options are to have branches in your expressions, as described in Shawn's answer, or you must have IBar<TResult> Bar<TResult> (this IFoo<TResult> foo) only, so that the type information needed is always passed through.

  • Of course, if some of your .Bars are actually like .First or .SingleOrDefault they shouldn't be followed by .FooBar anyway (at least not directly).
Stadia answered 6/8, 2013 at 13:8 Comment(0)
I
0

Note that generic type must be known at compile time. You can store the instance of Type Class at runtime but you can't use it in place of generic parameter.

You have created two types: IFoo and IFoo<T> : IFoo. However, the IBar class is created by the IFoo which doesn't have the information about its type because it hasn't host any. Thus the type information is lost. We can consider only solutions that are able to infer the type at compile time.


The first solution is known to you - Creating a generic versions of all types that you're using in your invocation chain. That requires much effort.


If you can assume, that the type will not change during the execution, then you can wrap that type and use your methods explicitly:

api.Foo(() => default(long))
   .Bar()
   .FooBar<long>(x => { });

That allows to create later a generic wrapper. It also makes sense, because you must be able to infer the type while coding. If not, then you are unable to use there generics at all.


The third and very flexible approach is to rid of generics in favor of simple objects:

void FooBar(this IBar bar, Action<object> action) { /* ... */ }

.
.
.

api.Foo(() => default(long))
   .Bar()
   .FooBar(x => { }); // <-- That will compile, but x is an object

Note, that FooBar is responsible for shipping the argument for an action. Therefore you can just check at runtime the type of the object that you are dealing with:

.
.
.FooBar(x => { if (x is MyType) { /* ... */ } });

By reflection you are able to obtain all required information about the x.

Inductee answered 9/8, 2013 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.