Reactive functor by Mark Seemann
IObservable<T> is (also) a functor.
This article is an instalment in an article series about functors. While the previous articles showed, in great detail, how to turn various classes into functors, this article mostly serves as a place-holder. The purpose is only to point out that you don't have to create all functors yourself. Sometimes, they come as part of a reusable library.
Rx is such a library, and IObservable<T>
is a functor. (It's also a monad, but this is not a monad tutorial; it's a functor tutorial.) Reactive Extensions define a Select
method for IObservable<T>
, so if source
is an IObservable<int>
, you can translate it to IObservable<string>
like this:
IObservable<string> dest = source.Select(i => i.ToString());
Since the Select
method is, indeed, called Select
and has the signature
public static IObservable<TResult> Select<TSource, TResult>( this IObservable<TSource> source, Func<TSource, TResult> selector)
you can also use C#'s query syntax:
IObservable<string> dest = from i in source select i.ToString();
In both of the above examples, I've explicitly declared the type of dest
instead of using the var
keyword. There's no practical reason to do this; I only did it to make the type clear to you.
First functor law #
The Select
method obeys the first functor law. As usual, it's proper computer-science work to actually prove that, but you can write some tests to demonstrate the first functor law for the IObservable<T>
interface. In this article, you'll see a few parametrised tests written with xUnit.net. Here's one that demonstrates that the first functor law holds:
[Theory] [InlineData(-101)] [InlineData(-1)] [InlineData(0)] [InlineData(1)] [InlineData(42)] [InlineData(1337)] public async Task ObservableObeysFirstFunctorLaw(int value) { var sut = new[] { value }.ToObservable(); IObservable<int> projected = sut.Select(x => x); var actual = await projected.FirstAsync(); Assert.Equal(value, actual); }
Here, I chose to implement the identity function as an anonymous lambda expression. In contrast, in a previous article, I explicitly declared a function variable and called it id
. Those two ways to express the identity function are equivalent.
As always, I'd like to emphasise that this test doesn't prove that IObservable<T>
obeys the first functor law. It only demonstrates that the law holds for those six examples.
Second functor law #
Like the above example, you can also write a parametrised test that demonstrates that IObservable<T>
obeys the second functor law:
[Theory] [InlineData(-101)] [InlineData(-1)] [InlineData(0)] [InlineData(1)] [InlineData(42)] [InlineData(1337)] public async Task ObservableObeysSecondFunctorLaw(int value) { string g(int i) => i.ToString(); string f(string s) => new string(s.Reverse().ToArray()); var sut = new[] { value }.ToObservable(); IObservable<string> projected = sut.Select(i => f(g(i))); var actual = await projected.FirstAsync(); var expected = await sut.Select(g).Select(f).FirstAsync(); Assert.Equal(expected, actual); }
This test defines two local functions, f
and g
. Instead of explicitly declaring the functions as Func
variables, this test uses a (relatively) new C# feature called local functions.
Again, while the test doesn't prove anything, it demonstrates that for the six test cases, it doesn't matter if you project the observable in one or two steps.
Summary #
The point of this article is mostly that functors are commonplace. While you may discover them in your own code, they may also come in a reusable library. If you already know C# LINQ based off IEnumerable<T>
, much of Rx will be easy for you to learn. After all, it's the same abstraction, and familiar abstractions make code readable.
Next: The Identity functor.