Skip to content

Add Value.to() general conversion methods #3018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

joytools
Copy link

@joytools joytools commented May 15, 2025

There are lot of useful type conversion to turn a Value into another well known data type, such as java.util.List, java.util.Map, io.vavr.collection.Map, etc.

The goal of this request is adding to Value a new generic to(Function) method in order to enable the conversion of a Value into a custom type by using a fluent syntax, as per following example:

record Book(int id, String title, String author) { }
    
class Library {
    Library(Iterable<Book> books) {
        this.books = Vector.ofAll(books);
    }
    public void display() {
        books.forEach(System.out::println);
    }
    private final Seq<Book> books;
}
    
void test() {
    Stream.of(1, 2, 3)
        .map(id -> new Book(id, "Book " + id, "John Doe"))
        .to(Library::new)
        .display();
}

This is also useful to enhance interoperability with other library data types, such as:

  • Guava -> Table, ImmutableList, etc
  • Apache Commons -> Bag, MultiMap, etc

There are other 3 variants to deal with more advanced convertions when the conversion Function takes in input an Iterable of Tuple2, Tuple3 or Tuple4, as per following example:

class SortedLibrary {
    SortedLibrary(Iterable<Tuple2<Integer, Book>> books) {
        this.books = TreeMap.ofEntries(books);
    }
    public void display() {
        books.forEach(System.out::println);
    }
    private final SortedMap<Integer, Book> books;
}
    
void test() {
    Stream.of(1, 2, 3)
            .to(id -> id * 2,                                 // Key mapper
                id -> new Book(id, "Book " + id, "John Doe"), // Value mapper
                SortedLibrary::new)
            .display();
}

@joytools joytools requested a review from pivovarit as a code owner May 15, 2025 14:42
@pivovarit
Copy link
Member

Thanks for your submission! I got it, and I need some time to think it through. I need to weigh pros and cons - there's some overlap with collect() and toMap(km, vm).toStream(), and I needto play with it for a bit - we're getting close to releasing a new major version and new APIs are likely to stay for a very long time

@joytools
Copy link
Author

joytools commented May 20, 2025

@pivovarit wrote:
Thanks for your submission! I got it, and I need some time to think it through. I need to weigh pros and cons - there's some overlap with collect() and toMap(km, vm).toStream(), and I needto play with it for a bit - we're getting close to releasing a new major version and new APIs are likely to stay for a very long time

IMHO collect() is more a compatibility layer between Vavr and JDK's stream API, so that you can use existing java.util.stream.Collector with io.vavr.Value based types.

The new to(Function) method proposed here aims to be a generalized version of the various Vavr's toXXXX() conversion methods.

To illustrate the idea, let's see how to rewrite some of the existing toXXXX() methods using to(Function)

Value.toArray() --> Value.to(Array::ofAll)
Value.toList() --> Value.to(List::ofAll)
Value.toLinkedSet() --> Value.to(LinkedSet::ofAll)
Value.toLinkedMap() --> Value.to(k -> k, v -> v, LinkedHashMap::ofEntries)

I am using a lot this approach combined with my JoyTools Commons Library containing many singleton conversion functions:

Element : get an element from any Iterable.

  • only()
  • first() / last()
  • firstOrNull() / lastOrNull()
  • firstOrFail() / lastOrFail()
  • firstOrElse(defValue) / lastOrElse(defValue)

Java : conversion to JDK data structures.

  • array(Class)
  • arrayList()
  • optional()
  • hashMap() / linkedHaspMap() / treeMap() / concurrentHashMap()
  • hashSet() / linkedHashSet()
  • etc

Guava : conversion to Guava's data structures.

  • hashBasedTable() / treeBasedTable() / treeBaseTableIgnoreCase() / immutableTable()
  • hashBiMap() / immutableBiMap()
  • etc

Vavr : conversion to Vavr'sdata structures.

  • array()
  • hashMap() / linkedHashMap() / treeMap() / treeMapIgnoreCase()
  • hashSet() / linkedHashSet() / treeSet() / treeSetIgnoreCase()
  • stream()
  • option() / some() / none()
  • etc

Let's combine all the above with some easy examples:

Get the only existing element

Stream.range(0, 1000)
    .map(...)
    .filter(...)
    .to(Element.only());

Get the first existing element as a Java Optional and throw an exception if multiple elements are found.

Stream.range(0, 1000)
    .map(...)
    .filter(...)
    .to(Java.optional());

Similar to the above, get the first existing element as a Vavr Option and throw an exception if multiple elements are found.

Stream.range(0, 1000)
    .map(...)
    .filter(...)
    .to(Vavr.option());

Create a new Guava BiMap

Vector.of("a", "b", "c")
    .to(Function.identity(), // map keys
        v -> v + v + v,  // map values
        Guava.immutableBiMap());

Create a new case insensitive Vavr SortedMap

Vector.of("a", "b", "c")
    .to(Function.identity(), // map keys
        v -> v + v + v,  // map values
        Vavr.treeMapIgnoreCase());

Create a new case insensitive Java SortedMap

Vector.of("a", "b", "c")
    .to(Function.identity(), // map keys
        v -> v + v + v,  // map values
        Java.treeMapIgnoreCase());

Create a new Java concurrentHashMap

Vector.of("a", "b", "c")
    .to(Function.identity(),   // map keys
        v -> v + v + v,  // map values
        Java.concurrentHashMap());

Create a new Guava Table

Vector.of("a", "b", "c")
    .to(Function.identity(), // map rows
        v -> v + v + v,    // map columns
        v -> v.length(),   // map cells
        Guava.hashBasedTable());

The advantages of this approach:

  • the syntax is elegant and fluent.
  • if a new conversion method is needed, it is just matter of creating a new singleton conversion function instead of adding new toXXXX() methods in Vavr.
  • the conversion functions may be implemented to work on any Iterable, not just on Vavr data types.

Hope this may help to clarify why I consider this proposal so interesting and stylistically noteworthy ;-)

@pivovarit
Copy link
Member

So, I considered both options, and I think:

  • a lot of those use-cases could be implemented with a dedicated to() Collector: integers.collect(to(...)), but this involves iteration which is not always needed, for example, when we just want to wrap the result
  • to() as a new method allows skipping iteration when not needed and requires way less hassle than integers.collect(to()))
  • to() with additional Tuple mappings is redundant - the same can be achieved by using map().to(), so I'd drop those companion methods for Tuple mappings and consider either expanding map() to accept multiple mapping functions

The new to() method should make better use of generics: at the moment, users are forced to provide a function of Iterable, which can be problematic in cases like:

image
import io.vavr.collection.List;
import java.util.Collections;

class Main {
    public static void main(String[] args) {
        List<Integer> integers = List.of(1);
        // reason: no instance(s) of type variable(s) T exist so that Iterable<Integer> conforms to List<? extends T>
        List<Integer> r = integers.to(l -> Collections.unmodifiableList(l));
    }
}

We also need tests :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants