If something is wrong, throw an exception. If it's the kind of error that must be handled, throw a checked exception. If there's no value to return, return Optional.empty.
The code receiving that Optional<Foo> is now free to do what the author is suggesting: map, filter, all without worrying if the value is actually there or not. The code is cleaner, easier to read, etc.
But no one should ever have to check for null anymore.
e.g. you know
public Optional<File> loadFile(String filename);
can fail to return a file, while public Int sum(Int a, Int b);
can't fail to return an Int.Obviously, because nulls exist in java, this guarantee isn't provable on compilation the way it is in e.g. Scala, but if your entire team writes code this way, at least you can rely on that for internal libraries. Like generics, this is one of those ideas that's ok in java 1.X, but doesn't shine as much until a backwards incompatible java 2.X
Which is why I like Kotlin's approach. Same guarantees, one letter instead of ten. If you're writing Java, though, I can see why Optional has some use.
Can't in Kotlin, though.
Let's take this back to map/filter on lists. Remember the bad old days when you didn't have map and filter on lists? If you had some code that iterated heavily and then you needed to add another step you often ended up refactoring a lot of manual for code that explicitly handled iteration logic. Now with streams you can just chain another call onto the same stream and as long as the types match up you're ready to go test it. Optional gives you the same thing, but instead of working with lists it handles values that might be missing.
Yes, you could argue that you will only use null for an actually empty result. But two things: 1) You won't keep to it because the compiler won't force you to do so, and 2) potential contributors will not comply. When using optionals/either/enums, compiler will not allow a wildcard result (such as null).
.empty is just more semantically specific, and if you read a method's API for the first time, semantics helps you grasp it a lot faster. That's the reason I'm using enums in Swift all over the place now. Compared to the nilling out of Objective-C, I can be way more confident that method results really are what they claim they are.
The code I've written using optionals instead is much shorter, much clearer in its intent, and generally has fewer bugs.
That's anecdotal experience, not proofs. All I can really recommend is that you try it with an open mind.
That is sometimes just not an option performance wise yet.
"If it's the kind of error that must be handled, throw a checked exception. If there's no value to return, return Optional.empty."
Again, why is Optional.empty() better then null? What makes it better? What do you get from throwing an exception? What is the benifit. You can't just say "my way is better" when we have years and years of Java development flying contrary to your statement. What has changed that makes null a non-viable practice?
"The code receiving that Optional<Foo> is now free to do what the author is suggesting: map, filter, all without worrying if the value is actually there or not. The code is cleaner, easier to read, etc."
The code being cleaner is a subjective, or at least extremely difficult to prove, statment. For instance
List<Integer> nums = getNumbers();
if (nums == null) {
S.o.p("Failed to load numbers");
return;
}
for (Integer i : nums)
System.out.println(i);
Is far better then Optional<List<Integers>> nums = getNumbers();
if (!nums.isPresent()) {
S.o.p("Failed to load numbers");
return;
}
nums.forEach(S.o::p);
Or even better yet for (int i : nums)
if (i < 10)
S.o.p(i);
VS nums.filter((n) -> n < 10).forEach(S.o::p);
I don't think that's more readable. It think that's more compressed. HEre's another example. Suppose we have a magical language that I'm sure you'll pick up. It's a very compressed (or as you'd say expressive) language. This is that same code written in it pa(i i nums < 10)
Which expands to "print all the ints `i` in nums that are less then 10" in english. That's far less readable. It is more compressed. I don't think compression is the goal of a language as much as it is a goal of a zip program. We're supposed to be writing software, not liturature to submit to the shortest story fully told contest. Readability is a function of correct verbosity.In my opinion. Just compressing your logic doesn't make it more readable. I think some verbosity is needed to preserve simplicity.
"But no one should ever have to check for null anymore. "
I mean that just doesn't make sense. If you're suggesting that there are some times the state of a program should never contain a null value is just ridiculous. Some things are correctly modled by null and some things are also too performance dependant to not use null.
I think some things benifit from using Optional<> but the case doesn't exist to completely remove null. Even just by the creation of a new container object wrapping your already expensive return object there exists a case for null to exist.
Just saying "Don't do it it's bad" is not proof. Saying "the code is cleaner, easier to read, etc" is not proof or even an example of a design that is simpler to pickup and get going with. You'd have to write some code with the Java/OOP paradigms and write a version (that is feature complete) with the FP paradigms and see which is easier to understand for a new programmer. I'd be hard pressed to belive that the FP implementation would be simpler. Maybe to you and me but no to someone without the domain specific knowladge required to understand what's going on. Even when I use map, zip, and list comprehensions in my python code it scares off some of my coworkers.
public List<Integer> add2(File file) {
List<Integers> nums = file.getNumbers();
if(nums != null) {
List<Integer> resultList = List<>();
for ( i : nums) {
resultList.append(i + 2);
}
return resultList;
} else {
return null;
}
}
Compare that to: public Optional<List<Integer>> add2(File file) {
Optional<List<Integers>> numsOpt = file.getNumbers();
return numsOpt.map((nums) =>
nums.map((i) => i + 2);
);
}
I find it hard to argue that the latter is worse. public List<Integer> add2(File file) {
List<Integer> resultList = new ArrayList<>();
for (int i : ListUtils.emptyIfNull(file.getNumbers())) {
resultList.append(i + 2);
}
return resultList;
} public List<Integer> add2(File file) {
List<Integers> nums = file.getNumbers(); // Show where the data comes from
if(nums == null) { // Show the invariant provided for the rest of the code
return Collections.<Integer>emptyList(); // Show the default return value if there has been an error
}
List<Integer> resultList = List<>(); // Apply your logic
for ( i : nums) {
resultList.append(i + 2);
}
return resultList; // Return your list
}
Then you document that behavior in a javadoc. Throw in @Nullable where applicable and move on. Or you can do this public List<Integer> add2(File file) {
// Show data source
List<Integers> nums = file.getNumbers();
// Show case where data is supplimented
if(nums == null)
nums = Collections.<Integer>emptyList(); // Show supplimnetal
// Show transformation
List<Integer> resultList = List<>();
for ( i : nums) {
resultList.append(i + 2);
}
return resultList;
}
This is important to unify because nums won't always be null. We also won't always be supplimenting with an empty set. I don't see what value is recived from compressing that all into a single line. I don't see that as being more readable. I do see this very simple, step by step, explination of what's happening as being dead simple that no one can misunderstand.Because with Optional the type tells the caller that it might not return a value. With null, you have no idea if you need to check for null or not.
Being able to chain maps, filters, etc. is convenient as well, but I'd argue that's just a side benefit.
Oh, one other thing Optional can do that null can't: nest. For example, if you call get() on a Map, and it returns null, you can't tell if the key wasn't in the map, or if it's value was null. If get() returned an Optional, then for a Map<String,Optional<String>>, get() would return Optional<Optional<String>>.
Look, what you're trying to do is simple.
getNumbers.fold(println("Failed to load numbers"))(nums => nums.foreach(println))
That's the Scala version, but I'm sure there's a Java version of the same code. This code assumes that getNumbers returns Option[List[Int]], which your compiler can guarantee. If you want something that's a little bit more readable, try this: getNumbers match {
case Some(nums) => nums.foreach(println)
case None => println("Failed to load numbers")
}
No null check, no loops, just very simple easy-to-read bulletproof code. This code cannot generate a NPE at all, ever.The second option, as I have stated many times in this thread, is far preferable to null. Java does not support it but when it does I will like it.
"* No null check, no loops, just very simple easy-to-read bulletproof code. This code cannot generate a NPE at all, ever.*"
The NPE isn't the illness, it's the symptom. It's the symptom of a far larger problem in your program: unhandled or otherwise unexpected state. That's why I like the match example. It forces you to expect all of the states of your program. The first example does neither. It hides flow and operation in a single (ever-expanding) line. Far from ideal in any case in my book.
As soon as Match is supported in Java I'll change my opinion. Until then I'm stuck. You get nothing better from the Java version in terms of readability.
Simple example that proves my point: You can't call .length on null, but you can for every string. Therefore null isn't a string. So why in hell should you be able to write code that states (falsely) that null is a string? You can't say an int is a string.
Removing null from a language adds a tremendous amount of safety.
> That is sometimes just not an option performance wise yet.
Why? I haven't heard about performance issues of exceptions since 90-ties and C++ problems.
And if you consider that you can throw exceptions instead of error codes (which null is an example of) why would a list of numbers be ever null?
It should be an empty list, not null.
Generally you don't add Optionals to Collections/Iterables/Maps because they already have a notion of empty.
So your example becomes:
List<Integer> nums = getNumbers();
for (Integer i : nums)
System.out.println(i);
And if getNumbers has an error, it should throw an exeption.Java's Optional is a bad idea implemented badly made even more bad by bad usage.
Option(null).map(str => str + "!").foreach(str => println(str)) is a noop.