Motivation
Ever dreamed about putting all your exception handling or logging stuff at a single place? Imagine the maintenance problems it can solve. How to do it? Use a monad.Note: Same code is shown in both images and text. Output is shown only in image.
What is a Monad?
A monad is a wrapper. What is a wrapper? A wrapper is a class that has an object of another class. What is the difference between an ordinary class and a monad? A monad has one special function, a higher-order function. What is a higher-order function? A higher-order function is a function that can take in argument a function. What do the higher-order function do with this input-function? It apply it on the object that the monad is containing. Here is the code:public class LoggerMonad{ public T Value { get; private set; } public LoggerMonad(T value) { Value = value; } public LoggerMonad Bind (Func func) { Console.WriteLine(func.Method.Name + " is called at " + DateTime.Now.ToString()); return func(Value).ToLoggerMonad(); } public void Bind(Action action) { Console.WriteLine(action.Method.Name + " is called at " + DateTime.Now.ToString()); action(Value); } } public static class LoggerMonadExtensionMethods { public static LoggerMonad ToLoggerMonad (this T value) { return new LoggerMonad (value); } }
Explanation
Lets go through it very slowly. You have a class called LoggerMonad. This class is your monad. It can wrap itself around any object, means it can contain an object of any type whatsoever. This class has a constructor which do the initialization stuff. Like all good constructors it do only one thing, initialize fields. There is one higher-order function too, called Bind which is overloaded twice. We will talk about it later. First a quick overview of generics.Generics
We are using c# generics here. LoggerMonad is a generic class. A generic class is any class that contains a generic field or a generic method. A generic field is a field whose type is not specified inside the class, its specified elsewhere in code. Its still a static type means at the time of compilation the type would be known. Its not a dynamic type means the type is not unclear until runtime. C# is static-typed language which means that it do not allow dynamic types, uptil version 3.5. A generic method is a method that takes in argument or return a variable of generic type.How To Use This Class?
int i = 1;LoggerMonad monad = new LoggerMonad
Further Explanation
You not have to use the monad this way. The above code is just a demonstration of how generics work. LoggerMonad can contain a field of any type, what type? the type that is specified when the constructor is called. You can use any type, means you are not limited to integer or numeric types. Note that we do not need to specify type above when calling constructor. Its because c# compiler is intelligent enough to infer the type of "i" variable itself.Higher-Order Functions
Now, lets talk about the higher-order function. Its overloaded and its generic. The first one, which takes in input a "func", is to be called when you want to return something from your input-function. The second one, which takes in input an "action", is to be called when you don't want to return anything from your input-function. Note that the main point here is what you want to return from the input-function, not the Bind function. You can say that the Bind function is a wrapper around your input-function, so its like a method-level wrapping.The Input-Function
The input-function itself can return a thing of any type. We have only one Bind method for it, so we have to return a general type of thing. Its because we don't want to have a separate Bind function for all types in c#, including your custom types. How do we do it? Remember that our LoggerMonad can contain an object of any type, so we simply return a LoggerMonad. That solves the problem. Note that our LoggerMonad has a public get around its field T, therefore the client-code can easily access the underlying value that is returned by the input-function.Extension Methods
The last part of the listing is an extension method. Since its an extension method so you can put it in any static class of your code. I personally prefer to make a separate class for each type and put the related extension methods there. Note that the argument contains a "this", which means that the method ToLoggerMonad is an extension method. The method simply wraps the input value in a new LoggerMonad object and return that object.public class Program { public static void Main(string[] args) { string sText = "North Pole is 20,000 km away from the South Pole at any direction except top."; int iBeginAt = 0; int iEndAt = 10; string sCountry = "Pakistan"; Listlist = new List ; int iNumber = 105; TestSubstring(sText, iBeginAt, iEndAt); TestCount(sText); TestStartsWith(sText, sCountry); TestAddInList(list, iNumber); Console.Read(); } public static void TestSubstring(string sText, int iBeginAt, int iEndAt) { Func funcSubstring = s => s.Substring(iBeginAt, iEndAt); string sSubstring = sText.Substring(iBeginAt, iEndAt); Console.WriteLine("sSubstring = " + sSubstring); string sSubstring_Monad = sText.ToLoggerMonad().Bind (funcSubstring).Value; Console.WriteLine("sSubstring_Monad = " + sSubstring_Monad); } public static void TestCount(string sText) { Func funcCount = s => s.Count(); int iCount = sText.Count(); Console.WriteLine("iCount = " + iCount); int iCount_Monad = sText.ToLoggerMonad().Bind (funcCount).Value; Console.WriteLine("iCount_Monad = " + iCount_Monad); } }
Explanation of Client-Code
The above is our client-code. Client-code is always the code that uses the library. Here, our library is all the code in the first image, that is, the LoggerMonad class and the extension method.The Four Methods In Client-Code
Our client-code calls four methods. Inside each of these methods, a call is made to an instance method directly, that is, without using the monad, and then the same call is made using the monad. You only have to see one of these methods to see the comparison. So, if I have an object called "sText", whatever its type is, and i have a method called "Substring" inside that object, then I can simply call it this way: "sText.Substring(argument1, argument2);" where argument1 and argument2 are any two objects of integer type. Its a simple call but it has following maintenance problems:- Most of the time the objects we are using are of library classes whose code we cannot modify. So we cannot put anything inside the methods we are calling.
- If we want to do something, such as logging, before we call a method, then wherever in our code we have called that method we have to write the logging code there. A copy-paste can do that, but later on, if we want to change the logging related code then we have to go through all of these places and change code there. Even that could be done. What can never be done is change that code after you have ship your library. Its a scenario where you have made a library of your own, deployed that library on computers of your customers, then make a client-code that uses that library, so far so good. But later on if you want to change your client-code without touching your library code then you cannot do that in anyway.
- You may simply forget to insert the logging code at some places or make some mistake so that the logging code is called differently at different places.
Its better to keep all the logging code at one place so you only have to change one class if ever a change requirement comes. Note that whatever is said above about logging goes also for exception handling or whatever stuff you want to do before or after execution of your methods.
The First Client-Code Method In Detail
Lets go through first of the methods in our client code, the TestSubstring method. Compare the call at line no. 65 with the call at line no. 68. The first call need not be explained because its the way you actually do your callings now, the new thing is the second way of calling. What we have done is that we have first converted our object "sText" into a LoggerMonad by calling the extension method. Once its a LoggerMonad it can access the Bind method. The overloaded Bind method we are interested here is the one that returns a value. Take a quick look at line no. 21 (image 1) to see the signature of the method.The first thing to note is that it takes a function in argument, not an action. The difference in two is that a function always returns a value while an action never. The function, that is, the input-function is a generic function. Its last type "R" is the return type. This gives our flexibility to return any type from our function. We are not limited to returning the same type of the wrapped-field inside the monad. This is actually different than the classic use of monads in functional languages such as haskell, and its a technique I invented. It do not deviate us from the essence of monads, because its an enhancement, not a replacement.
We do have to tell the return type. That is what "
The Bind method applies the function it takes in input on the field the class is containing. This is what the line "func(value)" is doing. We are taking in input a function and applying that function on the "value". Now, here is where it becomes tricky, and this is the thing which is stinging in your object-oriented brain since you have seen the code in the first image. What is going on?
A Look Back At Procedural Programming
Lets go back to the simple world of procedural programming, where there are no objects. Since you are an object-oriented programming so you are supposed to be already an expert in procedural programming, therefore you should understand the basic concept I am going to refer now. In the procedural programming term, when we say "apply a function on a variable v" then we mean that we pass the variable v as an input argument to the function. That is not what we mean here, in the higher-order function sense. When we say that Bind applies the input-function to the field "value" we do not mean "apply" in the same sense as in procedural programming.Applying A Function
What do we mean then? We mean this: "call the function of the object". The arguments are not mentioned here, in this code. The code "func(value)" of line no. 24 is same as the code "value.func()". Let it sink in your brain. We are not passing value to the func, we are applying func on value, we are calling func of value. We are calling a method called func which is inside value. We are calling value to call its method func. What parameters do func takes then? See line no. 68.sText.ToLoggerMonad().Bind
The Bind Function
Bind is taking in input a function called funcSubstring and then internally calling it on the object the monad is containing. Lets not talk about the return value now. Take a look at what funcSubstring is, line no. 63, listed below too:Func
s.Substring(iBeginAt, iEndAt);
We have defined a function here. The function itself takes two arguments "iBeginAt" and "iEndAt". Once inside the Bind the "s" is replaced by the wrapped-field Value. What finally happens is this "sText.Substring(iBeginAt, iEndAt)".
It looks like an unnecessarily long way of calling a method. Instead of doing this:
sText.Substring(iBeginAt, iEndAt)
we have to do this:
Func
sText.ToLoggerMonad().Bind
Why Use Such A Long Code?
It means instead of one simple line we uses two lines. Why? We do not really have to give a name to the input-function. We can very well do this:
sText.ToLoggerMonad().Bind
We gave a name to the input-function because we are doing logging here and we want to know the name of the function that is called. See output below:
Drawbacks of Monads
At the end, our calling do gets longer, instead of "sText.Substring(iBeginAt, iEndAt)" we have to do "sText.ToLoggerMonad().BindWhere Is The Logging Code Anyways?
See line no. 23. Here we are logging at screen. You can put your code here to log in a database or file or whatever.The Second Overload of Bind Method
We have only one thing left to talk about now. Its the second overload of the Bind method, the one that takes an "action" argument. Its to be called when the input-function do not returns anything. We have to use overload here because c# don't allow "R" to be void in the first overload. See line no. 27. Note that the Action is of type "T", we not have any return type so there is no "R" there. The Bind itself is of return type void. In the client-code we need not worry about which Bind to call because the c# compiler can handle it itself. See line no. 101 below./* code continues in class Program */ public static void TestCount(string sText) { FuncfuncCount = s => s.Count(); int iCount = sText.Count(); Console.WriteLine("iCount = " + iCount); int iCount_Monad = sText.ToLoggerMonad().Bind (funcCount).Value; Console.WriteLine("iCount_Monad = " + iCount_Monad); } public static void TestStartsWith(string sText, string sCountry) { Func funcStartsWith = s => s.StartsWith(sCountry); bool isStartsWith = sText.StartsWith(sCountry); Console.WriteLine("isStartsWith = " + isStartsWith); bool isStartsWith_Monad = sCountry.ToLoggerMonad().Bind (funcsStartsWith).Value; Console.WriteLine("isStartsWith_Monad = " + isStartsWith_Monad); } public static void TestAddInList(List list, int iNumber) { Action > actionAddInList = l => l.Add(iNumber); list.Add(70); Console.WriteLine(list[0].ToString()); list.ToLoggerMonad().Bind(actionAddInList); Console.WriteLine(list[0].ToString() + " " + list[1].ToString()); }
We pass on the action actionAddToList to the Bind method, which overload of Bind is not our concern. Ofcourse this could be simplified as following:
list.ToLoggerMonad().Bind(l => l.Add(iNumber));
You can enclose your calling of function or action in the overloads of Bind inside try catch and put your exception-handling logic there if you want to have an ExceptionHandlerMonad instead of a logger monad. You can have both by putting both of these logics in the Bind method.
Anonymouse Function
Lets take a closer look at "s => s.Substring(iBeginAt, iEndAt)". This is body of function. This function has no name, so its an anonymous function. This is the input-function we are supplying to the Bind function. Remember that Bind is a higher-order function so it can take in input a function. It can also return a function but we do not need that functionality in any monad so we not use it.
What do the above line means? It means that this function requires an argument called "s". We need not worry about the type of "s", that is specified and handled somewhere else in our code. If there are multiple arguments then we have to separate them by commas and use a bracket, such as "(s, m)" instead of "s". In a monad we apply the input-function on the wrapped-value as explained earlier, see line no. 24 and 30 for a quick reference. It means we only have to supply one argument, the wrapped-value. In all monads, the input-function, the function that is anonymous, always takes only one argument.
What do this input-function returns? It returns whatever the last line in it returns. Here it returns whatever "s.Substring(...)" returns. The return type could be anything, but whatever it is, it can be converted to a LoggerMonad, therefore our Bind method has to have only one return type, "LoggerMonad
The above is not actually true. The input-function may not return anything at all, means it can have a "void" return type. In that case we use the other overload of Bind explained earlier.
The Entire Code
I haven't shown code in sequence above, for better focus. Following is the complete code with output:
No comments:
Post a Comment