Identity as As

The identity function is a useful extension method in C#.

The identity function is so trivial. And yet, it is important enough that both F# and Haskell expose this function to all developers under the name id. The standard justification is that id can be a useful argument to a higher-order function. This is certainly true, but it is not the only justification. In C#, I have found reasonable uses for calling id as an extension method.

linkIdentity extension method

Here is how I prefer to define the identity function as an extension method.

GenericExtensions.cs
1linkpublic static class GenericExtensions {

2link public static A As<A>(this A a) => a;

3link}

My intended use case for this method is to explicitly specify the type parameter so that the C# compiler inserts an implicit conversion to A before supplying the result as the argument to this method. This method is an explicit way to call an implicit conversion.

The name As is probably a surprise. Pre-defined implicit conversions and the as operator are both type conversions and neither throws an exception. If all user-defined implicit conversions are properly designed, then the use of this method never causes an exception to be thrown. In fact, it would always succeed. I like the name As because of this similarity and because the resulting code reads so well.

linkExamples

Here are example use cases that I have found for this extension method. The typical alternative is to use a cast expression. I think using a cast expression is worse because it is a code smell (because a cast expression can fail at runtime) and because the readability of the extension method is better.

link1. Ternary operator

The expressions in the branches of a ternary operator must have an implicit conversion from one type to the other. When this not the case, we can use As to make it so.

1linkinterface IPlanet { }

2linkclass Earth : IPlanet { }

3linkclass Mars : IPlanet { }

4linkclass Sandbox {

5link public IPlanet GetPlanet(bool b) =>

6link b ? new Earth() : new Mars().As<IPlanet>();

7link}

This issue looks like it will be fixed in a future version of C#.

In the meantime, an alternative that I sometimes prefer is to replace the ternary operator with an if-else statement so that the compiler can infer both implicit conversions.

1linkpublic IPlanet GetPlanet(bool b) {

2link if (b) {

3link return new Earth();

4link } else {

5link return new Mars();

6link }

7link}

link2. Explicit interface

When a class explicitly implements an interface, one cannot directly call any interface method given a reference with the implementing class as its compile-time type. It is necessary to first invoke the implicit conversion that exists to an interface from an implementing class.

1linkinterface IPlanet {

2link bool HasLife { get; }

3link}

4link

5linkclass Earth : IPlanet {

6link bool IPlanet.HasLife => true;

7link}

8link

9linkclass Sandbox {

10link public bool EarthHasLife =>

11link new Earth().As<IPlanet>().HasLife;

12link}

link3. Hidden member

Members can be hidden in an inheritance hierarchy. This case is similar to the previous case with explicit interfaces. The difference is that removing As in that case causes a compiler error while removing As in this changes the resolved member.

1linkclass Planet {

2link public bool? HasLife => null;

3link}

4link

5linkclass Earth : Planet {

6link public new bool? HasLife => true;

7link}

8link

9linkclass Sandbox {

10link public bool? EarthAsPlanetHasLife =>

11link new Earth().As<Planet>().HasLife;

12link}

Don't use hidden members though. It makes things very confusing.

link4. Ambiguous call

I have only ever experienced this use case once, so I decided to provide that exact example instead of creating a simpler one.

language-ext contains extension methods for safely getting the value out of a dictionary that are mostly the same. The main difference is the type of the first argument. One is for IDictionary<K, V> and the other is for IReadOnlyDictionary<K, V>. Because of that, this is a compiler error.

1linknew Dictionary<int, int>().TryGetValue~~~~~~~~~~~(0);

Error CS0121 The call is ambiguous between the following methods or properties: OutExtensions.TryGetValue<K, V>(IDictionary<K, V>, K) and OutExtensions.TryGetValue<K, V>(IReadOnlyDictionary<K, V>, K)

I dislike so much about this. (Below, I omit the type parameters for brevity.)

First, I think that IReadOnlyDictionary is not a good name. It suggests that implementations must be immutable, but this is not the case. Instead, a better name would be IReadableDictionary. Better still would be to rename IDictionary to IMutableDictionary and rename IReadOnlyDictionary to IDictionary so that the safer type has the preferred name.

Second, IDictionary should extend IReadOnlyDictionary. With this change, I think the compiler would select the extension method for IDictionary. In fact, language-ext could then remove the extension method for IDictionary.

Third, language-ext could solve this problem now by also adding this extension method for Dictionary. Instead, the recommended workaround is to name the argument, which varies accordingly for these two extension methods.

That workaround is fine, but if the argument names were also the same, then calling As is another workaround.

1linknew Dictionary<int, int>().As<IDictionary<int, int>>().TryGetValue(0);

link5. Excessive inference

The C# compiler can infer the type arguments of a method. The C# compiler can infer the need for an implicit conversion. But the C# compiler cannot do too many of these at once.

Consider this function, which also appears on page 186 of Functional Programming in C#. Notice the implicit conversion from R to Option<R> for the value f(t).

1linkpublic Option<R> ApplyInTermsOfBind<T, R>(

2link Option<Func<T, R>> func,

3link Option<T> arg

4link) =>

5link arg.Bind(t => func.Bind<Func<T, R>, R>(f => f(t)));

The type parameters of the inner call to Bind are specified because there would be a compiler error without them.

Error CS1061 Option<Func<T, R>> does not contain a definition for Bind and no accessible extension method Bind accepting a first argument of type Option<Func<T, R>> could be found (are you missing a using directive or an assembly reference?)

If any one type argument of a method cannot be inferred, then all type arguments must be explicitly provided. In contrast, we only have to provide one type parameter for each call to As. Therefore, I typically prefer to help the compiler by explicitly invoking an implicit conversion via As. In this case, instead of specifying two type parameters to Bind, we only need to specify one type parameter to As to invoke the implicit conversion.

As least, that is what I expected. Instead, this code doesn't compile. I do not understand why.

1linkpublic Option<R> ApplyInTermsOfBind<T, R>(

2link Option<Func<T, R>> func,

3link Option<T> arg

4link) =>

5link arg.Bind(t => func.Bind<Func<T, R>, R>(f => f~(~t~).As<Option<R>>()));

Error CS1929 R does not contain a definition for As and the best extension method overload GenericExtensions.As<Option<R>>(Option<R>) requires a receiver of type Option<R>

It does work though when As is called like a static method instead of an extension method. Now we don't have to specify the type parameters to Bind.

1linkpublic Option<R> ApplyInTermsOfBind<T, R>(

2link Option<Func<T, R>> func,

3link Option<T> arg

4link) =>

5link arg.Bind(t => func.Bind(f => GenericExtensions.As<Option<R>>(f(t))));

In the end, the best implementation would be to define a function Some that invokes the implicit conversion from R to Option<R> and then use that.

1linkpublic Option<R> ApplyInTermsOfBind<T, R>(

2link Option<Func<T, R>> func,

3link Option<T> arg

4link) =>

5link arg.Bind(t => func.Bind(f => Some(f(t))));

link6. Type invariance

Added on 2020-07-27

Type variance extends the inheritance hierarchy of a generic type based on the inheritance hierarchy of its type parameters. For example, because IEnumerable<T> is covariant (in T) and because object is a subtype of string, it follows that IEnumerable<object> is a subtype of IEnumerable<string>. Covariance in C# is specified with the out keyword on the type parameter. For example, here it is in the definition for IEnumerable<T>.

1linkpublic interface IEnumerable<out T> : System.Collections.IEnumerable

In contrast, consider the definition of Task<TResult>.

1linkpublic class Task<TResult> : System.Threading.Tasks.Task

It lacks the out keyword (and the in keyword), as every class in C# must (since only interfaces and delegates support them), so Task<TResult> is invariant (in TResult). As a consequence, Task<object> is not a subtype of Task<string>, so this code doesn't compile.

1linkpublic Task<string> taskString = Task.FromResult("");

2linkpublic void Foo() => Bar(taskString~~~~~~~~~~);

3linkpublic void Bar(Task<object> _) { }

Error CS1503 Argument 1: cannot convert from System.Threading.Tasks.Task<string> to System.Threading.Tasks.Task<object>

We can compensate for the lack of covariance using our identity function.

1linkpublic Task<string> taskString = Task.FromResult("");

2linkpublic void Foo() => Bar(taskString.ContinueWith(x => x.As<object>()));

3linkpublic void Bar(Task<object> _) { }

I also encounter this situation with language-ext when using invariant types like Option<T>.

linkSummary

The identity function is a useful extension method in C# because it can help the compiler figure out the intended types in situations that would otherwise be too complicated and cause a compiler error.


The code in this post is available here.

linkTags

The tags feature of Coding Blog Plugin is still being developed. Eventually the tags will link somewhere.

#Suggestion #functional_programming #CSharp

linkComments

Identity extension methodExamples1. Ternary operator2. Explicit interface3. Hidden member4. Ambiguous call5. Excessive inference6. Type invarianceSummaryTagsComments

Home About Archive


Feeds