vavr-io / vavr

vʌvr (formerly called Javaslang) is a non-commercial, non-profit object-functional library that runs with Java 8+. It aims to reduce the lines of code and increase code quality.
https://vavr.io
Other
5.7k stars 633 forks source link

[Analysis] How to add a Monad abstraction on top of existing monadic classes. #300

Closed danieldietrich closed 8 years ago

danieldietrich commented 9 years ago

Here is a Scala example, taken from @travisbrown's abstracted. We need to check what is possible with Java in this direction.

Due to the lack of higher-kinded types, clearly Java's type system is not powerful enough to express this. The intention of this analysis story is, to investigate if we are able to create a Monad abstraction on top of existing classes without modifying them.

case class Box[A](val a: A) {
  def map[B](f: A => B): Box[B] = {
    println("Box's map")
    Box(f(a))
  }

  def flatMap[B](f: A => Box[B]): Box[B] = {
    println("Box's flatMap")
    f(a)
  }
}

And a monad instance for it:

import cats.Monad

implicit val boxMonad: Monad[Box] = new Monad[Box] {
  override def map[A, B](fa: Box[A])(f: A => B): Box[B] = {
    println("Box's functor's map")
    Box(f(fa.a))
  }

  def flatMap[A, B](fa: Box[A])(f: A => Box[B]): Box[B] = {
    println("Box's monad's flatMap")
    f(fa.a)
  }

  def pure[A](a: A): Box[A] = Box(a)
}
johnmcclean commented 9 years ago

Hi Daniel,

There are a couple of things in Cyclops, a relatively new project from Aol, which has a primary focus on interoperability across the different functional style libraries that are emerging, which might be helpful here.

This is still an evolving work in progress - there are a few interfaces that might be helpful for your analysis (either for ideas, reuse or for defining an interoperability standard).

The Monad interface wraps any Java Monad implementation, and makes some attempt to retain type information about the monad's type and the type it targets (i.e. both Stream<String> and String). This can be quite messy in practice. Monad source code

The AnyM class simplifies things by dropping the type information for the Monad being managed (i.e. it doesn't store Stream<String> just String). AnyM source code

In both cases the underlying managed monad instance can be accessed again via unwrap(), and it is possible to flatMap to any monad type, so in the case of AnyM it is always possible to extract a monad of a given type, in a type safe way.

Monad operations are managed under the hood by 'Comprehenders' implementations of the Comprehender interface. These facilititate the existence of the Monad wrapping classes and for comprehensions.

The plan is to create modules in Cyclops for Javaslang that would define Comprehenders for any monads within Javaslang and offer conversion between Javaslang types (and functionaljava, jdk, Guava, TottallyLazy). There are prototypes for some of these conversions already - they could just as easily live in Javaslang as Cyclops (perhaps it would be better?).

What do you think?

danieldietrich commented 9 years ago

@johnmcclean-aol This sounds great. I also see the emerging 'diversification' of functional style libs in the Java eco-system. My intention creating Javaslang was (the same as for everyone else) to add missing functionality to Java, which in turn was designed with the focus to be backward compatible. I would love to see the new libs converging to a standard. Waiting for Java to include things does not help, it will take too long.

I'm currently busy with reviewing two new functional Java books. Soon I'm able to get back to this issue. I will take a look at your project and give you feedback.

mperry commented 9 years ago

@johnmcclean-aol @danieldietrich Hi John, I will take the time to look at this in the next week. I haven't looked at the details of your library, how do you handle the type safety and the lack of kinds in your library?

johnmcclean commented 9 years ago

@danieldietrich @mperry Thanks guys. When I started working on this the goal was to provide away to get your projects, and the JDK/ Guava /TotallyLazy etc, to work together seamlessly!

Mark, under the hood this was very heavily influenced by the fantastic work you did in FunctionalGroovy (e.g. Groovy For Comprehensions). It started off totally dynamic and types have been added gradually. It's a process and it's definitely not complete (particularly for For Comprehensions).

I think type safety for AnyM is reasonably good (although I'm sure there are still bugs / holes to be found), at least if the Monad/ Monad-like class being wrapped takes a single generic parameter. You can create a factory method such as

 public static <T> AnyM<T> anyM(CompletableFuture<T> anyM){
      return new MonadWrapper<>(anyM).anyM();
 }

and calling it will preserve the type e.g.

CompletableFuture<Data> cf;
AnyM<Data> monadWithData = asAnyM(cf);

Types would then change as normal when map / flatMap etc are applied.

AnyM<String> monadWithString = monadWithData.map(data -> data.toString());

One concern would be, if you needed to get back to the wrapped CompletableFuture, AnyM could be anything. But something along these lines should work, and be guaranteed to always return a CompletableFuture (i.e. if monadWithString wraps an Optional, we will still get a CompletableFuture back).

CompletableFuture<String> cf2 = asAnyM(CompletableFuture.completedFuture(""))
                                                        .bind( str -> monadWithString.unwrap())
                                                        .unwrap();

It's a little long winded, if you wanted to live dangerously, the following would work in our case (but blow up if code elsewhere was passing a Stream or a Try etc).

 CompletableFuture<String> cf2 = monadWithString.unwrap();

Internally when a method is called, the actual method to be invoked on the wrapped Monad will be resolved by a Comprehender implementation. If no specific Comprehender has been implemented for the Monad type being wrapped, an InvokeDynamicComprehender will attempt pass on the method invocation.

johnmcclean commented 9 years ago

@danieldietrich Hi Daniel, cyclops-javaslang integration is now available in Maven Central. There are converters to Javaslang types (Tuples, Functions, Streams etc) from Guava, JDK, FunctionalJava and simple-react. But also included are Comprehenders for Javaslang monads (as of Javaslang 1.2.2).

This means you can use Cyclops For Comprehensions (which are now strongly typed) with Javaslang, JDK and Cyclops Monads. E.g. Example below shows a mixed For Comprehension with a JDK CompletableFuture and a Javaslang List.

   CompletableFuture<String> future = CompletableFuture.supplyAsync(this::loadData);
   CompletableFuture<List<String>> results1 = Do.add(future)
                                                    .add(Javaslang.anyM(List.of("first","second")))
                                                    .yield( loadedData -> localData-> loadedData + ":" + localData )
                                                    .unwrap();

Where this::loadData returns "loaded" the output from

 System.out.println(results1.join());

is

  List(loaded:first, loaded:second)

With the whole thing being executed asyncrhonously.

You can also use the AnyM abstraction directly with Javaslang monads (AnyM is released as part of Cyclops 5.0.0). E.g. here is AnyM treating a Javaslang Try as Success biased (default behaviour)

    assertThat(Javaslang.anyM(Try.of(this::success))
        .map(String::toUpperCase)
        .toSequence()
        .toList(),equalTo(Arrays.asList("HELLO WORLD")));

And a similar example, treating a Javaslang Try as Failure biased

   Exception e = new RuntimeException();
assertThat(Javaslang.anyMFailure(new Failure(e))
            .toSequence()
            .toList(),equalTo(Arrays.asList(e)));

I think we can regard this as something like a first draft, I think we can significantly improve this over time (for example by creating a Javaslang native for comprehension builder) - if there is interest to do so..

These links might be interesting

Cyclops for comprehensions Cyclops for comprehensions Javadoc Cyclops AnyM javadoc

@mperry Hi Mark, I would hope to have a similar module available for FunctionalJava over the next couple of weeks.

danieldietrich commented 9 years ago

@johnmcclean-aol Awesome, thanks for doing all the work. I will check it out.

danieldietrich commented 9 years ago

This may go along with #288. I've a proposal in the pipeline. There will be an article/blog post soon...

danieldietrich commented 8 years ago

This analysis is finished. We re-added the Monad-interface to Javaslang 2.0.0-RC*. We lift functions to monadic functions now.