-
Notifications
You must be signed in to change notification settings - Fork 17
A Look at Functor
We'd like to define a composable functional mapping interface that works for any container object, such as the following C# interface:
interface Functor<A> {
Functor<B> fmap<B>( Func<A,B> f);
}
This interface will allow us to compose several successive .fmap() calls, however, no matter what type we call .fmap()
on, we get back a Functor<B>
, which means we lost access to the original object's real type and capabilities. What we want is the ability to call .fmap()
on a List<A>
and get back a List<B>
, or call .fmap()
on a SortedList<A>
and get back a SortedList<B>
. This is how fmap works in Haskell.
The type relationship we want can be specified in C#, but the syntax becomes much more verbose and explicit than we'd like.
interface Functor<A> {
C fmap<B,C>(Func<A, B> f) where C : Functor<B>;
}
class MyList<A> : List<A>, Functor<A> {
public C fmap<B,C>(Func<A,B> f) where C : Functor<B>, new() {
var list = new C();
foreach (var elem in this) {
list.Add(f(elem));
}
return list;
}
}
class Program
{
static void Main(string[] args) {
MyList<int> alist = new MyList<int>{ 1, 2, 3};
MyList<string> blist =
alist.fmap<string,MyList<string>>(
x => { return x.ToString(); }
);
}
}
Here you can see we're able to call fmap(Func<int,string>)
on List<int>
and get back List<string>
. However, in order to do it we have to list the type parameters of fmap
explicitly...every time. This is not the convenient functional composability we were looking for. Further, whether we like it or not, what we're actually doing is explicitly telling fmap()
what type of output container we would like it to create for us.