1

I am defining a type Option<T> in Java that should behave as much as possible as Rust's equivalent.

It has a method, Option::flatten, that is only implemented if the inner T is some other Option<T>. I am thinking of something like this:

public class Option<T> {
    /* fields, constructors, other methods */

    @Bound(T=Option<U>)
    public <U> Option<U> flatten() {
        if (isNone()) return None();
        else return this.unwrap();
    }
}

But the syntax is of course completely fictional. Is there some way to make this work in Java? I know static methods are an option, but they can't be called like a normal method which is the only goal of this type.


This is not supposed to be a standalone thing, but rather a part of a larger Java implementation of Rust iterators I'm currently working on.

SuperStormer
  • 4,997
  • 5
  • 25
  • 35
leo848
  • 637
  • 2
  • 7
  • 24
  • Do you have a practical use case for this? I see a data type like this and I immediately think it's weird to have a `Foo>` (or a Foo of type Foo of type T). – Makoto Aug 22 '22 at 16:24
  • 3
  • @Makoto `Option – leo848 Aug 22 '22 at 16:30
  • @Rogue I know the `Optional` type exists, but I don't like its API design. My question is much more general and also applies to many other pieces of Java code I've written. – leo848 Aug 22 '22 at 16:31
  • @Rogue One example of this in the standard library is that a `List` does not implement `Comparable>` even though the lists can definitely be compared. In Rust this is easy using `impl` blocks and trait bounds. – leo848 Aug 22 '22 at 16:35
  • 4
    @leo848: Sure, but it's not common in Java. What is it you're really trying to *accomplish* here? There's a purpose to this being in Rust, since your iterators return this type while iterating, but that's not how Java works. – Makoto Aug 22 '22 at 16:46
  • @Makoto I agree, but I'm trying to build a library that works like Rust iterators in Java, i.e. a `Iterator` interface with a `Option next()` method. `Option` is just a first (and absolutely necessary) step towards this goal. – leo848 Aug 22 '22 at 16:54
  • So in the end, I believe you're saying that you're truly trying to "write Rust in Java" (for lack of a better phrase). Which I'd have to say that Java is not Rust, and you're running into the headaches of that (e.g. nullability). Stepping back just a little bit, _why_ do you want to make these rust-like `Iterator`/`Option` classes, as opposed to using what is already available? – Rogue Aug 22 '22 at 17:08
  • 1
    @leo848: ...but ***why***? Java isn't Rust and it doesn't *have* to behave like Rust, and it shouldn't behave like Rust if you're trying to maintain it as a Java library. – Makoto Aug 22 '22 at 17:09
  • @Makoto Because I think the Rust model for iteration is (arguably) the most concise, expressive and useful, and by porting it to Java I gain all of these benefits. I do not care about being idiomatic or following OOP principles in Java, but instead just try to adhere to the Rust Iterator style as much as possible. – leo848 Aug 22 '22 at 17:19
  • 1
    @Makoto To be fair, insisting that one should just follow the Java way of doing things doesn't help much here. The core of this question is about making a `flatten` method. Even if we put Rust out of the equation, how could the Java standard library introduce a `Stream#flatten` method? For what it's worth, there is interest in flattening a collection in idiomatic Java ([1](https://stackoverflow.com/questions/18290935/flattening-a-collection) [2](https://stackoverflow.com/questions/25147094/how-can-i-turn-a-list-of-lists-into-a-list-in-java-8)). – E_net4 Aug 22 '22 at 17:19
  • Also I'm not trying to establish this as some sort of Java library, rather as a tool to port existing Rust iterator code to Java or just follow Rust's iterator model in Java. – leo848 Aug 22 '22 at 17:20
  • @E_net4thecommentflagger: You're not flattening a collection in this example, you're flattening an unbound generic. That doesn't make a whole lot of sense in the ten-plus years I've been doing Java. – Makoto Aug 22 '22 at 17:26
  • 2
    @leo848: I can respect your motivations, but you may run into more papercuts than balm if you go that route. The iteration models for both are different, and they both have their pros and cons. You'll need to determine if it's worth swimming against the current in Java to do something Rust-centric as opposed to doing this in an idiomatic Java way. Java's generics [really aren't that good.](https://stackoverflow.com/a/355075/1079354) – Makoto Aug 22 '22 at 17:28
  • 1
    @Makoto An `Option` is indeed equivalent to a collection of up to one element. But ultimately, on one thing we agree: Java's generics will feel rather thin after doing generic programming in other languages. – E_net4 Aug 22 '22 at 17:31
  • @Makoto well I'm pretty much already done with the standard libraries' iterator functions and now working on stuff from the `itertools` crate (see the [GitHub repo](https://github.com/leo848/rust-in-java) ). I would already consider this to be more useful for me than `java.util.{Iterator, stream.Stream}`, but this is of course subjective. – leo848 Aug 22 '22 at 17:42

3 Answers3

1

The problem with trying to come up with a non-static method such as flatten is that in Java one cannot conditionally add more methods to a class based on whether the type parameter of the class fulfills a certain constraint.

You can, however, make it a static method and constrain its arguments to whatever you need.

class Option<T> {
    // ...
    public static <U> Option<U> flatten(Option<Option<U>> option) {
        if (option.isNone()) return None();
        return option.unwrap();
    }
}

Which would work for valid implementations of None, isNone and unwrap.


A more complete example follows.

public static class Option<T> {
    private final T value;
    
    private Option(T x) {
        this.value = x;
    }
    
    public static <T> Option<T> of(T x) {
        java.util.Objects.requireNonNull(x);
        return new Option<>(x);
    }

    public static <T> Option<T> None() {
        return new Option<>(null);
    }
    
    public T unwrap() {
        java.util.Objects.requireNonNull(this.value);
        return this.value;
    }
    
    public boolean isNone() {
        return this.value == null;
    }

    public static <U> Option<U> flatten(Option<Option<U>> option) {
        if (option.isNone()) return Option.None();
        return option.unwrap();
    }
    
    @Override
    public String toString() {
        if (this.isNone()) {
            return "None";
        }
        return "Some(" + this.value.toString() + ")";
    }
}

Usage:

var myOption = Option.of(Option.of(5));
System.out.println("Option: " + myOption);
System.out.println("Flattened: " + Option.flatten(myOption));

Output:

Option: Some(Some(5))
Flattened: Some(5)
E_net4
  • 27,810
  • 13
  • 101
  • 139
  • Alternatively, you can add `#flatten` non-statically with an exception for being fully flattened (or a silent "failure" that self-returns), but you may face the need for an `#isFlattened` with the exception route. I'd say the typical approach is to avoid a situation that would provide a non-flattened `Optional` to begin with. – Rogue Aug 22 '22 at 16:38
  • I think the downside here, that the OP probably wants, is that to even call this method you have to check to make sure is already an `Option – Nathaniel Ford Aug 22 '22 at 16:38
  • 1
    @Rogue Although it is a possibility, adding checks at run-time seems undesirable over keeping everything checked as compile time, which I believe is more aligned to the implementation that the OP wishes to obtain. – E_net4 Aug 22 '22 at 16:41
  • @NathanielFord It is under the OP's expectations that such a condition would already be known at compile time, as it would be in Rust. One trivial case of this is imagining an `Option` being mapped into a `Option – E_net4 Aug 22 '22 at 16:42
  • Thanks, that's probably what I needed to know. Do you know how I could define a method that checks if the generic type parameter `T` is of type `Option`? Does it involve reflection or can you cast it and catch a `ClassCastException`? – leo848 Aug 22 '22 at 16:42
  • 1
    @E_net4thecommentflagger indeed and I agree, I'm writing up an answer regarding my comment and why it's a bad idea. @leo848 you can use `instanceof` and some runtime reflection checks, but overall java erases its generics at runtime. – Rogue Aug 22 '22 at 16:44
  • 1
    @leo848 The type parameter `T` itself is erased at run-time, but the inner value could eventually be checked for its class at run-time via `getClass()` and bound into a variable of a known type via class casting. – E_net4 Aug 22 '22 at 16:51
  • @E_net4thecommentflagger yep, that's pretty much what I already implemented. I though about if it was possible to do this without a static method, but apparantly it isn't. – leo848 Aug 22 '22 at 16:57
0

I think the way you want to handle this is not to actually have a flatten() method, but have different handling in your constructor. Upon being created, the constructor should check the type it was handed. If that type is Option, it should try and unwrap that option, and set its internal value to the same as the option it was handed.

Otherwise, there isn't really a way for an object to 'flatten' itself, because it would have to change the type it was bounded over in the base case. You could return a new object from a static method, but are otherwise stuck.

Nathaniel Ford
  • 20,545
  • 20
  • 91
  • 102
  • Well, but that would mean that an `Option – leo848 Aug 22 '22 at 16:43
  • Personally I would inspect that assumption. I'm not saying you *don't* need it, but in my experience that sort of convolution isn't actually necessary. – Nathaniel Ford Aug 22 '22 at 16:45
  • @NathialFord in Rust, it is absolutely necessary. An `Iterator` (actually Item is an associated type, but that doesn't matter) has a method `Iterator::next` returning `Option`. If one now iterates over e.g. a list of options (like `[Some(4), None, None, Some(5), Some(3)].into_iter()`) the iterator's return type is `Option – leo848 Aug 22 '22 at 16:49
  • Are you under the same constraint in Java? – Nathaniel Ford Aug 22 '22 at 16:55
  • It's necessary in _Rust_, but you're working in _Java_. Even for a similar example of, say, a stream of `Optional`, you'd be able to `stream.filter(Optional::isPresent)`. With this approach, it'd automatically "unwrap" any potential new `Option`. – Rogue Aug 22 '22 at 16:55
  • Being able to hold any other value within an `Option` should not have to be portrayed as an unusual or inorganic constraint. This feels much the opposite: Rust's `Option` type is not constrained to holding values other than `Options`. It can hold any other sized type `T`. It seems clear that the OP wishes to replicate this freedom as part of the question's criteria. – E_net4 Aug 22 '22 at 17:02
  • I think it is pretty inorganic, especially in Java. The `flatten()` in Rust exists *because* it's inorganic and you want to get out of the inorganic region of 'I might have the possibility of having something'. That's logically equivalent to 'I might have something', but way more convoluted brainspace-wise. Abstractly it makes plenty of sense that the `T` of `Option` could be anything. But it also makes plenty of sense to collapse wrapping classes, and it's a code smell if you're not. Java just makes this particularly awkward. – Nathaniel Ford Aug 22 '22 at 17:19
  • I sense a strawman. The method exists because [it was implemented](https://github.com/rust-lang/rust/pull/60256), possibly after someone felt the need for it, but it is not that important. It was only added in version 1.40.0, in 2019. `flat_map` is still more idiomatic if applicable. On the other hand, the need for flattening collections or other things which may hold values [is not unique to Rust](https://stackoverflow.com/questions/25147094). – E_net4 Aug 22 '22 at 17:28
  • I'm not arguing against `flatten()` as a concept - that's certainly a straw man. But while `Option – Nathaniel Ford Aug 22 '22 at 17:39
  • _"while `Option – E_net4 Aug 22 '22 at 19:26
  • I'm curious: what is the semantic meaning of `Some(None)` that differs from `None`? Both are options, usable wherever an option would be. Both resolve to `None`. I'm happy to be wrong about this, but I lack the imagination to see where that would be relevant, correct way to represent a state. – Nathaniel Ford Aug 22 '22 at 19:28
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/247456/discussion-between-e-net4-the-comment-flagger-and-nathaniel-ford). – E_net4 Aug 22 '22 at 19:33
  • After discussion, given the assumption of a Rust-like iterator, it's probable that this approach isn't one you want. In particular because type erasure prevents you from properly delineating to what question a particular level of `Option` refers. – Nathaniel Ford Aug 22 '22 at 20:12
0

I want to point out some of the potential headaches and issues regarding this re-implementation of Optional<T>.

Here's how I would initially go about it:

public class Option<T> {
    /* fields, constructors, other methods */

    public <U> Option<U> flatten() {
        if (isNone()) return None();
        T unwrapped = this.unwrap();
        if (unwrapped instanceof Option) {
            return (Option<U>) unwrapped; //No type safety!
        } else {
            return (Option<U>) this;
        }
    }
}

However, this code is EVIL. Note the signature of <U> Option<U> flatten() means that the U is going to be type-inferenced into whatever it needs to be, not whatever a potential nested type is. So now, this is allowed:

Option<Option<Integer>> opt = /* some opt */;
Option<String> bad = opt.flatten();
Option<Option<?>> worse = opt.<Option<?>>flatten();

You will face a CCE upon using this for the other operations, but it allows a type of failure which I would say is dangerous at best. Note that any Optional<Optional<T>> can have #flatMap unwrap for you: someOpt.flatMap(Function.identity());, however this again begs the question of what caused you to arrive at a wrapped optional to begin with.

Another answer (by @NathanielFord) notes the constructor as an option, which seems viable as well, but will still face the runtime check upon construction (with it simply being moved to the constructor):

public class Option<T> {
    /* fields, constructors, other methods */

    public Option<T>(T someValue) { ... }
    public Option<T>(Option<T> wrapped) {
        this(wrapped.isNone() ? EMPTY_OBJECT : wrapped.unwrap());
    }

    public Option<T> flatten() {
        return this; //we're always flattened!
    }
}

Note as well, the re-creation of Optional<T> by @E_net4thecommentflagger has the potential for a nasty future bug: Optional.ofNullable(null).isNone() would return true! This may not be what you want for some potential use-cases, and should #equals be implemented in a similar manner, you'd end up with Optional.ofNullable(null).equals(Optional.None()), which seems very counter-intuitive.

All of this to say, that while Rust may require you to deal with these nested optionals, you are writing code for Java, and many of the potential restrictions you faced before have changed.

Rogue
  • 11,105
  • 5
  • 45
  • 71
  • 1
    _"Optional.ofNullable(null).isNone() would return true!"_ That was by design, but sure, this method can and should be removed. Rust does not have it either, because it does not even allow null references. An equivalent in Java's `Optional` most likely exists for convenience. – E_net4 Aug 22 '22 at 17:04
  • To be fair, I'm not as up-to-speed with Rust's implementations, but this truly does feel like redesigning the wheel (and in a dangerous way). I'd have to say that barring a reason that cannot be refuted or done in a Java-idiomatic way, I don't see the reason for what OP is attempting beyond research and curiosity. – Rogue Aug 22 '22 at 17:06
  • _"this truly does feel like redesigning the wheel"_ Yes. It seems to me that all of this is merely an exercise to see how far one could replicate the `Option` construct from Rust into Java, and the lack of constrained implementations was one of the roadblocks. – E_net4 Aug 22 '22 at 17:10