Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Optional, introduced along with Stream and Function, in Java 8, has a map() method. Does it obey the functor laws, particularly the one pertaining to function composition?
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class Scratch {
/*
A method that converts a String to an Integer.
Returns null if the String can't be parsed.
*/
private static Integer toInteger(final String s) {
try {
return Integer.valueOf(s);
} catch(final NumberFormatException e) {
return null;
}
}
/*
Another method. This one converts an Integer into a String.
If the Integer is null, return the string "NULL!".
*/
private static String toString(final Integer i) {
if (null == i)
return "NULL!";
else
return i.toString();
}
/*
And finally, a third method.
This is the composition of the first two: like toString(toInteger(x))
*/
private static String toIntegerThenString(final String s) {
return toString(toInteger(s));
}
/*
But we wont use these two methods--we're gonna use analogous functions instead...
*/
public static void main(String[] args) {
/*
A function that converts a String to an Integer.
Returns null if the String can't be parsed.
*/
final Function<String,Integer> toInteger = (s) -> {
try {
return Integer.valueOf(s);
} catch(final NumberFormatException e) {
return null;
}
};
/*
Another function. This one converts an Integer into a String.
If the Integer is null, return the string "NULL!".
*/
final Function<Integer,String> toString = (i) -> {
if (null == i)
return "NULL!";
else
return i.toString();
};
/*
And finally, a third function.
This is the composition of the first two: like toString(toInteger(x))
*/
final Function<String, String> toIntegerThenString = toString.compose(toInteger);
/*
The two functions can be used in turn to transform a Stream of values
(two map()ings per value)
*/
final List<String> stringListViaTwoMappings =
toList(Stream.of("1").map(toInteger).map(toString));
/*
Likewise, the _composition_ of the two functions can be used to transform a Stream of values
(one map()ing per value)
*/
final List<String> stringListViaMappingSingleComposedFunction =
toList(Stream.of("1").map(toIntegerThenString));
/*
With identical results...
*/
show("are composition and successive mapping (on Streams) equal: %s%n",
stringListViaTwoMappings.equals(stringListViaMappingSingleComposedFunction));
show("result: (%s)%n", stringListViaTwoMappings.get(0));
/*
...even if some functions return null
*/
final List<String> stringListOfNullViaTwoMappings =
toList(Stream.of("Not an integer").map(toInteger).map(toString));
final List<String> stringListOfNullViaMappingSingleComposedFunction =
toList(Stream.of("Not an integer").map(toIntegerThenString));
show("are composition and successive mapping (on Streams) equal when fns return null: %s%n",
stringListOfNullViaTwoMappings.equals(stringListOfNullViaMappingSingleComposedFunction));
show("result: (%s)%n", stringListOfNullViaTwoMappings.get(0));
/*
Other things implement a map() method, notably Optional
*/
/*
As with a Stream, the two functions can be used in turn to transform the Optional's
(internal) value
*/
final Optional<String> stringOptionalViaTwoMappings =
Optional.of("1").map(toInteger).map(toString);
/*
Ooh! Look at that symmetry--not only does Optional, like Stream, have map(),
Optional.of() works just like Stream.of()
It's just that a Stream can carry zero or more values,
whereas an Optional can only carry zero or one
*/
/*
And again, as with Stream, the composition of the two functions can be used to transform
the Optional's (internal) value
*/
final Optional<String> stringOptionalViaSingleComposedFunction =
Optional.of("1").map(toIntegerThenString);
/*
With identical results...
*/
show("are composition and successive mapping (on Optionals) equal: %s%n",
stringOptionalViaTwoMappings.equals(stringOptionalViaSingleComposedFunction));
show("result: (%s)%n", stringOptionalViaTwoMappings.get());
/*
Again with the symmetry! Notice how Optional.get() is like List.get(offset). Since an Optional
can carry at most one value, there is no need for an offset parameter.
Don't you just love all this symmetry!
*/
/*
Now, what if some function calls return null? (recall this worked just fine with Streams above)
If we hand toInteger() a String that that does not look like an integer, then it'll return null
*/
final Optional<String> stringOptionalOfNullViaTwoMappings =
Optional.of("Not an integer").map(toInteger).map(toString);
/*
Conceptually, we expect our container (Optional) to contain null after the first map() call.
That's just fine because the second map() call applies toString() which can handle a null
argument just fine
That should give us the same result as if we map() once per value using the composed fn:
*/
final Optional<String> stringOptionalOfNullViaMappingSingleComposedFunction =
Optional.ofNullable("Not an integer").map(toIntegerThenString);
show("are composition and successive mapping (on Optionals) equal when fns return null: %s%n",
stringOptionalOfNullViaTwoMappings.equals(stringOptionalOfNullViaMappingSingleComposedFunction));
show("result of mapping the composed fn: (%s)%n", stringOptionalOfNullViaMappingSingleComposedFunction.get());
show("result of mapping the two fns in succession: (%s)%n", stringOptionalOfNullViaTwoMappings);
/*
woopsie! Unlike Stream, Optional mapping breaks the composition rule
two explanations:
https://blog.developer.atlassian.com/optional-broken/
https://www.sitepoint.com/how-optional-breaks-the-monad-laws-and-why-it-matters/
understand functors in the Haskell tradition:
https://wiki.haskell.org/Functor#Functor_Laws
http://learnyouahaskell.com/functors-applicative-functors-and-monoids
a better Option for Java:
https://www.vavr.io/vavr-docs/#_option
*/
}
private static void show(final Object value) {
System.out.println(value);
}
private static void show(final String format, final Object...args) {
System.out.printf(format,args);
}
private static <T> List<T> toList(final Stream<T> s) {
return s.collect(Collectors.toList());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.