Vavr: Any thoughts on adding a "Just" type?

Created on 5 May 2017  路  7Comments  路  Source: vavr-io/vavr

Looking to start a discussion on whether there might be value in having a Just type representing the Just monad. There have been lots of times recently when implementing code using this library where I wished my regular objects supported .map and many of the other methods of Value, just to keep my code concise and fluent, but the API is centered around returning the object itself.

Could be easy enough by adding a Value value() to the Value interface, and then overriding it everywhere that get() is overridden. Probably would also have to add methods like valueOrElse etc. too. Thoughts?

question 芦vavr-core禄

Most helpful comment

Hi @danieldietrich,

Yes, I reacted with +1 since I probably have a scenario where Just (as far as I understood it) will be the missing _being_. In my project there is a class called EmailFactory which is responsible.. for creating emails. It has a single method createEmail which accepts single argument, a DTO class, named EmailData. EmailData is passed as is - it's not wrapped in any container (Option, whatever), hence I've no starting point for further processing which looks like below:

public void createEmail(EmailData data) {
        notNull(data, "Email data cannot be null!");
        final EmailDraft draft = Option
            .of(data)
            .map(EmailData::getClass)
            .map(EmailTemplateType::forDataClass)
            .map(templateLoader::findByType)
            .map(template -> draftFactory.createDraftWith(template, data))
            .get();

        if (data.hasNoRecipients()) {
            log.info("No recipients for e-mail with title `{}`", draft.getSubject());
        }

        data.getRecipients().forEach(recipient ->
            messageRepository.save(new EmailMessage(draft.getSubject(), recipient, draft.getContent()))
        );
    }

and I need to start with Option whereas Just looks like a perfect fit here. It represents a Value that has some _value_ set. I don't want to accept Option argument since I've read this is not a good practice. Also I don't think if an Option is good starting point here.

All 7 comments

Hi,

there might be value in having a Just type representing the Just monad.

I'm not aware of a Just monad. Do you mean Haskell's Maybe monad, that has Just (representing a defined value) and Nothing (representing the empty state)?

The Maybe monad is exactly our Option (the only difference is the name).

I wished my regular objects supported .map and many of the other methods of Value

I do not understand that. What is a 'regular' object? Value is an interface, you are able to implement it. There is no default implementation for .map(), so you have to specify what the semantics of map() is in the context of your 'regular' object.

I do not understand the use-case of your suggestion to add a Value value() method. Do you have some code that shows a use-case of a value() methods?

You're are already discussing technical solutions but you haven't specified the problem that should be solved. The chance is high, that there is already a solution present.

(@Opalo you reacted with +1, maybe you also have an example/a use-case)

Here's my guess at what he means with 'Just'.

The vavr abstractions for data transformation are quite handy. I think he wants those abstractions starting out from a single value. __RxJava__ is a different kind of data transformation abstraction (push-based, can be asynchronous), but the idea is similar. They have a just method to start out from a single value and chain transformations from there in a fluent API.

__IxJava__ is another example, which is something like Iterables on steroids. It's also a data transformation library (pull-based, synchronous, lazy), it's practically RxJava's synchronous dual, and functionally it's quite similar to vavr's data transformation abstractions. It also has a just method. The particularily nice feature of IxJava is that it has really no dependencies, you can feed in the most simple thing as an Iterable and end up with another Iterable (you can collect into Collections similarly to Java Streams) and the whole thing is lazy, i.e., you only pay for what you're pulling out from the Iterable. It's a pity that people don't know about this nice lib, since it's low popularity might seal it's fate in the long run.

These libraries are maintained by the same guy, BTW, D谩vid Karnok, and they're awesome. He also has some nice performance measurement comparing different data processing libraries and he describes the results on his blog.

I think @durron597 meant that he would like to start out from a single value just as you can start out from a single value with these libraries and build your way up from there through the fluent API. Maybe a <T>聽Value<T>聽Value.just(T) method so you can start from there and map/flatMap/etc your way up from there. Probably backed by a Single type, which could stay package-private. Not sure what significant difference would that bring compared to Array.from(T), besides probably one less indirection (single element array vs direct reference) and isSingleValued().

I started writing an answer this answer differently, but then I realized you have something that's fairly close to what I'm trying to get at: Lazy. Perhaps using Lazy.of(() -> myObj) would work in many cases, but I'd have to wrap lots of things in it using Lazy.of instead .getLazy


Stepping back from that for a moment, consider code like:

public Option<Quux> chainSomeMonads(Option<Foo> foo) {
   return foo.flatMap(this::toBar)
                   .flatMap(this::toBaz)
                   .flatMap(this::toQuux);
}

But what if, in my use case, I know I'm going to have an object every single time? With Value, you could certainly do:

public Value<Quux> chainSomeMonads(Value<Foo> foo) {
   return foo.flatMap(this::toBar)
                   .flatMap(this::toBaz)
                   .flatMap(this::toQuux);
}

Well, actually, I can't, because:

flatMap signatures are manifold and have to be declared by subclasses of Value.

But hopefully you see what I'm driving at? This would be a way to do it, obviously:

public Quux chainSomeMonadsSortOf(Foo foo) {
    return toQuux(toBaz(toBar(foo)));
}

But it doesn't match the style of the rest of the syntactic flavor. Also I can't easily call toList or toMap or any of the other nice things Value provides.

In my ideal ideal world, Object would extend Value. But it doesn't, so we have to hack around it.

There are of course lots of ways to do things like what I'm driving at today. For example, I can call Lazy.of(() -> foo). I can also do Array.from(T), as pointed out by @nfekete. But none of these have the syntactic elegance that makes Vavr such a large improvement over vanilla Java. In other words, this isn't really a "feature request" in the sense that I'm asking to be able to do something new that the library can't already do. I'm asking for a syntactic sugar that could make some code cleaner in many cases.

Hi @danieldietrich,

Yes, I reacted with +1 since I probably have a scenario where Just (as far as I understood it) will be the missing _being_. In my project there is a class called EmailFactory which is responsible.. for creating emails. It has a single method createEmail which accepts single argument, a DTO class, named EmailData. EmailData is passed as is - it's not wrapped in any container (Option, whatever), hence I've no starting point for further processing which looks like below:

public void createEmail(EmailData data) {
        notNull(data, "Email data cannot be null!");
        final EmailDraft draft = Option
            .of(data)
            .map(EmailData::getClass)
            .map(EmailTemplateType::forDataClass)
            .map(templateLoader::findByType)
            .map(template -> draftFactory.createDraftWith(template, data))
            .get();

        if (data.hasNoRecipients()) {
            log.info("No recipients for e-mail with title `{}`", draft.getSubject());
        }

        data.getRecipients().forEach(recipient ->
            messageRepository.save(new EmailMessage(draft.getSubject(), recipient, draft.getContent()))
        );
    }

and I need to start with Option whereas Just looks like a perfect fit here. It represents a Value that has some _value_ set. I don't want to accept Option argument since I've read this is not a good practice. Also I don't think if an Option is good starting point here.

Hi all (@durron597, @Opalo, @nfekete),

I see three options:

  1. Adding a static factory method static <T> Value<T> just(T obj) { ... } to Value.
  2. Introducing a new Monad called Just or Id
  3. Using method chaining
  4. Using existing monads

Here are my thoughts:

Ad 1) We return an instance of type Value, i.e. an internal implementation. As @durron597 said, Value can't have flatMap() because of Java's type system limitations. Therefore this is no option for us.

Ad 2) We could add a new Monad that wraps a Value. It has to be always defined (otherwise it would be exactly like Option). But then filter() cannot return an instance of the new Monad type because we have no 'empty' state. It would have to return Option (like Lazy's filter() does, because a Lazy value is always defined by definition). Do we really want that?

Ad 3) Method chaining does only cover the map() method. So this might help in some cases.

Ad 4) Our existing already cover many use-cases along the call chain, e.g. undefined values (Option), presence of errors (Try), lazy evaluation (Lazy, Stream), ...

Summed up I suggest to use one of the existing Monads, the one that fits best the use-case. E.g. in @Opalo's example the App would be more robust if we had an empty state for templateLoader::findByType. I would use Option or Try here.

I would prefer Option or Try over Lazy (to wrap a single value) because lazy internally create additional Suppliers.

I would prefer Option or Try over List / Array (to wrap a single value) because it is a bit more memory efficient.

For simple transformation pipelines function chaining works well.

Greets

- Daniel


@nfekete the method isSingleValued() tells us something about the _type_, not about the _instance_. E.g. Option and Try are single-valued but List(1) isn't. This is important for us in the case we need to gather some information about a general Value instance at runtime. There are more introspection methods (also for collections), e.g. isLazy(), isAsync(), isTraversableOnce(), ...

@durron597 Thank you for asking the question. And thanks to all other participants. I will close the ticket now.

@danieldietrich now that we've hashed it out, I agree with closing the ticket as won't-fix. Very glad we had the conversation though.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

roookeee picture roookeee  路  5Comments

ronanM picture ronanM  路  3Comments

nfekete picture nfekete  路  5Comments

carnott-snap picture carnott-snap  路  4Comments

HiDAl picture HiDAl  路  3Comments