2
\$\begingroup\$

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 Optionals and made the decision to trade a slight increase in memory so I could use the more functional interface presented by Optionals.
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the background around the Pair data type - very informative. And good call with making the class final. 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 that Optionals provide :) \$\endgroup\$ Commented Mar 11, 2022 at 15:20
2
\$\begingroup\$

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.

\$\endgroup\$
2
  • \$\begingroup\$ Not terribly familiar with this Identity interface pattern - how would this interface be incorporated with this Union concept? \$\endgroup\$ Commented 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 a resolveToEntity() method), and have the implementing classes provide their way to find the Entity. That's polymorphism, and IMHO perfectly solves your identities situation. \$\endgroup\$ Commented Mar 11, 2022 at 16:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.