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.