Giter Club home page Giter Club logo

bearded.monads's Introduction

Bearded.Monads

Publish Bearded.Monads

Monads for use in C#. These include implementations of SelectMany (aka. bind) so you can use C#s fluent linq syntax.

Currently provides Option and Either, as they are useful for error checking, as well as task.

Also provides applicative instances for Task and Maybe.

Installation

Bearded.Monads is available from NuGet:

Install-Package Bearded.Monads

Then, just add using Bearded.Monads; to the top of your C# source file. There is also a Bearded.Monads.Syntax module that you can reference using using static Bearded.Monads.Syntax; with your other using statements.

Option

An obvious implementation which provides various helper methods. This allows you to avoid passing back null from methods and perfoming some special logic. By using Option, you enable the compiler to check that you've handled the missing case.

Usage

As an academic example, consider a trivial method that might have looked thusly without Option:

public bool TryParseInt(string input, out int output)
{
    if(int.TryParse(input, out int i))
    {
        output = i;
        return true;
    }
    output = default
    return false;
}

Can now be transformed into something a little more sane:

public Option<int> TryParseInt(string input)
{
    if(int.TryParse(input, out int i))
    {
        // Note the implicit operator that converts the `A` to an `Option<A>`
        return i;
    }
    return Option<int>.None;
}

As a more realistic example, imagine loading something from a database:

public MyEntity LoadEntityBy(int id)
{
    // Assume Dapper is being used...
    using(var connection = new IDbConnection(_connectionString))
    {
        string sql = "SELECT * FROM Entity WHERE Id = @Id";
        var result = connection.Query<DbEntity>(sql, new { Id = id });
        
        if(result.Any())
        {
            return _mapper.From(result.First()).To(new MyEntity());
        }
        return null;
    }
}

Becomes:

public Option<MyEntity> LoadEntityBy(int id)
{
    // Assume Dapper is being used...
    using(var connection = new IDbConnection(_connectionString))
    {
        string sql = "SELECT * FROM Entity WHERE Id = @Id";
        var result = connection.Query<DbEntity>(sql, new { Id = id });
        
        if(result.Any())
        {
            return _mapper.From(result.First()).To(new MyEntity());
        }
        return Option<MyEntity>.None;
    }
}

This doesn't seem to add much, but if you compose them:

public void LoadEntity(string fromPossibleId)
{
    var maybeEntity = from id in TryParseInt(fromPossibleId)
                        from entity in LoadEntityBy(id)
                        select entity;
    // This will shortcircuit if none of these work.
}

Either

This has been renamed from EitherSuccessOrFailure, and takes it's "success" value as the first type parameter.

This was done as Either "short circuits" on a Left in haskell, but this seems a little unnatural from C#. Please raise an issue if you don't believe this is the case.

Usage

This is useful to return some error condition from a long chain of Either results (or even via the AsEither when dealing with Option results).

For example, if I have the following chain of optional results:

public Option<ResultFromTertiaryService> LoadFromAMultitudeOfServices(string value)
{
    return from id in TryParseInt(value)
            from first in ExpensiveCallToAWebServiceThatMightFail(id)
            from second in TryAndLoadARecordFromTheDatabase(id, first.ClientData.SomeField)
            from third in TryAndFindDataInTertiaryService(id, second.AnotherField, first.Some.Other.Context)
            select third;
}

This might fail at any point, so it's helpful to tag the None with some helpful context by using AsEither to convert from Option<Success> to Either<Success, string> e.g.

public Either<ResultFromTertiaryService,string> LoadFromAMultitudeOfServices(string value)
{
    return from id in TryParseInt(value).AsEither("Failed to parse ({0}) into an id", value)
            from first in ExpensiveCallToAWebServiceThatMightFail(id).AsEither("Didn't find a value")
            from second in TryAndLoadARecordFromTheDatabase(id, first.ClientData.SomeField).AsEither("Couldn't find {0} in the database", id)
            from third in TryAndFindDataInTertiaryService(id, second.AnotherField, first.Some.Other.Context).AsEither("Failed to load from tertiary source")
            select third;
}

Try

This is similar to Either, but uses an exception in the error case. This is useful for things like the SafeCallback extension method over all objects, which provides an ergonomic version of wrapping everything in a try-catch block.

Task Applicative (aka Asynquence)

This is probable the most interesting use of Task. This allows one to chain together a sequence of tasks and provide a callback at the end to produce a final result.

It's recommended to use the below to bring the class into scope directly.

using static Bearded.Monads.Syntax;

Then usage is as follows:

            var result = await Asynquence(Task.FromResult(10))
                .And(Task.FromResult(10))
                .And(Task.FromResult(10))
                .And(Task.FromResult(10))
                .Select((a, b, c, d) => a + b + c + d);
            var expected = 40;

            Assert.Equal(expected, result);

bearded.monads's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

bearded.monads's Issues

License?

Can you add a license to the project? I imagine is no longer supported, but it may be useful to others. As it is posted right now, it default to non-OSS and no redistribution/modification rights.

Would you consider a MapError?

Hi

I have a problem where methods return different error types and I can't see a way to reconcile them...

return from user in GetLoggedInUser() //Returns Either<User, ActionResult>
   from profile in _userSvc.GetUserProfile(user.Id) //Returns Either<Profile, string>
   select profile;

In F# the Result<Success, Error> monad has a mapError function so you can change the type of the returned error. Would adding that be the correct approach here or would is there some other way to structure my code so that it isn't required?

With MapError my code would be like this:

return from user in GetLoggedInUser() //Returns Either<User, ActionResult>
   from profile in UpdateProfile(user.Id, newProfile) //Returns Either<Profile, string>
   select profile;

private Either<Profile, ActionResult> UpdateProfile(int userId, Profile newProfile) =>
   _userSvc.UpdateProfile(userId, newProfile).MapError(e => BadRequestObjectResult(e));

I'm happy to add a PR if this is an ok suggestion.

Cheers

Option.Return does not check for null

Calling Option<A>.Return simply wraps the argument in a Some, allowing for Some of null.
I believe that the behavior should be similar to the implicit cast where attempting to pass a null value results in a None.

Can't create an Either that has the same type for success and error

The create method for either uses the type to disambiguate between the success and error case. So code such as this will not compile:

var x = Either<string, string>.Create("hello"); <-- doesn't compile

I'm not sure if this was a design decision made on purpose?

If it is not by design then some suggestions for the change are:

public static Either<Success, Error> Error(Error value) =>
            new ErrorContainer(value);
public static Either<Success, Error> Success(Success value) =>
            new SuccessContainer(value);

Or

public static Either<Success, Error> CreateError(Error value) =>
            new ErrorContainer(value);
public static Either<Success, Error> Create(Success value) =>
            new SuccessContainer(value);

However, in either case (lol), the implicit operator will stop working for one of the cases. Perhaps it could work for the success case and the error case must be explicitly created? This would make the change a breaking change.

Upgrade to DotnetCore 2.1?

Hey,

Would you mind if I upgrade the project to dotnet core 2.1? I can't get the tests to run locally unless I make a new test project that is 2.1 and then I have to copy them back to the existing project to commit them.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.