If it is an argument, it is certainly a design issue:
`response_serializer(PersonOrError: person_or_error)` is confusing, impossible to reason about easily, hard to test etc. It signals a way too tightly coupled API, for one. And it breaks the SRP, secondly.
Why not `response_serializer(Person: person, Error: error)` or `try_error(person).then(response_serializer(person))` or many other designs that do the same, but make it explicit where, when and why the distinction is made.
It is something you often see in e.g. Rails, though. And I consider it really poor and confusing API: an example of that dreaded "Rails Black Magick" where a method like `form_for()` can get literally hundreds of combinations of arguments and behaves entirely different according to what is being passed. In runtime. In production. With user-generated content and dynamic typing and all that.
If it is a return-type, there too: why not make it a tuple, or do like Rust: make it an `Option<Some, None>: user` or a `Result<Ok, Err>: user`, the typing now contains this "either/or" not the variable names;
And if it is a class, or even method: it is violating SRP.
Neither of the reasons are absolute. But they all signal that a better solution is possible (but maybe not practical given constraints). Which is fine: but acknowledging this design is lacking (and choosing to still use it) is IMO the correct path; telling yourself that the design is fine, is not.
> And if it is a class, or even method: it is violating SRP.
Not always! Somebody is going to have to discriminate on the argument, and if indeed the caller already knows what they're providing, there's no need to wrap it in an Either -- there should be individual methods for both variants. But if the caller doesn't know what it is yet, and the type represents a domain concept that happens to be a set of options, then there's no reason the caller has to busy itself with that knowledge. To the caller, this might be (actually or morally) an opaque type, and the callee may be where knowledge about it is centralized, and hence where discriminating on the variants ought to happen.
The single-responsibility principle is definitely something that always needs to be kept in mind, but it's not something that can be replaced with broad rules, either.
> If it is a return-type, there too: why not [...] do like Rust: make it an `Option<Some, None>: user` or a `Result<Ok, Err>: user`, the typing now contains this "either/or" not the variable names;
That's exactly what they did -- in the ML-like syntax they used, their `PersonOrError` is exactly `Either<Person, Err>`, but with the type arguments pre-instantiated.
type PersonOrError1 =
| P of Person
| E of Error
type 'l 'r either =
| Left of 'l
| Right of r
type PersonOrError2 = Person Error either
I agree about passing Either types as arguments, I wouldn't do this. Pattern matching solves this problem.
> If it is a return-type
As for making them return types, it forces you to match over them, you can have the compiler do exhaustive matching over the types it can be. If you have a tuple of (int, string), you could store an int and a string. With Either types, it has to be one OR the other, it can never be both.
Like I said, you can pattern match over this depending on what the type contains. It also allows nice binding and failing out the chain early. e.g. something like in a C style syntax:
public Either<Error, int> M()
{
"In M".Print();
return 6;
}
public Either<Error, string> M2(int y)
{
$"In M2 {y}".Print();
return $"{++y}";
}
public Either<Error, string> M3(string y)
{
$"In M3 {y}".Print();
return $"Result {y}";
}
public void ShowResult(string y) => y.Print();
public void DisplayError(Error error) => error.Message.Print();
M().Then(M2)
.Then(M3)
.Match(left: DisplayError,
right: ShowResult);
So, the functions that operate on the Either types only ever take the right type of the Either. If say M2 returned an Error, M3 would never be called and you shortcut to the error handler.
The match forces you to explicitly handle if it's an error or not.
I like this pattern, it can be concise and expressive when used in a context where it's applicable.
> And if it is a class, or even method: it is violating SRP.
I disagree, the Person class is just a Person (and nothing else), the Error class is an Error and nothing else. The Either class is generic and is a way to hold Either of multiple types (but only one at any time).
> make it an `Option<Some, None>:
I like Rusts option type, but the problem is the None state doesn't collect information, but I think it's certainly a nicer alternative to null. Again you can compile time enforce pattern matching to ensure you handle both states.
> Result<Ok, Err>: user`
Assuming Result can only ever be OK or Err, this is basically an Either type!
> Assuming Result can only ever be OK or Err, this is basically an Either type
Yes. It is the same concept.
SRP is rather vague, and interpreted differently depending on when and where to apply it (SRP for microservices is very different from SRP for a method).
But for the Either, or the Result, it applies perfectly. Because this thing does only one thing: it discrimates between this or that: it leaves what this and that do, are, mean etc to this and to that.
I was trying to put forward examples where the code does both this discrimination and applies the resulting meaning from it. In dynamically typed languages a bigger problem than in many others, where e.g. "null" or "some object of type X" or "a string with the error message" are far too common as return values.
type PersonOrError = P of Person | E of Error
It can be either or a Person or an Error