Introduction
I'm trying to get familiar with functional programming by implementing some functional concepts.
One of these concepts that I've attempted to implement below is a Union
data type (or more specifically, a Union2
, to indicate that it's a union of two values as a union data type can represent many values).
I know there is an Either
data type but I don't think it's appropriate to use the Either
data type in the case of a Union2
because at least from my understanding, there's an implication of "correctness" when it comes to the Left
value vs. the Right
value. In more concrete terms, a filter
method might make sense on an Either
but I would argue would not make sense on a Union2
.
Use Case
I've seen this occur in legacy systems where there are two different types of unique identifiers for a given entity - like a UUID and, say, an integer primary key.
You can imagine this system might have an API method like getEntityByIdentifier
that takes a Union2
of UUID
and Integer
and returns an Optional<Entity>
Optional<Entity> getEntityByIdentifier(Union2<UUID, Integer> identifier) {
return identifier.fold(
uuid -> getByUUID(uuid),
primaryKey -> getByPrimaryKey(primaryKey)
);
}
Implementation
public class Union2<T1, T2> {
private final Optional<T1> _1;
private final Optional<T2> _2;
Union2(final T1 _1, final T2 _2) {
this._1 = Optional.ofNullable(_1);
this._2 = Optional.ofNullable(_2);
if (this._1.isEmpty() && this._2.isEmpty()) {
throw new IllegalArgumentException("Union must contain a non-null value");
}
if (this._1.isPresent() && this._2.isPresent()) {
throw new IllegalArgumentException("Union contains two non-null values");
}
}
public static <T1, T2> Union2<T1, T2> _1(final T1 _1) {
return new Union2<>(_1, null);
}
public static <T1, T2> Union2<T1, T2> _2(final T2 _2) {
return new Union2<>(null, _2);
}
public <R> Union2<R, T2> map1(
final Function<T1, R> mapper1
) {
return map(
mapper1,
Function.identity()
);
}
public <R> Union2<T1, R> map2(
final Function<T2, R> mapper2
) {
return map(
Function.identity(),
mapper2
);
}
public <R1, R2> Union2<R1, R2> map(
final Function<T1, R1> mapper1,
final Function<T2, R2> mapper2
) {
return fold(
(_1) -> Union2._1(mapper1.apply(_1)),
(_2) -> Union2._2(mapper2.apply(_2))
);
}
public <R> R fold(
final Function<T1, R> mapper1,
final Function<T2, R> mapper2
) {
return _1
.map(mapper1)
.orElseGet(
() -> _2
.map(mapper2)
.orElseThrow(() -> new RuntimeException("All values are null"))
);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Union2<?, ?> union = (Union2<?, ?>) o;
return Objects.equals(_1, union._1) && Objects.equals(_2, union._2);
}
@Override
public int hashCode() {
return Objects.hash(_1, _2);
}
@Override
public String toString() {
return "Union{" +
"_1=" + _1 +
", _2=" + _2 +
'}';
}
}
Discussion
- Does the implementation / use-case make sense?
- Am I missing any potentially useful methods?
- I've represented the underlying data as
Optional
s and made the decision to trade a slight increase in memory so I could use the more functional interface presented byOptional
s.
2 Answers 2
For the record
Here's the official opinion on using Optionals instead of if-statements: https://stackoverflow.com/a/26328555 Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent "no result", and using null for such was overwhelmingly likely to cause errors.
Here's the reason why Java does not have a generic Pair data type and I think similar wisdom can be applied here too: https://stackoverflow.com/a/156685 The main argument is that a class Pair doesn't convey any semantics about the relationship between the two values
Review
The specific use case is a design error and in my opinion should not be solved with a generic solution, as generic solutions tend to leak out and encourage the implementation of more design errors elsewhere.
Having a non-private constructor along with the static initializers forces you to do unnecessary error checking to ensure the correctness of the state of the instance. Is there a reason why the class is not final? The implmentation of the operations are pretty much set in stone so allowing subclassing only introduces the possibility for the subclass to break the contract of the fold
and map
methods. I would rather make the class final, make the constructor private and rely on the static initializers to not allow construction with invalid parameters.
Style wise, if the input parameter checking needs to stay in the constructor, I would rather first check that the constructor parameters are correct before assigning them to instance variables just to avoid any possibility of invalid object state. But since you've deliberately chosen to absolutely never write an if-statement, I suppose that this is what you need to do :).
Numbers as field names force you to do unconventional naming (the underscore prefix) which is very confusing. Just use a
and b
. They're just as uninformative but at least they don't break naming conventions.
There is nothing wrong with if-statements and null-checks.
-
\$\begingroup\$ Thanks for the background around the
Pair
data type - very informative. And good call with making the classfinal
. The constructor is package-private (for testing, effectively) - do you not find that level of encapsulation sufficient? For the record, I never said there was anything wrong with if-statements and null-checks, just that I'd prefer to use the functional style thatOptional
s provide :) \$\endgroup\$Jae Bradley– Jae Bradley2022年03月11日 15:20:50 +00:00Commented Mar 11, 2022 at 15:20
Style
Your formatting is quite unusual.
public <R> Union2<R, T2> map1(
final Function<T1, R> mapper1
) {
return map(
mapper1,
Function.identity()
);
}
would typically look like
public <R> Union2<R, T2> map1(final Function<T1, R> mapper1) {
return map(mapper1, Function.identity());
}
That's the formatting that most Java developers expect to see (and thus gives the best readability in the Java community), and what your IDE would typically produce with its auto-formatting.
Naming
Use descriptive names, that make understanding some piece of code easy. Creating a Union2
looks like
Union2<UUID, Integer> idUnion = Union2._2(42);
which does not at all convey its intent. It should better be
Union2<UUID, Integer> idUnion = Union2.fromSecondOption(42);
Using the "from" prefix is a quite popular convention for static methods meant to provide a matching instance.
Generics
As the arguments of your static methods aren't enough to determine the generic type of the resulting instance, you'll quite often even have to write expressions like
Union2.<UUID, Integer>fromSecondOption(42);
where you have to explicitly supply the type parameters, in a construct that even seasoned Java developers rarely ever encounter.
Typical Java generic methods are designed such that the type parameters can be omitted, as the compiler can deduce them from the method arguments. From the 42
, the compiler can deduce the T2
to be Integer
, but has no way to guess that T1
should be UUID
.
The typical way of handling your situation (an identity that can be expressed either as an Integer
or as a UUID
) would be to create an interface Identity
with two implementing classes IntIdentity
and UUIDIdentity
, having the appropriate methods that deal with identification independent of the chosen option. This would avoid a lot of hard-to-read, clumsy code.
-
\$\begingroup\$ Not terribly familiar with this
Identity
interface pattern - how would this interface be incorporated with thisUnion
concept? \$\endgroup\$Jae Bradley– Jae Bradley2022年03月11日 15:24:33 +00:00Commented Mar 11, 2022 at 15:24 -
1\$\begingroup\$ @JaeBradley Instead of using something as generic as a
Union2<UUID, Integer>
for your situation, you'd specify what operations an identity has to support (e.g aresolveToEntity()
method), and have the implementing classes provide their way to find theEntity
. That's polymorphism, and IMHO perfectly solves your identities situation. \$\endgroup\$Ralf Kleberhoff– Ralf Kleberhoff2022年03月11日 16:46:24 +00:00Commented Mar 11, 2022 at 16:46