In this way you end up encoding more intent within your types and behaviours rather than implicitly letting the consumer figure it out themselves.
/u/anw's example shows that, in fact, the benefits of the type system are greater than that. Not only can the type system prevent you from accessing data before confirming that the data is present (which is, at the end of the day, not much better than an Optional type, already available in Java and others), but it can also prompt you to "handle" all possible cases.
In "most functional languages", your result would be a sum type which requires dispatching explicitly between one case or the other, aka you wouldn't have a pair of conditionals instead you'd have something along the lines
switch (customerOrError) {
case IsCustomer(customer):
Console.WriteLine(customer.ID);
case IsError(errorMessage):
Console.WriteLine(errorMessage);
}
so from `customerOrError` you wouldn't have any sort of direct access to the customer or error, you must go through a dispatching construct.In OO languages lacking sum types this sort of constructs would generally be implemented through lambdas or some sort of visitor e.g.
customerOrError.then(
customer => { Console.WriteLine(customer.Id); },
onError: errorMessage => { Console.WriteLine(errorMessage) }
);I'm not familiar with most languages, but I can state that Rust requires matching sum types to cover all possible variants. For example:
enum Animal {
Dog,
Cat,
Pigeon
}
let pet = Animal::Dog;
let sound = match pet {
Animal::Dog => String::from("ouaf"),
Animal::Cat => String::from("miaou")
};
This would not compile, because you've forgotten to cover the pigeon. You would need to either add one of the two lines below: Animal::Pigeon => String::from("rou rou")
or _ => String::from("eeeeeeeeeeeei")
The last line is a catch all, which would cover anything not checked for explicitly.Any language that provides this sort of union type will error in some way if you try to access something that isn’t guaranteed to be on every type in the union. Some languages which support null checking will infer Customer to be nullable.
const c = getCustomer(); // type is Customer | null
const z : Customer = c; // ERROR; incompatible types
if (x === null) {
// handle null
} else {
const y : Customer = c; // OK; knows c isn't null here
}A simpler example:
declare type Foo = {name:String};
declare function getFoo():Foo|null;
const f = getFoo();
if(f!==null) // mandatory or yields an error
console.log(f.name);
But afaik it doesn't guard against null by default.
It seems to me that the "reasonable" version is getting its reasonableness from naming the variable that gets the GetById return "customerOrError" which reminds the reader that there is an error case, not from the language having sum types. That's just a convention. Nothing stops you from naming it "customer" just like in the "unreasonable" language.
(I'd actually expect that from a lot of people, because you are probably going to have a lot more code in the case that you have gotten the Customer variant than in the Error variant case, and you probably don't want to be calling it customerOrError in the 99% of the code where you know that it is a Customer).