-
Notifications
You must be signed in to change notification settings - Fork 310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Change to interfaces #239
Conversation
The approach with Regarding I was hoping for this C# feature: dotnet/csharplang#1239 It would have mitigated the issues with the Result signature verboseness. But it doesn't look like Microsoft is going to implement it any time soon. |
Still not finished but this is the direction I was heading in |
These are the migrations from struct statics to interface extensions However, ran into a bit of a bug here. Certain things need to stay as struct statics for the 99% that won't build their own Result struct i.e. |
I'll need some time to review this, will try to do that over the long weekend. I'm curious, where does the /cc: @hankovich @space-alien |
I'm just curious how this should change my programming model. Previously lots of my methods returned I also extremely like to do Result<int, string> Foo()
{
if (!GetRandomBool())
{
return "Try again.";
}
return 42;
} i.e. to use implicit casts. I'm interested in how it will work now. Also I think switching to interfaces will increase allocations dramatically (Result structs will be boxed each time). To reduce this cost we can change void Foo(IResult<int> result)
{
if (result.IsSuccess)
{
Console.WriteLine(result.Value);
}
} to void Foo<T>(T result) where T : IResult<int>
{
if (result.IsSuccess)
{
Console.WriteLine(result.Value);
}
} but it does not looks great. Generally speaking I'm interested in cases when people want to write their own results. @c17r could you please specify some use cases? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also thing it's very natural for some logic (Result.Try
, Result.Combine
, etc) to be static.
For me such kind of code does not look obvious:
unusedResult.Try(() =>
{
if (_condition) throw null;
return 42;
)
My understanding is that it will somehow rely of unusedResult
's state, but it doesn't
{ | ||
private static readonly Func<Exception, string> DefaultTryErrorHandler = exc => exc.Message; | ||
|
||
public static IResult<T, E> Try<T, E>(this IResult<T, E> result, Func<T> func, Func<Exception, E> errorHandler) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I see result
is used only to construct another results (and it's state is not used).
I'm not sure it will look elegant in a caller code.
Same concern applies to all Try
overloads.
public static IResult Combine(this IEnumerable<IResult> results, string errorMessageSeparator = null) | ||
{ | ||
results = results.ToList(); | ||
var first = results.First(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it will throw if collection was empty, right?
The same thing with the other .First()
calls
@@ -10,6 +10,7 @@ public partial struct Result : IResult, ISerializable | |||
private readonly ResultCommonLogic<string> _logic; | |||
public bool IsFailure => _logic.IsFailure; | |||
public bool IsSuccess => _logic.IsSuccess; | |||
public Unit Value => Unit.Default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be cleaner to implement this property explicitly?
The initial conversation happened here: http://disq.us/p/26f3cgj Without targeting interfaces, there's no point to having Result<TVal, TErr>because you can't subclass a struct to have TErr be more meaningful than a string. Or, if you do write your own struct, you can't use any of the Extension methods since their explicitly using the library structs. Current work for a client does require that an error provide more information than just a string. I started to build my own version, remembered the 9 month ago exchange, and started this PR. |
Could you clarify what you mean by this? As I understand it, Is there some other limitation/constraint that means you can't use your custom Or do you wish to avoid using |
@space-alien yes, the main use case (for me at least) is to avoid the verbosity of There is a C# proposal ( dotnet/csharplang#1239 ) that would allow you to do
But it's been open for 5 years already and probably will never be implemented. @hankovich this should not change the programming model at all unless you want to introduce your own |
@vkhorikov well, I got the idea :)
Result<R, Error> Foo(Request r)
{
if (!r.IsValid)
{
return Error.NotValid;
}
return _repo.AddRequest(r)
.Tap(() => _sender.SendEmail(r));
} i.e. where we have several return points (one uses implicit cast, another one use extensions). If we rewrite all extensions to return interface references, we will erase the original type information, so the code will not compile anymore (
public interface IResult<TVal, TErr>
{
bool IsFailure { get; }
bool IsSuccess { get; }
TVal Value { get; }
TErr Error { get; }
IResult Failure(string resultError);
IResult<TNewVal> Failure<TNewVal>(string resultError);
IResult<TNewVal, TNewErr> Failure<TNewVal, TNewErr>(TNewErr resultError);
IResult Success();
IResult<TNewVal> Success<TNewVal>(TNewVal value);
IResult<TNewVal, TNewErr> Success<TNewVal, TNewErr>(TNewVal value);
IResult<TVal, TErr> Combine(IEnumerable<IResult<TVal, TErr>> results, Func<IEnumerable<TErr>, TErr> composerError = null);
} So I think they don't really belong to IResult itself, probably they can be moved to another abstraction public interface IResult<TVal, TErr>
{
bool IsFailure { get; }
bool IsSuccess { get; }
TVal Value { get; }
TErr Error { get; }
IMetaResult ResultFactory { get; }
}
public interface IMetaResult
{
IResult Failure(string resultError);
IResult<TNewVal> Failure<TNewVal>(string resultError);
IResult<TNewVal, TNewErr> Failure<TNewVal, TNewErr>(TNewErr resultError);
IResult Success();
IResult<TNewVal> Success<TNewVal>(TNewVal value);
IResult<TNewVal, TNewErr> Success<TNewVal, TNewErr>(TNewVal value);
IResult<TVal, TErr> Combine<TVal, TErr>(IEnumerable<IResult<TVal, TErr>> results, Func<IEnumerable<TErr>, TErr> composerError = null);
} I haven't measured allocations yet, so I don't have additional information here. |
To be honest, I don't think, that that changes are really needed. "Result" is type for maintaining logic of that library, not business logic of the application, where this lib is used. End-user shouldn't change it. It is only encapsulation of user information. And user can easily change it with the help of type params. But he shouldn't affect the Result itself. It is infrastructure thing of this lib, why it should be changed? But introducing interface also will bring some bad practices into user code. User created types (implementing IResult) will be used under interface reference, so nothing except interface members can be used. Or user can explicitly cast interface to his own types, that implement this interface. And this is not extensible and maintainable, it is bad practice. User defined names of those types wouldn't be seen, because IResult used everywhere. And of course, it will hit the performance, since users can use structs under reference types, that cause boxing. It is only my opinion, but please, consider that points. |
@ZloyChert Yeah exactly. @c17r conciser the following custom result (as I understand it's exactly what we want to achieve): public class MyResult<T> : IResult<T, IEnumerable<MyErrorTypeWithAVeryLongName>>
{
// implementation
} It will have the following problem: public MyResult<MyUser> Foo()
{
MyResult<MyUser> result = // get it somewhere
var valueToReturn = result.Bind(DoSmth); // any extension here really
// return valueToReturn; won't compile, since its static type is IResult<MyUser, IEnumerable<MyErrorTypeWithAVeryLongName>>
// so you have to cast
return (MyResult<MyUser>)valueToReturn;
}
MyResult<MyUser> DoSmth(MyUser user)
{
// impl here
} So we will loose the original type info anyway. |
@hankovich @ZloyChert I consider this work item experimental with the goal to see if all/most of the current properties of I haven't considered that re-targeting extension methods from In the end, we may settle for the addition of @hankovich @ZloyChert @space-alien As a side question, how do you guys typically use the
|
Isn't this inevitable for async code, because |
And assuming that's correct, then your methods can't return |
@space-alien yes, that seems correct as well. |
@vkhorikov , I don't use custom errors. Usually the fact of error occurred and message is enough for me |
Maybe I'm missing something about the library currently and happy to be corrected but here's a example scenario below (warning, this will get long!) Cribbing from the README a bit, let's say I have code like: class Model
{
public string Name { get; }
public string PrimaryEmail { get; }
public Model(string name, string email)
{
Name = name;
PrimaryEmail = email;
}
}
class Customer
{
public string Name { get; }
public string PrimaryEmail { get; }
public Customer(string name, string primaryEmail)
{
Name = name;
PrimaryEmail = primaryEmail;
}
} And so I create 2 Value Objects class CustomerName : ValueObject<CustomerName>
{
public string Value { get; }
private CustomerName(string value) => Value = value;
protected override bool EqualsCore(CustomerName other)
=> string.Equals(other.Value, Value);
protected override int GetHashCodeCore()
=> Value.GetHashCode();
public static Result<CustomerName> CreateWithStrE(string customerName)
{
if (string.IsNullOrEmpty(customerName) || string.IsNullOrWhiteSpace(customerName))
return Result.Failure<CustomerName>("CUSTOMER_NAME_INVALID");
return new CustomerName(customerName);
}
}
class Email : ValueObject<Email>
{
public string Value { get; }
private Email(string value) => Value = value;
protected override bool EqualsCore(Email other)
=> string.Equals(other.Value, Value);
protected override int GetHashCodeCore()
=> Value.GetHashCode();
public static Result<Email> CreateWithStrE(string email)
{
if (string.IsNullOrEmpty(email) || string.IsNullOrWhiteSpace(email))
return Result.Failure<Email>("EMAIL_INVALID");
return new Email(email);
}
} With the process looking like Result<Customer> WithStrE(Model model)
{
Result<CustomerName> name = CustomerName.CreateWithStrE(model.Name);
Result<Email> email = Email.CreateWithStrE(model.PrimaryEmail);
Result result = Result.Combine(name, email);
if (result.IsFailure)
return Result.Failure<Customer>(result.Error);
return new Customer(model.Name, model.PrimaryEmail);
} Which works great. But for contrived-ness, lets say instead of strings we want integer error codes. The Value Objects shift to class CustomerName : ValueObject<CustomerName>
{
public string Value { get; }
private CustomerName(string value) => Value = value;
protected override bool EqualsCore(CustomerName other)
=> string.Equals(other.Value, Value);
protected override int GetHashCodeCore()
=> Value.GetHashCode();
public static Result<CustomerName, int> CreateWithNumE(string customerName)
{
if (string.IsNullOrEmpty(customerName) || string.IsNullOrWhiteSpace(customerName))
return 1;
return new CustomerName(customerName);
}
}
class Email : ValueObject<Email>
{
public string Value { get; }
private Email(string value) => Value = value;
protected override bool EqualsCore(Email other)
=> string.Equals(other.Value, Value);
protected override int GetHashCodeCore()
=> Value.GetHashCode();
public static Result<Email, int> CreateWithNumE(string email)
{
if (string.IsNullOrEmpty(email) || string.IsNullOrWhiteSpace(email))
return 1;
return new Email(email);
}
} and the process becomes Result<Customer, int> WithNumE(Model model)
{
Result<CustomerName, int> name = CustomerName.CreateWithNumE(model.Name);
Result<Email, int> email = Email.CreateWithNumE(model.PrimaryEmail);
Result<bool, int> result = Result.Combine<bool, int>(
(a) => a.Sum(),
name,
email
);
if (result.IsFailure)
return Result.Failure<Customer, int>(result.Error);
return new Customer(model.Name, model.PrimaryEmail);
} except this doesn't even compile because because Result<Customer, int> WithNumE(Model model)
{
Result<CustomerName, int> name = CustomerName.CreateWithNumE(model.Name);
Result<Email, int> email = Email.CreateWithNumE(model.PrimaryEmail);
Result<bool, int> result = Result.Combine<bool, int>(
(a) => a.Sum(),
Result.FailureIf<bool, int>(name.IsFailure, true, name.Error),
Result.FailureIf<bool, int>(email.IsFailure, true, email.Error)
);
if (result.IsFailure)
return Result.Failure<Customer, int>(result.Error);
return new Customer(model.Name, model.PrimaryEmail);
} This, while hokey, does compile. But it will fail during runtime if Result<Customer, int> WithNumE(Model model)
{
Result<CustomerName, int> name = CustomerName.CreateWithNumE(model.Name);
Result<Email, int> email = Email.CreateWithNumE(model.PrimaryEmail);
Result<bool, int> result = Result.Combine<bool, int>(
(a) => a.Sum(),
name.IsSuccess ? Result.Success<bool, int>(true) : Result.Failure<bool, int>(name.Error),
email.IsSuccess ? Result.Success<bool, int>(true) : Result.Failure<bool, int>(email.Error)
);
if (result.IsFailure)
return Result.Failure<Customer, int>(result.Error);
return new Customer(model.Name, model.PrimaryEmail);
} which both compiles and works at runtime but it's so unwieldy and requires passing a Func to Lets try it with something other than a primitive. The library provides struct NumErr : ICombine
{
public int Code { get; }
public NumErr(int code = 0) => Code = code;
public ICombine Combine(ICombine value)
{
var other = (NumErr) value;
return new NumErr(other.Code + Code);
}
}
class CustomerName : ValueObject<CustomerName>
{
public string Value { get; }
private CustomerName(string value) => Value = value;
protected override bool EqualsCore(CustomerName other)
=> string.Equals(other.Value, Value);
protected override int GetHashCodeCore()
=> Value.GetHashCode();
public static Result<CustomerName, NumErr> CreateWithNumErrE(string customerName)
{
if (string.IsNullOrEmpty(customerName) || string.IsNullOrWhiteSpace(customerName))
return new NumErr(1);
return new CustomerName(customerName);
}
}
class Email : ValueObject<Email>
{
public string Value { get; }
private Email(string value) => Value = value;
protected override bool EqualsCore(Email other)
=> string.Equals(other.Value, Value);
protected override int GetHashCodeCore()
=> Value.GetHashCode();
public static Result<Email, NumErr> CreateWithNumErrE(string email)
{
if (string.IsNullOrEmpty(email) || string.IsNullOrWhiteSpace(email))
return new NumErr(1);
return new Email(email);
}
} with the process being Result<Customer, NumErr> WithNumErrE(Model model)
{
Result<CustomerName, NumErr> name = CustomerName.CreateWithNumErrE(model.Name);
Result<Email, NumErr> email = Email.CreateWithNumErrE(model.PrimaryEmail);
Result<bool, NumErr> result = Result.Combine(
name,
email
);
if (result.IsFailure)
return Result.Failure<Customer, NumErr>(result.Error);
return new Customer(model.Name, model.PrimaryEmail);
} Again, this fails to compile due to specificity. And this Result<Customer, NumErr> WithNumErrE(Model model)
{
Result<CustomerName, NumErr> name = CustomerName.CreateWithNumErrE(model.Name);
Result<Email, NumErr> email = Email.CreateWithNumErrE(model.PrimaryEmail);
Result<bool, NumErr> result = Result.Combine<bool, NumErr>(
Result.FailureIf<bool, NumErr>(name.IsFailure, true, name.Error),
Result.FailureIf<bool, NumErr>(email.IsFailure, true, email.Error)
);
if (result.IsFailure)
return Result.Failure<Customer, NumErr>(result.Error);
return new Customer(model.Name, model.PrimaryEmail);
} again compiles but errors at runtime for the "accessing Error of a successful Result". So we're back to similar to what we had before Result<Customer, NumErr> WithNumErrE(Model model)
{
Result<CustomerName, NumErr> name = CustomerName.CreateWithNumErrE(model.Name);
Result<Email, NumErr> email = Email.CreateWithNumErrE(model.PrimaryEmail);
Result<bool, NumErr> result = Result.Combine<bool, NumErr>(
name.IsSuccess ? Result.Success<bool, NumErr>(true) : Result.Failure<bool, NumErr>(name.Error),
email.IsSuccess ? Result.Success<bool, NumErr>(true) : Result.Failure<bool, NumErr>(email.Error)
);
if (result.IsFailure)
return Result.Failure<Customer, NumErr>(result.Error);
return new Customer(model.Name, model.PrimaryEmail);
} which works AND we don't have to provide the "how to Combine" every time but still feels like terrible DX. If I could do something like class ResultNumE<T> : IResult<T, int>
{
} where the interface had methods for ResultNumE<Customer> WithStrE(Model model)
{
var name = CustomerName.CreateWithResultNumE(model.Name);
var email = Email.CreateWithResultNumE(model.PrimaryEmail);
var result = name.Combine(name, email);
if (result.IsFailure)
return result
return new Customer(model.Name, model.PrimaryEmail);
} and I don't have to reimplement the entire library which is what I'd have to do today since the extension methods target structs not interfaces and I can't have my versions inherit from the structs. If this approach isn't preferred by this library, that's fine for me. I had started off by essentially cloning this repo and making the changes I needed for my own needs. But I also know I'm not the only one to run into this. And if you made it all the way down here, I salute your efforts! |
I think maybe the problem here is that you aren't making use of the library in the way it is intended. Jumping straight to your final point - if you want your code to look like this:
You could already achieve this* today with something as simple as: Result<Customer, int> CreateCustomer(Model model)
{
return CustomerName.CreateWithNumErrE(model.Name)
.Bind(_ => Email.CreateWithNumErrE(model.PrimaryEmail))
.Map(_ => new Customer(model.Name, model.PrimaryEmail));
} (*) The only difference being that if there is a validation failure, you only get the first error. However, if you are in practice using ints for your error codes, then you are already destroying the information they represent if you sum them as per your example code. I need to sign off, but perhaps Vladimir can jump in with more general points about how to improve your approach here. |
My intent is
The switch to an integer was just a contrived example since as I mentioned before the library currently is very much geared towards error strings. |
So you want to use This sounds like something that could perhaps be accomplished with a new |
@c17r I think Result<Customer, int> WithStrE(Model model)
{
var name = CustomerName.CreateWithResultNumE(model.Name).Map(_ => Unit.Instance);
var email = Email.CreateWithResultNumE(model.PrimaryEmail).Map(_ => Unit.Instance);
return Result.Combine(ints => ints.Sum(), name, email)
.Map(_ => new Customer(model.Name, model.PrimaryEmail));
} will work for you. |
@hankovich Your suggestion is the best one so far! Especially with an actual error object that implements Result<Customer, NumErr> HankovichTest2(Model model)
{
var name = CustomerName
.CreateWithNumErrE(model.Name)
.Map(_ => Unit.Default);
var email = Email
.CreateWithNumErrE(model.PrimaryEmail)
.Map(_ => Unit.Default);
return Result.Combine(name, email)
.Map(_ => new Customer(model.Name, model.PrimaryEmail));
} which taken a step further with some changes to the Value Objects class CustomerName : ValueObject<CustomerName>
{
public string Value { get; }
private CustomerName(string value) => Value = value;
protected override bool EqualsCore(CustomerName other)
=> string.Equals(other.Value, Value);
protected override int GetHashCodeCore()
=> Value.GetHashCode();
public static Result<CustomerName, NumErr> CreateWithValidation(string customerName)
=> Validate(customerName).Map(_ => new CustomerName(customerName));
public static Result<Unit, NumErr> Validate(string customerName)
{
if (string.IsNullOrEmpty(customerName) || string.IsNullOrWhiteSpace(customerName))
return new NumErr(1);
return Unit.Default;
}
}
class Email : ValueObject<Email>
{
public string Value { get; }
private Email(string value) => Value = value;
protected override bool EqualsCore(Email other)
=> string.Equals(other.Value, Value);
protected override int GetHashCodeCore()
=> Value.GetHashCode();
public static Result<Email, NumErr> CreateWithValidation(string email)
=> Validate(email).Map(_ => new Email(email));
public static Result<Unit, NumErr> Validate(string email)
{
if (string.IsNullOrEmpty(email) || string.IsNullOrWhiteSpace(email))
return new NumErr(1);
return Unit.Default;
}
} you can now do Result<Customer, NumErr> HankovichTest3(Model model)
=> Result.Combine(
CustomerName.Validate(model.Name),
Email.Validate(model.PrimaryEmail)
).Map(_ => new Customer(model.Name, model.PrimaryEmail)); which I think is pretty sharp 😄 |
@c17r nice to hear that helped! |
@vkhorikov Usually we start from something like public enum ErrorType
{
NotFound,
Conflict,
Unspecified
}
public class Error
{
public ErrorType Type { get; }
public string Reason { get; }
private Error(ErrorType type, string reason)
{
Type = type;
Reason = reason;
}
public static Error NotFound() => new Error(ErrorType.NotFound, nameof(NotFound));
public static Error Conflict(string? reason = null) => new Error(ErrorType.Conflict, reason ?? nameof(Conflict));
public static Error Unspecified(string reason) => new Error(ErrorType.Unspecified, reason);
} and that's enough for 90% percent of our cases. However, sometimes we evolve this structure to something like public enum ErrorType
{
NotFound,
Forbidden,
TooManyRequests,
Multiple,
Unknown
}
public class Error
{
public ErrorType Type { get; }
public DetailsBase Details { get; }
// a lot of useful static factory methods and helpers (to flatten errors of type `Multiple`, for example)
}
public abstract class DetailsBase
{
}
public class ErrorsDetails : DetailsBase
{
public IReadOnlyCollection<Error> Errors { get; }
}
public class StringDetails : DetailsBase
{
public string Value { get; }
}
public class TypeValueDetails : DetailsBase // usually used for validation, where `type` is a machine-readable error type/code, and value is an error ready to be presented to a user
{
public string Type { get; }
public string Value { get; }
} |
@hankovich thanks for the info about your company's practices, that's very helpful for better understanding of usage patterns. @c17r @space-alien @hankovich Regarding the use case Christian provided. I think the best way to handle it is this: public class Error : ValueObect
{
public int Code { get; }
public string Reason { get; }
/* ... */
}
public class Errors : ValueObect, ICombine
{
public IReadOnlyList<Error> ErrorList { get; }
/* ... */
}
public class Email : ValueObject
{
public Result<Email, Errors> Create(string email) { /* ... */ }
}
public class CustomerName : ValueObject
{
public Result<CustomerName, Errors> Create(string email) { /* ... */ }
}
// Controller
Result<Customer, Errors> WithNumErrE(Model model)
{
Result<CustomerName, Errors> name = CustomerName.Create(model.Name);
Result<Email, Errors> email = Email.Create(model.PrimaryEmail);
Result<Unit, Errors> result = Result.Combine(name, email);
if (result.IsFailure)
return result.Error;
return new Customer(name.Value, email.Value);
} Note a couple of important points/differences:
|
Also, it could be that instead of |
So taking @vkhorikov comments above with @space-alien Result<Customer, NumErr> WithNumErrE(Model model)
{
Result<CustomerName, NumErr> name = CustomerName.Create(model.Name);
Result<Email, NumErr> email = Email.Create(model.PrimaryEmail);
UnitResult<NumErr> result = Result.Combine<NumErr>(name, email);
if (result.IsFailure)
return result.Error;
return new Customer(name.Value, email.Value);
}
class Model
{
public string Name { get; }
public string PrimaryEmail { get; }
public Model(string name, string email) => (Name, PrimaryEmail) = (name, email);
}
class CustomerName : ValueObject<CustomerName>
{
public string Value { get; }
private CustomerName(string value) => Value = value;
protected override bool EqualsCore(CustomerName other) => string.Equals(other.Value, Value);
protected override int GetHashCodeCore() => Value.GetHashCode();
public static implicit operator string(CustomerName customerName) => customerName.Value;
public static Result<CustomerName, NumErr> Create(string customerName)
=> (string.IsNullOrEmpty(customerName) || string.IsNullOrWhiteSpace(customerName))
? new NumErr(1)
: new CustomerName(customerName);
}
class Email : ValueObject<Email>
{
public string Value { get; }
private Email(string value) => Value = value;
protected override bool EqualsCore(Email other) => string.Equals(other.Value, Value);
protected override int GetHashCodeCore() => Value.GetHashCode();
public static implicit operator string(Email email) => email.Value;
public static Result<Email, NumErr> Create(string email)
=> (string.IsNullOrEmpty(email) || string.IsNullOrWhiteSpace(email))
? new NumErr(1)
: new Email(email);
}
class Customer
{
public string Name { get; }
public string PrimaryEmail { get; }
public Customer(string name, string primaryEmail) => (Name, PrimaryEmail) = (name, primaryEmail);
}
struct NumErr : ICombine
{
public int Code { get; }
public NumErr(int code = 0) => Code = code;
public ICombine Combine(ICombine value) => new NumErr(((NumErr) value).Code + Code);
} Only two comments:
I do want to say I appreciate the everyone's input and discussion on this topic. I'm enjoying the direction that we're headed in. |
This looks good, though I'm still not sure about introducing @space-alien could you please post your |
Why? What about |
@hankovich I meant that I'm not sure which one is better -- |
I'll elaborate on why I'm not sure which one is better. |
@vkhorikov despite the fact the difference between I once discussed it with my colleagues and not everyone likes I think that |
Maybe we should continue the discussion about |
@c17r Christian, for the benefit of the discussion on #200 I added a 'unitresult' branch which has just a type definition for |
Work In Progress: no mergeable or buildable right now. Before putting too much work in, figured there should be a discussion about the path forward.
Looking at switching the extension methods over to the interfaces so people can make custom Result structures/classes (see http://disq.us/p/26f3cgj).
Based off the way some of the extension methods work, some extension methods will need to be moved to instance methods and defined on the interface. An example of this is Bind:
Failure()
would have to be an instance method so that the custom Result can create and instance of its type.While doing this I was looking at ways we could simplify (for various definitions of simplify) things and thought about turning things on it's head. If the base type is the generic
IResult<T, E>
with other two being specializations then the extension methods need only to worry about theIResult<T, E>
cases and the others naturally fall in line.This would be controversial since
IResult
shouldn't have a value, just Success/Failure flags, hence the discussion here first. My solution to that is to bring inUnit
and makeIResult
beIResult<Unit, string>
There is also some functionality that is directly on the Result struct e.g.
ConvertFailure
. Would we want to push those up into extension methods?