From df64fb7a8bef29bb173a58afa945eefd636bb186 Mon Sep 17 00:00:00 2001 From: Bart Sokol Date: Fri, 15 Sep 2017 22:46:35 +0200 Subject: [PATCH] Docs update and editorconfig/cleanup --- .editorconfig | 9 +++ Monacs.Core/ErrorDetails.cs | 2 +- Monacs.Core/Option.cs | 6 +- Monacs.Core/Result.cs | 6 +- Monacs.UnitTests/ErrorDetailsTests.fs | 6 +- Monacs.UnitTests/OptionTests.fs | 24 +++---- Monacs.UnitTests/ResultTests.fs | 28 ++++---- docs/GettingStarted.md | 36 ++++++++++ docs/Index.md | 3 + docs/Option.md | 100 +++++++++++++++++++++++++- docs/Result.md | 3 + 11 files changed, 185 insertions(+), 38 deletions(-) create mode 100644 .editorconfig create mode 100644 docs/GettingStarted.md create mode 100644 docs/Result.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4044ccd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false \ No newline at end of file diff --git a/Monacs.Core/ErrorDetails.cs b/Monacs.Core/ErrorDetails.cs index bd6d07c..214e901 100644 --- a/Monacs.Core/ErrorDetails.cs +++ b/Monacs.Core/ErrorDetails.cs @@ -35,7 +35,7 @@ public static class Errors { public static ErrorDetails Trace(string message = null, string key = null, Exception exception = null) => new ErrorDetails(ErrorLevel.Trace, message.ToOption(), key.ToOption(), exception.ToOption()); - + public static ErrorDetails Debug(string message = null, string key = null, Exception exception = null) => new ErrorDetails(ErrorLevel.Debug, message.ToOption(), key.ToOption(), exception.ToOption()); diff --git a/Monacs.Core/Option.cs b/Monacs.Core/Option.cs index 1628948..01075af 100644 --- a/Monacs.Core/Option.cs +++ b/Monacs.Core/Option.cs @@ -70,7 +70,7 @@ public static Option OfString(string value) => public static T2 Match(this Option option, Func some, Func none) => option.IsSome ? some(option.Value) : none(); - + public static T2 MatchTo(this Option option, T2 some, T2 none) => option.IsSome ? some : none; @@ -91,7 +91,7 @@ public static Option Map(this Option option, Func mapper public static T2 GetOrDefault(this Option option, Func getter, T2 whenNone = default(T2)) => option.IsSome ? getter(option.Value) : whenNone; - + /* Side Effects */ public static Option Do(this Option option, Action action) @@ -112,7 +112,7 @@ public static Option DoWhenNone(this Option option, Action action) public static IEnumerable Choose(this IEnumerable> items) => items.Where(i => i.IsSome).Select(i => i.Value); - + public static Option> Sequence(this IEnumerable> items) => items.Any(i => i.IsNone) ? None>() diff --git a/Monacs.Core/Result.cs b/Monacs.Core/Result.cs index 6adaa0b..e617cd8 100644 --- a/Monacs.Core/Result.cs +++ b/Monacs.Core/Result.cs @@ -81,7 +81,7 @@ public static Result OfString(string value, ErrorDetails error) => public static T2 Match(this Result result, Func ok, Func error) => result.IsOk ? ok(result.Value) : error(result.Error); - + public static T2 MatchTo(this Result result, T2 ok, T2 error) => result.IsOk ? ok : error; @@ -102,7 +102,7 @@ public static Result Map(this Result result, Func mapper public static T2 GetOrDefault(this Result result, Func getter, T2 whenError = default(T2)) => result.IsOk ? getter(result.Value) : whenError; - + /* Side Effects */ public static Result Do(this Result result, Action action) @@ -123,7 +123,7 @@ public static Result DoWhenError(this Result result, Action Choose(this IEnumerable> items) => items.Where(i => i.IsOk).Select(i => i.Value); - + public static Result> Sequence(this IEnumerable> items) => items.Any(i => i.IsError) ? Error>(items.First(i => i.IsError).Error) diff --git a/Monacs.UnitTests/ErrorDetailsTests.fs b/Monacs.UnitTests/ErrorDetailsTests.fs index b84c9bd..5d5dbbd 100644 --- a/Monacs.UnitTests/ErrorDetailsTests.fs +++ b/Monacs.UnitTests/ErrorDetailsTests.fs @@ -29,7 +29,7 @@ module Constructors = details.Message |> should equal (Option.Some(message)) details.Key |> should equal (Option.Some(key)) details.Exception |> should equal (Option.Some(ex)) - + [] let ``Info sets ErrorLevel.Info and error details`` () = let message = "Message" @@ -40,7 +40,7 @@ module Constructors = details.Message |> should equal (Option.Some(message)) details.Key |> should equal (Option.Some(key)) details.Exception |> should equal (Option.Some(ex)) - + [] let ``Warn sets ErrorLevel.Warn and error details`` () = let message = "Message" @@ -72,4 +72,4 @@ module Constructors = details.Level |> should equal ErrorLevel.Fatal details.Message |> should equal (Option.Some(message)) details.Key |> should equal (Option.Some(key)) - details.Exception |> should equal (Option.Some(ex)) \ No newline at end of file + details.Exception |> should equal (Option.Some(ex)) \ No newline at end of file diff --git a/Monacs.UnitTests/OptionTests.fs b/Monacs.UnitTests/OptionTests.fs index 7c2acb4..6392094 100644 --- a/Monacs.UnitTests/OptionTests.fs +++ b/Monacs.UnitTests/OptionTests.fs @@ -39,7 +39,7 @@ module Converters = let ``OfObject returns Some when value is not null`` () = let object = obj() Option.OfObject(object) |> should equal (Option.Some(object)) - + [] let ``OfNullable returns None when value is null`` () = let empty = new Nullable() @@ -49,11 +49,11 @@ module Converters = let ``OfNullable returns Some when value is not null`` () = let value = Nullable(42) Option.OfNullable(value) |> should equal (Option.Some(value.Value)) - + [] let ``OfString returns None when value is null`` () = Option.OfString(null) |> should equal (Option.None()) - + [] let ``OfString returns None when value is empty`` () = Option.OfString("") |> should equal (Option.None()) @@ -70,19 +70,19 @@ module Match = let value = Option.None() let expected = "test" Option.Match(value, some = (fun _ -> ""), none = (fun () -> expected)) |> should equal expected - + [] let ``Match returns result of some when value is Some`` () = let value = Option.Some(42) let expected = "test" Option.Match(value, some = (fun _ -> expected), none = (fun () -> "")) |> should equal expected - + [] let ``MatchTo returns result of none when value is None`` () = let value = Option.None() let expected = "test" Option.MatchTo(value, some = "", none = expected) |> should equal expected - + [] let ``MatchTo returns result of some when value is Some`` () = let value = Option.Some(42) @@ -102,7 +102,7 @@ module Bind = let value = Option.None() let expected = Option.None() Option.Bind(value, (fun x -> Option.Some(x.ToString()))) |> should equal expected - + module Map = [] @@ -118,7 +118,7 @@ module Map = Option.Map(value, (fun x -> x.ToString())) |> should equal expected module GetOrDefault = - + [] let ``GetOrDefault returns encapsulated value when value is Some`` () = let value = Option.Some("test") @@ -136,7 +136,7 @@ module GetOrDefault = let value = Option.None() let expected = "test" Option.GetOrDefault(value, whenNone = expected) |> should equal expected - + [] let ``GetOrDefault returns getter result when value is Some`` () = let value = Option.Some((1, "test")) @@ -172,7 +172,7 @@ module ``Side effects`` = let mutable result = expected Option.Do(value, (fun x -> result <- "fail")) |> should equal value result |> should equal expected - + [] let ``DoWhenNone returns value and doesn't execute action when value is Some`` () = let value = Option.Some(42) @@ -196,7 +196,7 @@ module Collections = let values = seq { yield Option.Some(42); yield Option.None(); yield Option.Some(123) } let expected = [| 42; 123 |] Option.Choose(values) |> Seq.toArray |> should equal expected - + [] let ``Sequence returns collection of values wrapped into Option when all items are not None`` () = let values = seq { yield Option.Some(42); yield Option.Some(123) } @@ -204,7 +204,7 @@ module Collections = let result = Option.Sequence(values) result.IsSome |> should equal true result.Value |> Seq.toArray |> should equal expected - + [] let ``Sequence returns None> when any item is None`` () = let values = seq { yield Option.Some(42); yield Option.Some(123); yield Option.None() } diff --git a/Monacs.UnitTests/ResultTests.fs b/Monacs.UnitTests/ResultTests.fs index a881ee7..e30bba7 100644 --- a/Monacs.UnitTests/ResultTests.fs +++ b/Monacs.UnitTests/ResultTests.fs @@ -18,13 +18,13 @@ module ``Constructors and equality`` = let ``Ok doesn't equal Ok when the Value is not equal`` () = Result.Ok(42) = Result.Ok(13) |> should equal false Result.Ok(42) <> Result.Ok(13) |> should equal true - + [] let ``Error equals Error when the Error is equal`` () = let error = Errors.Error() Result.Error(error) = Result.Error(error) |> should equal true Result.Error(error) <> Result.Error(error) |> should equal false - + [] let ``Error doesn't equal Error when the Error is not equal`` () = let error1 = Errors.Error("test1") @@ -50,7 +50,7 @@ module Converters = let ``OfObject returns Ok when value is not null`` () = let object = obj() Result.OfObject(object, Errors.Error()) |> should equal (Result.Ok(object)) - + [] let ``OfNullable returns Error when value is null`` () = let empty = new Nullable() @@ -61,12 +61,12 @@ module Converters = let ``OfNullable returns Ok when value is not null`` () = let value = Nullable(42) Result.OfNullable(value, Errors.Error()) |> should equal (Result.Ok(value.Value)) - + [] let ``OfString returns Error when value is null`` () = let error = Errors.Error() Result.OfString(null, error) |> should equal (Result.Error(error)) - + [] let ``OfString returns Error when value is empty`` () = let error = Errors.Error() @@ -85,19 +85,19 @@ module Match = let error = Errors.Error(expected) let value = Result.Error(error) Result.Match(value, ok = (fun _ -> ""), error = (fun e -> e.Message.Value)) |> should equal expected - + [] let ``Match returns result of ok when value is Ok`` () = let value = Result.Ok(42) let expected = "test" Result.Match(value, ok = (fun _ -> expected), error = (fun _ -> "")) |> should equal expected - + [] let ``MatchTo returns result of error when value is Error`` () = let expected = "test" let value = Result.Error(Errors.Error()) Result.MatchTo(value, ok = "", error = expected) |> should equal expected - + [] let ``MatchTo returns result of ok when value is Ok`` () = let value = Result.Ok(42) @@ -118,7 +118,7 @@ module Bind = let value = Result.Error(error) let expected = Result.Error(error) Result.Bind(value, (fun x -> Result.Ok(x.ToString()))) |> should equal expected - + module Map = [] @@ -135,7 +135,7 @@ module Map = Result.Map(value, (fun x -> x.ToString())) |> should equal expected module GetOrDefault = - + [] let ``GetOrDefault returns encapsulated value when value is Ok`` () = let value = Result.Ok("test") @@ -153,7 +153,7 @@ module GetOrDefault = let value = Result.Error(Errors.Error()) let expected = "test" Result.GetOrDefault(value, whenError = expected) |> should equal expected - + [] let ``GetOrDefault returns getter result when value is Ok`` () = let value = Result.Ok((1, "test")) @@ -189,7 +189,7 @@ module ``Side effects`` = let mutable result = expected Result.Do(value, (fun x -> result <- "fail")) |> should equal value result |> should equal expected - + [] let ``DoWhenError returns value and doesn't execute action when value is Ok`` () = let value = Result.Ok(42) @@ -213,7 +213,7 @@ module Collections = let values = seq { yield Result.Ok(42); yield Result.Error(Errors.Error()); yield Result.Ok(123) } let expected = [| 42; 123 |] Result.Choose(values) |> Seq.toArray |> should equal expected - + [] let ``Sequence returns collection of values wrapped into Result when all items are not Error`` () = let values = seq { yield Result.Ok(42); yield Result.Ok(123) } @@ -221,7 +221,7 @@ module Collections = let result = Result.Sequence(values) result.IsOk |> should equal true result.Value |> Seq.toArray |> should equal expected - + [] let ``Sequence returns Error> with first error when any item is Error`` () = let error = Errors.Error("error!") diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md new file mode 100644 index 0000000..d19f338 --- /dev/null +++ b/docs/GettingStarted.md @@ -0,0 +1,36 @@ +# Getting started + +Monacs is a library that provides a set of types and functions that can be used to substantialy change the approach you use to write your C# code. And while it won't change object-oriented language into fully-featured functional language, it gives you oportunity to use some of the FP concepts in your C# code today. + +To fully leverage the potential of this library, you'll need to get familiar with a few simple concepts. Once you get through them, everything about this library should be pretty obvious. + +## Don't fear the monad +The M Word. It's been a topic of countless discussions, jokes and even flamewars. You can look at the monad from many perspectives, but the perspective this library encourages is pretty simple: Monad is a combination of a type (e.g. Option) and a collection of functions around this type (like Map and Bind). + +Actually, as a .NET developer you've probably used at least a couple of monads. `IEnumerable` with LINQ is actually a monad. TPL and async/await workflow is a monad as well. And if you had chance to use Reactive Extensions and `IObservable` type, then yes, it's a proper monad too. + +You may find quite a few similarities between LINQ and what you'll find in this library, although the naming is a bit different. For example, both LINQ and Rx use `Select()` name for the function that makes a projection of an encapsulated value to the value of (potentially) other type. In Monacs you'll find it under the name `Map()`, which is quite common across FP languages. + +## Higher order functions +This is another complicated name for a pretty simple concept. Higher order function is just a function which takes another function as a parameter. And again, if you've used LINQ, TPL or Rx you're probably quite familiar with this idea and I think it doesn't require more explanation. + +## Value types +As you may know, there are two kinds of types in C# - reference types, such as `System.Object` or any class you write, and value types, such as `System.Int32` or enums. One particular thing that is very often underestimated by developers is the ability to create your own value types - structs. Apart from memory management differences, the key distinction between struct and class is the default value - the problematic null in reference type is substituted by the default value you create when designing a struct. And while you don't want to go and replace all your classes with structs, there are certain places where it can make a huge difference to not have to deal with (implicit) null. + +## The power of extension methods +One of the most important features of C# that allowed to build this library is extension methods. Having the possibility to extend any type with additional methods from virtually any place gives us the flexibility to build modular fluent APIs around simple types. + +## Leveraging `using static` imports +There is one particular feature of C# language that can significantly reduce the amount of code you have to write when you use the same static class multiple times. Take a look at an example: + + namespace Monacs.Samples + { + using System; + + public class Sample1 + { + TODO + } + } + +TODO \ No newline at end of file diff --git a/docs/Index.md b/docs/Index.md index cb39143..d332c3d 100644 --- a/docs/Index.md +++ b/docs/Index.md @@ -1,4 +1,7 @@ # Monacs - documentation +- **Intro** + - [Getting Started](GettingStarted.md) - **Types** - [Option\](Option.md) + - [Result\](Result.md) diff --git a/docs/Option.md b/docs/Option.md index b5fe858..83e170b 100644 --- a/docs/Option.md +++ b/docs/Option.md @@ -1,3 +1,99 @@ -# Monacs.Core.Option +# Monacs.Core.Option\ -Option type is an implementation of Maybe monad. It wraps value that can be not set, in similar way as Nullable wraps value types. \ No newline at end of file +`Option` type is an implementation of Maybe monad. It has two possible states - default one representing absence of value (`None`) and the other one representing presence of value (`Some`). `Option` wraps value that can be absent, in similar way as `Nullable` wraps value types. The are core differences though. While `Nullable` only wraps structs (value types), `Option` works with all kinds of types, making things more consistent. The value representing absence of data in `Nullable` is the same as for reference types - both use `null` for that purpose. With `Option` you don't really care what is the value representing absence of data, because it's the property of the type itself. + +## When to use it + +The main use case of `Option` type is modeling properties of data structures which are not required to create given structure. Let's say we want to build expense tracking system and need to model Expense enity. To make things simple, we want it to contain 3 properties: category, amount and optional notes. We may start with creating it like this: + + public class Expense + { + public ExpenseCategory Category { get; set; } + public decimal Amount { get; set; } + public string Notes { get; set; } + } + +This design has quite a few flaws, one of them is the fact that we don't state explicitly which properties are required and which are optional. Let's make a simple change: + + public class Expense + { + public ExpenseCategory Category { get; set; } + public decimal Amount { get; set; } + public Option Notes { get; set; } + } + +Now we explicitly say that Notes can be present or not. That not only applies to the structure of the class, but also to the usage. + + public string GetExpenseNotesUppercase(Expense expense) => + expense.Notes.ToUpper(); + +The problem with the code above is that it will crash badly with `NullReferenceException` if the Notes field is null. Of course we can fix that: + + public string GetExpenseNotesUppercase(Expense expense) => + expense.Notes?.ToUpper(); + +Not much more complicated, but we only postponed the the issue to the caller - we'll return `null` if the value was `null` in the first place. Of course we could make it return some proper value each time: + + public string GetExpenseNotesUppercase(Expense expense) => + expense.Notes?.ToUpper() ?? string.Empty; + +But we get to the same issue that we had with our data structure - we don't state if the return value will always be present. Consumers of this method still need to consider `null` case - just in case you decide to change the internal implementation details. + +Now let's see how it can look with `Option` type: + + public Option GetExpenseNotesUppercase(Expense expense) => + expense.Notes.Map(s => s.ToUpper()); + +Note that signature of the function has changed, and now we say explicitly that you may not get the value. Consumers will need to handle it directly, making it harder to get unexpected errors. + +One other benefit is that you can have now consistent way of checking if the value is there - just by checking `IsSome` or `IsNone` property of the option. If you, for example, would like to hide the Notes field in the UI, you've got it for free. But it doesn't stop there. + +## How to use it + +Having one type to wrap all the optional values gives you the power to extend things easily. If you've ever used LINQ, you should already know how convenient it can be. Actually the `Option` type has the same superpowers as `IEnumerable`, allowing you to chain calls as long as your inputs and outputs are options (fluent APIs, anyone?). Take a look at this example. Let's say that we want to extract the list of hashtags present in the `Notes` field mentioned above, and also make all of them lowercase. + + public Option> GetHashtags(string s) {...} + + public Option> GetNotesHashtags(Expense expense) => + expense.Notes + .Map(s => s.ToLower()) + .Bind(GetHashtags); + .Map(x => x.ToList()); + +Above code is actually equivalent to this: + + public Option> GetNotesHashtags(Expense expense) + { + if (expense.Notes.IsNone) + return Option.None>(); + var lowercaseNotes = expense.Notes.Value.ToLower(); + var hashtags = lowercaseNotes.GetHashtags(s); + if (hashtags.IsNone) + return Option.None>(); + return hashtags.ToList(); + } + +Not only there is less code to write and read in the first example, but it also reduces the need for naming things. + +You can also reimplement it without option at all, for example like this: + + public List GetNotesHashtags(Expense expense) + { + if (string.IsNullOrEmpty(expense.Notes)) + return null; + var lowercaseNotes = expense.Notes.ToLower(); + var hashtags = lowercaseNotes.GetHashtags(s); + return hashtags?.ToList(); + } + +This seems like a good option, especially with null propagation operator in C# being very convenient, but one thing you have to remember is that it doesn't solve the problem of null. It only moves the responsibility of handling it from one piece of code to another. + +As you can see, option type has quite a few benefits: + +- explicity - you state in a very obvious way that something may be not present +- brevity - with just a few helpers you can make code much more concise +- extensibility - the same simple helpers build powerful fluent API +- safety - you substantialy reduce the risk of having `NullReferenceException` +- versality - the same type represents the lack of value for structs and classes + +When you start using it, you realize that it is a sweet spot between convenience of simple null propagation and complexity of multi-level null handling. \ No newline at end of file diff --git a/docs/Result.md b/docs/Result.md new file mode 100644 index 0000000..fd61f1d --- /dev/null +++ b/docs/Result.md @@ -0,0 +1,3 @@ +# Monacs.Core.Result\ + +TODO \ No newline at end of file