Posted
over 15 years
ago
by
Support at Lokad
|
Posted
over 15 years
ago
by
Support at Lokad
|
Posted
over 15 years
ago
by
Support at Lokad
|
Posted
over 15 years
ago
by
Geva Perry
|
Posted
over 15 years
ago
by
Support at Lokad
|
Posted
over 15 years
ago
by
Support at Lokad
|
Posted
over 15 years
ago
by
Support at Lokad
|
Posted
over 15 years
ago
by
Support at Lokad
|
Posted
over 15 years
ago
by
Rinat Abdullin
One of the things I like about software development is a simple fact, that efficiently developed code, block or software, takes much more time to document, than to deliver. This efficiency could be based on certain practices, constraints and
... [More]
principles, followed by discipline. These items allow to take away small bits of complexity and development friction, allowing us to concentrate on the things that really matter and generate business value. Let's talk about one of this small things - a really simple construct in C# called Maybe (of the monad fame) that had completely freed me from all cases of NullReferenceException in my code, while making it cleaner. Sounds interesting? Let's have a look at the C# code and .NET usage patterns. Introduction Maybe<T>, as the simplest monad implementation in .NET, is a generic class that can either hold value of type T or be empty. There is a little bit of helper syntax and some usage guidelines and patterns. In this article we'll cover all of these; links for the sources in C# and binaries would be provided, too. Here's how the declaration of the Maybe class starts in C#: [Serializable, Immutable]
public sealed class Maybe<T> : IEquatable<Maybe<T>>
{ readonly T _value; readonly bool _hasValue; Maybe(T item, bool hasValue) { _value = item; _hasValue = hasValue; } /// <summary> Gets the underlying value, if it is available </summary> /// <value>The value.</value> public T Value { get { if (!_hasValue) throw Errors.InvalidOperation( ResultResources.Dont_access_value_when_maybe_is_empty); return _value; } } internal Maybe(T value) : this(value, true) { // ReSharper disable CompareNonConstrainedGenericWithNull if (value == null) throw new ArgumentNullException("value"); // ReSharper restore CompareNonConstrainedGenericWithNull } /// <summary> Gets a value indicating whether this instance /// has value. </summary> /// <value><c>true</c> if this instance has value; otherwise, /// <c>false</c>.</value> public bool HasValue { get { return _hasValue; } } /// <summary> /// Default empty instance. /// </summary> public static readonly Maybe<T> Empty = new Maybe<T>(default(T), false);
There are a few implicit operators and helpers methods that improve the usage experience (we'll talk about them later), but the simple code above is the core of the approach. Primary monad operations could be expressed in this simple snippet: // function that MIGHT return a number
Maybe<int> DetectNumber(string number)
{ switch (number) { case "Zero": // we are allowed an implicit conversion // so here we return a number directly return 0; case "One": return 1; case "Two": return 2; default: // empty singleton for missing value return Maybe<int>.Empty; }
} // Actually getting a Monad in C#
var number = DetectNumber("Zero"); // prints "True"
Console.WriteLine(number.HasValue);
// prints "0"
Console.WriteLine(number.Value); var number2 = DetectNumber("Hm..");
// prints "False"
Console.WriteLine(number2.HasValue);
// throws InvalidOperationException
Console.WriteLine(number2.Value);
So basically this Maybe monad is extremely simple class that can either be empty or have value. If it is empty, and we attempt to access value, then the exception is thrown. In other words, that's a strongly-typed reference to an object that acts like null with some syntactic flavor. How is it even better than using nullable references and getting NullReferenceException when you forget to check for null? Here are the reasons justifying the usage of Maybe{T} for me: Reason 1: Good Citizenship By using Maybe{T} in my code I promise that all the code will be following good citizenship principles and will keep all object references non-null (initialized to some value from the default). This promise is to be uphold by a self-discipline and unit tests. This helps to avoid all null references completely. Reason 2: Explicit behavior and clean code If there is a potentially nullable reference, parameter or a function return result, then it should be explicitly marked with the Maybe{T}, exposing its behaviour to the reader and forcing him to explicitly handle the states. Additionally, using Maybe{T} allows us to avoid throwing weird exceptions or returning nulls in situations, where result could be undefined. Consider declaration of the method like this: /// <summary> Parses an encoded string into
/// authentication information. </summary>
/// <param name="encodedString">Encoded string.</param>
/// <returns>authentication information</returns>
/// <exception cref="ArgumentNullException">when string is null</exception>
/// <exception cref="InvalidOperationException">when string is invalid</exception>
AuthInfo ParseStringToAuthInfo(string encodedString);
This method declaration is really similar to a lot of existing methods from the .NET BCL (i.e.: int.Parse as the simplest one) and it shares a lot of excessive noise and inherent problems. Most importantly we are throwing some exceptions, when the input is not expected and could not be handled gracefully. This:
blows the code execution and must be handled by a separate try-catch;
requires code consumer to read the documentation instead of reading the method signature;
does not prevent the code from potentially breaking in every single place, should we refactor and replace InvalidOperationException by InvalidAuthDataException.
Let's rewrite the method signature using the Maybe monad style: /// <summary> Attempts to parse an encoded string into /// authentication information. </summary>
/// <param name="encodedString">Encoded string.</param>
/// <returns>authentication information</returns>
static Maybe<AuthInfo> ParseStringToAuthInfo(string encodedString)
With this refactoring we:
decoupled ourselves from the ArgumentNullException, since we believe that input will never be null. Method body would live without the appropriate null check as well;
decoupled ourselves from the InvalidOperationException and use optional value, that would be valid only if the parsing succeeded;
no longer require the developer using the method to look in the code documentation in order to understand what exactly the method works;
do not need defensive and noisy try-catch programming around the calls to this method (or anywhere in the call stack).
Of course, there are other refactoring alternatives to the signature: // Option1 : use bool
static bool TryParseString(string encodedString, out AuthInfo); // Option 2: return value that could be null
static AuthInfo ParseString(string encodedString);
First option uses the TryGetValue approach you can see on the IDictionary interface. This approach resorts to the out keyword (which I always avoid), breaks the functional style of the programming (method should always have a single value being returned) and adds some noise to the using code. Also AuthInfo is returned as undefined, if the parsing fails, violating the good citizenship. Second option returns value that would be null in case the parsing fails. Developer, in order to discover this, has to read the documentation. And he has to protect himself from the possible problems with null checks against the result. In my opinion, method using the Maybe monad is the most clean approach of them all. Reason 3: Chaining and pipeline syntax Ok, so we have our Maybe-powered method signature: static Maybe<AuthInfo> ParseStringToAuthInfo(string encodedString);
Let's actually see the full power of the concept in C# on a sample taken fresh from the production code. Consider the method: // simple pipeline sequence
Maybe<AuthPrincipal> LoadPrincipal(string key)
{ return ParseStringToAuthInfo(key) .Combine(u => _service.BuildAccountSession(u)) .Convert(sa => new AuthPrincipal(sa));
}
In this LINQ-style and purely functional sequence of 3 lines, we:
Attempt to parse the key into AuthInfo.
If the attempt was successful, we use some service to try to build an account session out of the parsed AuthInfo. BuildAccountSession operation might also fail, by the way. This is indicated in the method signature (by using Maybe) and would happen if the provided AuthInfo didn't have a match in the repository.
If we were successful in parsing the original string and in building the account session, then we create a new AuthPrincipal class using this session; otherwise an empty Maybe{AuthPrincipal} is returned to indicate the composite failure.
As you can see, detailed method description takes a bit more space, than the actual implementation. And the implementation itself is less fragile and smaller compared to traditional approach with nulls, throwing and handling various exceptions. After getting used to this benefit of Maybe, dealing with the code that has exceptions and nulls feels like working in Visual Studio without ReSharper installed. All the benefits listed above come from two classes (that contain less code, than this article) and some discipline. That's the way of Zen. If you want to try Maybe{T}, the code is available in Lokad.Shared.dll from the Lokad Shared Libraries. You can either copy this class to your solution (while keeping an eye on the project for any improvements and changes in the guidelines) or grab the latest download. Let's continue our article by outlining common operations available for Maybe{T}. Creating Maybe // implicit conversion is the most common way to create Maybe<T>
// mostly it happens when we return actual value from within a
// method that returns maybe.
// Or, when we pass value into the optional function parameter
Maybe<int> maybeInt = 10; // creates a maybe object when implicit casting is not available // for some reason (i.e.: anonymous types or failing type inferrence)
var nonEmptyMaybe = Maybe.From(value); // each type has only a single empty maybe instance that is
// cached via the type-caching
var emptyAuthInfo = Maybe<AuthInfo>.Empty;
Getting Value // tests if the Maybe has value
var hasValue = maybeInt.HasValue; // returns value behind the Maybe, throwing exception if it is empty
// this approach is rarely used (because of the pipeline operators)
var value = maybeInt.Value // get's value from maybe, using some default, if it is empty
var value = maybeUsername.GetValue("[email protected]"); // get's value from maybe, executing default generation if it is empty
// handy for scenarios, when generating default values is CPU expensive
var value = maybeUsername.GetValue(() => ComposeName());
Pipeline Operations // get Maybe that either has value derived from the parent Maybe
// or is empty, if the parent is empty
var maybeUsername = maybeIdentity.Convert(i => i.Username); // gets actual value that is derived from the parent Maybe, if it is not
// empty, or uses a predefined default // equivalent of Convert.GetValue
var username = maybeIdentity.Convert(i => i.Username,"[email protected]");
var username = maybeIdentity.Convert(i => i.Username,() => ComposeName()); // combines result of the two Maybe's, either of which could be empty
var maybeFullName = maybeUsername.Combine(k => LookupFullName(k)); // executes the action against the value, if Maybe is not empty,
// returns the original Maybe to continue pipelining maybeFullName.Apply(fn => Console.WriteLine(fn)) // executes the action, if maybe is Empty
// returns the original Maybe to continue pipelining
maybeFullName.Handle(() => Application.Terminate());
In addition to the operators above, Maybe{T} class also defines proper equality and hashing operations. It is debugger-friendly, too. However, this does not end the list of reasons of using Maybe for the efficient development. Reason 4: Simplified unit testing Using Maybe monad in .NET also has the benefit of allowing to introduce simplified DSL syntax for unit-testing code leveraging the approach. Here are the core extensions you need (they are pipelined together for the brevity) // extensions checking different aspects of valid Maybe<T> monad
maybeValue .ShouldPass() .ShouldPassCheck(i => i == 10) .ShouldPassWith(10) .ShouldBe(10) .ShouldBe(true); // checking empty Maybe Moned
MaybeNot .ShouldFail() .ShouldBe(false);
Each extension, basically throws a descriptive assertion exception, if the condition is not met. Here's how the unit test might look like with one of these extensions: [Test]
public void System_info_is_disabled()
{ WhenViewIsShown.SetResult(DialogResult.OK); Subject.BindModel(RandClientModels.NextFeedbackView()); Implementation.CheckSystemInfo(false); Subject .GetModel() .ShouldPassCheck(m => m.SystemInformation == "");
}
If the check fails, because the maybe has a value but this value is invalid, then the failure would look like: FailedAssertException: Expression should be true: '(m.SystemInformation = "")'.
These test extensions are located within Lokad.Testing.dll, which is also a part of Lokad Shared Libraries within a Lokad.Testing namespace. This separation is done in order to prevent IntelliSense from being polluted with all the Should statements, while we are working in the production code. Testing code, on the contrary can have access to these routines (which is done by referencing the testing assembly and using the namespace). Reason 5: Initialization of Immutable classes All we know that in F# it is extremely easy to create an immutable object from another record by providing a few override values in the cloning process: let conn2 = { conn1 with Service = MyService }
If we are using only C#-style immutable classes as models in our application (which is beneficial for a number of reasons, but is a large topic on its own), then we are limited to the constructor initialization only, which could be rather messy: // model definition
[Immutable, Serializable]
public sealed class ServiceConnection
{ public readonly string Username; public readonly string Password; public readonly string AuthMode; public readonly Uri Service; public ServiceConnection(string username, string password, string authMode, Uri service) { Username = username; Password = password; AuthMode = authMode; Service = service; }
} // cloning with a custom service reference
var conn2 = new ServiceConnection(conn1.Username, conn2.Password, conn2.AuthMode, MyService);
More fields the model has, more messy cloning the process becomes. Fortunately, in the upcoming C# 4.0 we will be able to do everything more efficiently and with different combinations: var conn2 = conn1.With(service: MyService);
var conn3 = conn1.With(username: Anonymous, AuthType: Plain);
This could be achieved with a single method by using C# 4.0 Optional Parameters and Maybe{T}: public ServiceConnection With( string username = Maybe.String, string password = Maybe.String, string authType = Maybe.String, Uri service = Maybe.Uri) { return new ServiceConnection( username.GetValue(Username), password.GetValue(Password), authType.GetValue(AuthType), service.GetValue(Service); }
where Maybe.String (and its Uri analogue) is just a short-cut for Maybe{string}.Empty that is already declared on the Maybe non-generic helper class in Lokad.Shared.dll. Caveats This approach, as any other piece of knowledge from the xLim body of knowledge, might be controversial and not applicable for you projects, development practices and beliefs. All that I can say - it works for me, allowing slightly more efficient delivery, evolution and maintenance of .NET development projects ranging from business analytics servers, RESTful NoSql automation engines and up to Smart Client applications with reusable MVC infrastructure shards. This limited improvement in development efficiency sometimes requires using approaches and principles that may be somewhat incompatible with the traditional ones (MS .NET or Alt.NET), while continuously enforcing them across the codebases. But in the end, I think, it's worth it, just like Continuous Integration or Inversion of Control. But again, that's the way of Zen - everybody has his own way to satori. Questions and Answers Question: Why add another needless layer of complexity for such a simple task? Just learn how to use the null coalescing operator (??) and/or inline if-thens. Console.WriteLine(number != null ? "True" : "False");
Console.WriteLine(number ?? "Null Value");
Imagine a large real-world project that spawns multiple components and tiers, while facing problems slightly more challenging, than writing output to the console. If developers know that this project uses Maybe to declare potentially empty variables, then they are completely freed from checking for nulls and even thinking about that. That's one less thing to bother, making quite a difference in complex scenarios (and human being can simultaneously bother only about 7 things max, anyway). This actual usage of Maybe{T} might happen in just one or two methods of the project. However, when developers use them, they will know immediately (and not from the documentation) and will be forced to handle them because of the syntax. Things like that reduce development friction and allow complex project to be delivered efficiently and with really small teams. Question: How is this different from Nullable{T} in C#? Only value types are supported by Nullable generic (or its shorthand syntax). So you can't use a class variable in a nullable type (which makes sense, since the reference itself can be null). Question: Why do Lokad Shared Libraries feature null checks, although Maybe{T} is declared in the code? Because Lokad.Shared.dll, as a really lightweight library, might be used in various projects. Some of these projects might not have "no nulls returned and expected" policy. So it is logical to protect shared methods from the nulls explicitly. in order to avoid the unpredictable behavior. Question: How do I use Maybe{T} if method can fail because of the multiple reasons and I want to know them? Maybe{T} on its own is not designed to handle such situations. These are the primary logically-complete solutions for this situation:
You can return Maybe{T} while printing multiple failure details to the IScope passed as an argument. That's how Lokad Rules are working. It also works well in long-running complex (especially asynchronous) operations that require logging and detailed failure control.
You can use Result{TValue,TError} monad returning strongly-typed error result (i.e.: enumeration value) or it's generic counterpart Result{TValue} (if the strong-typing is not possible for some reason).
You can always use good old exceptions, if the above options do not suite you for some reason and you are OK with try-cath and breaking the functional code flow.
Question: Does Maybe work with validation rules? Yes, Maybe{T} works with Lokad validation and business rules. You can also use MaybeIs helper class to compose strongly-typed rules on top of the Maybe{T} monad. MaybeIs has two static methods:
MaybeIs.EmptyOr{T}(params Rule{T}[] rules) - composes validation rule that succeeds only if the Maybe{T} is empty or the underlying value passes additional rules.
MaybeIs.ValidAnd{T}(params Rule{T}[] rules) - composes validation rule that succeeds if the Maybe{T} is not empty and the underlying value passes additional rules.
Sample usage in composing more complex rule: public static void ModelIsValid(FeedbackModel model, IScope scope)
{ scope.Validate(() => model.Sender, MaybeIs.EmptyOr<string>(StringIs.ValidEmail)); scope.Validate(() => model.Model, ModelIsValid);
}
Question: So essentially you are replacing "optional != null" with "optional.HasValue". Not exactly. HasValue is rarely checked against directly. Take a look at the piece of authentication code below. There are two possibly failing functions in it, that return Maybe{T}: Maybe<AuthPrincipal> LoadPrincipal(string key)
{ return ParseStringToAuthInfo(key) .Combine(u => _service.BuildAccountSession(u)) .Convert(sa => new AuthPrincipal(sa));
}
Signatures are like: static Maybe<AuthInfo> ParseStringToAuthInfo(string encodedString);
Maybe<AccountSession> BuildAccountSession(AuthInfo authInformation);
HasValue and Value aren't mentioned in this code. That's how it usually works. Question: What about operator overloading: public static implicit operator T(Maybe<T> mayBe)
{ return mayBe.Value; //or mayBe._value;
} public static implicit operator bool(Maybe<T> mayBe)
{ return mayBe.HasValue;
}
This kind of implicit conversion from Maybe directly to value/state is not advised for two reasons:
It looses the intent of using Maybe{T} to force explicit handling of nullable situations.
Implicit conversion from Maybe to value is logically incorrect as it might return default(T) value or result in exception (depending on implementation).
Basically such an implicit operation would feel somewhat similar to the old C++ joke from bash.org: #define TRUE false
#define FALSE true
// and good luck with debugging))
Side note: Implicit conversion from value to Maybe{T} or from error/value to Result{TValue,TError}, as opposed to the case above, does not violate this logic. This is because we are not forcing collapse of possibility of multiple states into one known state (while have side effects if the state is not what we expect). This is why conversion from value to maybe is implicit, while the opposite has to be done either via the Value property or via the explicit conversion. Summary Let's wrap the things up. In this article we've talked about Maybe{T} generic class that, as a simple monad concept, allows in .NET to:
promote Good citizenship in the code avoiding nulls and exceptions;
write clean code with explicit behavior;
use expressive LINQ-style syntax with chaining and pipeline operators;
benefit from the simplified unit testing syntax;
initialize immutable classes efficiently in C# 4.0.
C# source files and compiled binaries featuring Maybe monad and testing extensions could be downloaded from the Lokad Shared Libraries Open Source project. It is important to note, that the current design of Maybe{T} and development practices around it are based on the ongoing R&D process and production usage cases. As such, they might gradually evolve and improve further (this being reflected in Lokad Shared Libraries). This article is also considered to be an essential part of xLim 3 development body of knowledge. If you are interested in more articles on Zen Development practices, you can stay tuned by subscribing to this blog via RSS. Next article I'm considering for these series is about Result variations of the monads in C#, that allow handling more complex failure scenarios, than a simple Maybe (BTW, the previous Zen article was about Method-level IoC). All comments, thoughts and any other feedback are welcome and appreciated.
[Less]
|
Posted
over 15 years
ago
by
Joannes Vermorel
|