Click here to Skip to main content
15,867,704 members
Articles / Programming Languages / C#

MONADs in C#

Rate me:
Please Sign up or sign in to vote.
2.81/5 (15 votes)
4 Sep 2015CPOL3 min read 24.1K   7   15
What a MONAD is and how they impact your C# code

Introduction

MONADs are the cornerstone to LINQ, but few people know what they are and even fewer know how they work. It's possible to use MONADs in ways that destroy performance, so this tip is going to share how they work and how you can avoid that.

Background

Before reading this tip, you should be familiar with:

  • Collections
  • IEnumerable
  • IQueryable
  • LINQ
  • Enumerators

What is a MONAD?

A MONAD as defined by Wikipedia is a structure that represents computations defined as sequences of steps: a type with a monad structure defines what it means to chain operations, or nest functions of that type together.

Why Should I Care?

So what's the big deal and who cares if I know what the definition is? Well the problem is that if you don't understand how a MONAD executes versus how traditional lines of code execute, you risk disastrous performance effects. LINQ builds MONADs so you might not have realized it, but you're probably using MONADs all the time.

When you build a MONAD, the code doesn't actually execute until you call a finalizer (not sure if this is the actual term) that causes the MONAD to evaluate. Some common finalizers are Count(), First(), Last(), and ToList().

At first, this could sound like a good thing. Less code executing is better, right? Well... Not always... Let's say my MONAD includes some sort of intensive service call to return a result set of items. If I have five places that use the Count() of the result set is going to call that intensive service call five times!

So How Disastrous is Disastrous?

Below is a sample app you can run to see how bad this can get:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MONAD
{
    class Program
    {
        static void Main(string[] args)
        {
            var people = new List<Person>()
            {
                new Person("Alan"),
                new Person("Kiersten"),
                new Person("Bryan"),
                new Person("Madeline"),
                new Person("Jessica"),
                new Person("Mabel"),
                new Person("Diane"),
                new Person("David")
            };

            Console.WriteLine("Creating MONADs");

            var sortedPeople = people.OrderBy(p => 
                { 
                    Console.WriteLine("Ordering for " + p.Name); 
                    return p.Name; 
                });

            var sortedPeopleNotStartingWithD = sortedPeople.Where(p => 
                { 
                    Console.WriteLine("Checking start for " + p.Name); 
                    return !p.Name.StartsWith("D"); 
                });

            Console.WriteLine("Starting loop");

            for (int i = 0; i < sortedPeopleNotStartingWithD.Count(); ++i)
            {
                var personToPrint = sortedPeopleNotStartingWithD.ElementAt(i);

                Console.WriteLine("Retrieved " + personToPrint.Name);
            }

            Console.WriteLine("Finished loop, press any key to continue...");
            Console.ReadKey();
        }

        public class Person
        {
            public Person(string name)
            {
                Name = name;
            }

            public string Name { get; set; }
        }
    }
}

Here's the output:

Creating MONADs
Starting loop
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Checking start for Mabel
Checking start for Madeline
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Retrieved Alan
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Checking start for Mabel
Checking start for Madeline
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Retrieved Bryan
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Checking start for Mabel
Checking start for Madeline
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Retrieved Jessica
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Checking start for Mabel
Checking start for Madeline
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Retrieved Kiersten
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Checking start for Mabel
Checking start for Madeline
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Checking start for Mabel
Retrieved Mabel
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Checking start for Mabel
Checking start for Madeline
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Checking start for Mabel
Checking start for Madeline
Retrieved Madeline
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Checking start for Mabel
Checking start for Madeline
Finished loop, press any key to continue...

The reason why you get so many excessive calls is that in a MONAD, it's evaluated on demand and there is nothing that I'm doing in that sample Console application to push the results to a physical list.

Notice how "Starting loop" is called before any elements are ordered? That's because the first time the MONAD is demanded to be evaluated is when I call Count(). Since neither sortedPeople nor sortedPeopleNotStartingWithD are evaluated, I have to call each of those. And the next time the loop has an iteration, guess what? They're still evaluated on demand so everything gets run again.

If I add one little ToList() to push the results to a physical list as such:

C#
var sortedPeopleNotStartingWithD = sortedPeople.Where(p =>
    {
        Console.WriteLine("Checking start for " + p.Name);
        return !p.Name.StartsWith("D");
    })
    .ToList();

Here's what you get:

Creating MONADs
Ordering for Alan
Ordering for Kiersten
Ordering for Bryan
Ordering for Madeline
Ordering for Jessica
Ordering for Mabel
Ordering for Diane
Ordering for David
Checking start for Alan
Checking start for Bryan
Checking start for David
Checking start for Diane
Checking start for Jessica
Checking start for Kiersten
Checking start for Mabel
Checking start for Madeline
Starting loop
Retrieved Alan
Retrieved Bryan
Retrieved Jessica
Retrieved Kiersten
Retrieved Mabel
Retrieved Madeline
Finished loop, press any key to continue...

The General Rules

Because of how disastrous an expensive call can get if left in a MONAD, I always return an evaulated collection (i.e., List<T>, Dictionary<TKey, TValue>) instead of a MONAD (i.e. IEnumerable<T>, IGrouping<TKey, TSource>) for expensive calls.

It's not optimal to evaluate everything all the time though. There are cases where you can build a MONAD but it never gets consumed, so you save if you didn't evaluate it. LINQ will also try to optimize the calls so it can be beneficial to leave it unevaluated until completely built.

If I have the time, I'll generally try to optimize it accordingly, but I default to finalizing everything.

Any() versus Count() > 0

So these are two LINQ extension methods commonly used, but do you know which one you should use? If you didn't notice in the output calls to Count() functions by incrementing a count as it enumerates the entire collection. After all, Count() is an extension method for IEnumerables.

Any() simply needs to iterate the first item. Once it verifies, it reaches an item or not, you have your result - much more efficient.

Obviously, if you need the actual count like in the code example iterator, you can't replace it with a call to Any(). Just be aware of how it's working though so you can determine what you can do to improve performance when you need to.

Points of Interest

Seriously folks, I have seen applications destory performance with MONAD service and database calls. Be aware of how each LINQ method operates so you can avoid these situations.

History

  • 2015-09-04: Celebrating I wrote about MONADs because no one ever asks about MONADs

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
United States United States
More than fourteen years of experience programming with six years of WPF application development. Dedicated business application developer with large focus on performance tuning for multithreaded applications. Experience developing for small scale applications and large scale distributed systems at senior and architect levels. Experience developing for a wide range of industry applications to include artificial lift, baggage tracking, distribution, customer relation management, and commodities.

Comments and Discussions

 
Questionit is your examples that create the multiple de-referencing of the IEnumerables Pin
BillWoodruff18-Sep-15 4:23
professionalBillWoodruff18-Sep-15 4:23 
AnswerRe: it is your examples that create the multiple de-referencing of the IEnumerables Pin
PureNsanity18-Sep-15 4:40
professionalPureNsanity18-Sep-15 4:40 
GeneralRe: it is your examples that create the multiple de-referencing of the IEnumerables Pin
BillWoodruff18-Sep-15 6:57
professionalBillWoodruff18-Sep-15 6:57 
GeneralRe: it is your examples that create the multiple de-referencing of the IEnumerables Pin
PureNsanity18-Sep-15 7:31
professionalPureNsanity18-Sep-15 7:31 
GeneralRe: it is your examples that create the multiple de-referencing of the IEnumerables Pin
BillWoodruff19-Sep-15 4:10
professionalBillWoodruff19-Sep-15 4:10 
GeneralRe: it is your examples that create the multiple de-referencing of the IEnumerables Pin
PureNsanity19-Sep-15 5:02
professionalPureNsanity19-Sep-15 5:02 
GeneralMy vote of 5 Pin
IrfanRaza7-Sep-15 9:18
IrfanRaza7-Sep-15 9:18 
GeneralRe: My vote of 5 Pin
PureNsanity8-Sep-15 4:11
professionalPureNsanity8-Sep-15 4:11 
General[My vote of 2] Finalize everything is often a poor choice when using databases Pin
John Brett6-Sep-15 21:01
John Brett6-Sep-15 21:01 
GeneralRe: [My vote of 2] Finalize everything is often a poor choice when using databases Pin
PureNsanity8-Sep-15 3:59
professionalPureNsanity8-Sep-15 3:59 
GeneralRe: [My vote of 2] Finalize everything is often a poor choice when using databases Pin
John Brett8-Sep-15 4:07
John Brett8-Sep-15 4:07 
GeneralRe: [My vote of 2] Finalize everything is often a poor choice when using databases Pin
PureNsanity8-Sep-15 4:28
professionalPureNsanity8-Sep-15 4:28 
General[My vote of 2] Really wrong example Pin
Alexandr Stefek4-Sep-15 21:16
Alexandr Stefek4-Sep-15 21:16 
GeneralRe: [My vote of 2] Really wrong example Pin
PureNsanity5-Sep-15 5:12
professionalPureNsanity5-Sep-15 5:12 
SuggestionRe: [My vote of 2] Really wrong example Pin
InvisibleMedia5-Sep-15 13:48
professionalInvisibleMedia5-Sep-15 13:48 
My little contribution about this post...

Yes, it's tricky to write code with a better performance...But, I would tell you that IEnumerator<> works with :

1) foreach
2) yield return

The second purpose is to enumerate something like an asynchronous call (but, it's really not asynchronous).
In your example, you are sorting the list. I would tell you that a ComboxBox control has also a sort method...but if you set DataSource before to set the Sorted property to True, you will get an exception thrown.

Exactly, .NET uses itself to work at the best performance.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.