7
\$\begingroup\$

I am learning about monadic structures and have created a sort of monad list that implements the map and binding "operators" (or technically methods) in C#.

I understand that Linq already exists and would solve many of the problems that my solution solves (I am in fact already using linq to help in parts of my code) and that lists are technically already monads. However this is still a learning experience for me and so I want to make sure I have the right idea.

class ListMonad<T> : IEnumerable<T>
{
    private readonly List<T> items;
    private List<T> Items => items;

    public ListMonad(List<T> value)
    {
        this.items = new List<T>(value);
    }

    public ListMonad()
    {
        items = new List<T> { };
    }

    public ListMonad<TO> Bind<TO>(Func<T, ListMonad<TO>> func)
    {
        var lists = new List<ListMonad<TO>>();
        foreach(var el in Items)
            lists.Add(func(el));

        return new ListMonad<TO>(lists.SelectMany(x => x.Items).ToList());
    }

    public ListMonad<TO> Map<TO>(Func<T, TO> func)
    {
        var result = new ListMonad<TO>();
        foreach(var item in Items)
            result.Items.Add(func(item));

        return result;
    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        foreach(var item in Items)
            yield return item;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable<T>)this).GetEnumerator();
    }

    public T this[int index] => Items[index];
}

Use:

class Program
{
    static void Main(string[] args)
    {
        var listMonad = new ListMonad<int>(new List<int> { 1, 2, 3, 4, 5, 6, 7 });

        var doubledAndSquared = Compose((int x) => x * x, (int x) => x * 2);
        Func<int, ListMonad<int>> zeroSeparated = x => new ListMonad<int>(new List<int> { x, 0 });

        listMonad = listMonad
            .Bind(zeroSeparated)
            .Map(doubledAndSquared);

        foreach(var item in listMonad)
            Console.Write(item + ", ");
    }

    static Func<V, U> Compose<T, U, V>(Func<T, U> func1, Func<V, T> func2) =>
        (V x) => func1(func2(x));
}
\$\endgroup\$
4
\$\begingroup\$

Just a few points that I've noticed:

  • Readonly property can be declared directly like this:

    private List<T> Items { get; }
    
  • It is not clear if this class is intended to be immutable. The Items is not exposed directly, and its backing field is marked with readonly. However, the indexer is exposed. In case it is, you should use ReadOnlyCollection<T> as the container. If not, an T[] would allow you to mutate its element, but prevents you from adding/removing item.

  • ctor argument value should pluralized.
  • An overload of ctor that takes params T[] values would be a nice addition, enabling this type of code: new ListMonad<int>(1, 2, 3)
  • Another ctor overload that support IEnumerable<T> would be great too, which will allow you to write this:

    // this likely defeats the purpose of this class?
    public ListMonad<TO> Map<TO>(Func<T, TO> func) => new ListMonad<TO>(this.Select(func));
    public ListMonad<TO> Bind<TO>(Func<T, ListMonad<TO>> func) => new ListMonad<TO>(this.SelectMany(func));
    
  • In the Bind method, you can use AddRange to avoid creating a nested list:

    public ListMonad<TO> Bind<TO>(Func<T, ListMonad<TO>> func)
    {
        var results = new List<TO>();
        foreach (var element in this)
            results.AddRange(func(element));
    
        return new ListMonad<TO>(results);
    }
    
  • The argument order of Compose method seems weird, although it may just be my personal preferences. I would suggest to put the inner-most selector first, and moving toward outside, since this is how we normally solves equations:

    public static Func<TInput, TResult> Compose<TInput, T2, TResult>(Func<TInput, T2> func1, Func<T2, TResult> func2)
    {
        return x => func2(func1(x));
    }
    
  • Compose can also be placed in a generic-ally typed static class to avoid having to explicitly mentioning the generic type multiple times:

    public static class FuncHelper<T>
    {
        // usage: FuncHelper<int>.Compose(x => x * 2, x => x * x)
        public static Func<T, TResult> Compose<T2, TResult>(Func<T, T2> func1, Func<T2, TResult> func2)
        {
            return x => func2(func1(x));
        }
        // and, you can chain even more function
        public static Func<T, TResult> Compose<T2, T3, TResult>(Func<T, T2> func1, Func<T2, T3> func2, Func<T3, TResult> func3)
        {
            return x => func3(func2(func1(x)));
        }
        // and, so on ...
    
        // or, you can chain as long as you want, if the input, intermediate and result types are all the same
        // usage: FuncHelper<int>.Compose(x => x + 1, x => x * 2, x => x + 3, x => x * 4, ...) 
        public static Func<T, T> Compose(params Func<T, T>[] funcs)
        {
            return x => funcs.Aggregate(x, (y, f) => f(y));
        }
    }
    
\$\endgroup\$
  • \$\begingroup\$ Thank you for your insight. Come to think of it, it's kind of ridiculous I'm not already allowing an IEnumerable to be used as a constructor argument. \$\endgroup\$ – NaN Oct 6 '18 at 12:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.