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.
Here is how I prefer to define the identity function as an extension method.
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.
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.
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}
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}
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.
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>()./*~err~*/TryGetValue~~~~~~~~~~~/*~err~*/(0);
Error CS0121 The call is ambiguous between the following methods or properties:
OutExtensions.TryGetValue<K, V>(IDictionary<K, V>, K)
andOutExtensions.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);
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 forBind
and no accessible extension methodBind
accepting a first argument of typeOption<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 => /*~err~*/f~(~t~/*~err~*/).As<Option<R>>()));
Error CS1929
R
does not contain a definition forAs
and the best extension method overloadGenericExtensions.As<Option<R>>(Option<R>)
requires a receiver of typeOption<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))));
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(/*~err~*/taskString~~~~~~~~~~/*~err~*/);
3linkpublic void Bar(Task<object> _) { }
Error CS1503 Argument 1: cannot convert from
System.Threading.Tasks.Task<string>
toSystem.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>
.
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.
The tags feature of Coding Blog Plugin is still being developed. Eventually the tags will link somewhere.
#Suggestion
#functional_programming
#CSharp