Hey there. We use Javaslang quite extensively, but have found that we use the type Try<Option<T>> quite a bit. While this on its own is fine, it becomes unwieldy to compose multiple instances of these together. So I've created a monad transformer for them called TryOptionT<T>. These are obviously similar to Scalaz's or Cats's monad transformers. We've found this new type to be quite a bit simpler to use.
I wanted to gauge if there would be any interest in incorporating this type into Javaslang?
I'm sure it's clear how the code would work, but here's an example of composing without the transformer, and an example of composing with the transformer.
Try<Option<String>> t1 = Try.success(Option.some("Hello"));
Try<Option<String>> t2 = Try.success(Option.some("World"));
Try<Option<String>> t3 =
t1.flatMap(o1 ->
t2.map(o2 ->
o1.flatMap(oo1 ->
o2.map(oo2 -> oo1 + oo2)
)
)
);
TryOptionT<String> to1 = TryOptionT.of(() -> "Hello");
TryOptionT<String> to2 = TryOptionT.of(() -> "World");
TryOptionT<String> to3 =
to1.flatMap(o1 ->
to2.map(o2 -> o1 + o2)
);
The clarity is much greater, and I've added several helper methods as well.
Anyway, not sure if this would fit in with Javaslang, but we've found it highly useful and would love to help get it into Javaslang if others agreed. We'd love to make it more generic than specific to just Try<Option<T>>, but that's pretty dang hard without higher kinded types (which is why it's much more generic in Scala like OptionT[F[_], A]). Any feedback is certainly welcome. Thanks!
Hi Eric!
thank you for sharing your use-cases, it is very valuable to hear how Javaslang is used in real world applications.
I've thought about it and think expressing compositional characteristics on the type system is too much for Java. As you said, Java's type system is not able to express that level of abstraction.
From the designer-viewpoint composed types like TryOptionT do not scale very well for a library like Javaslang. I'm sure that feature requests will follow because there are use-cases for TryFutureT, OptionTryT, ...
I don't want to say just 'no' without having an alternate solution. When looking at your code we see two times flatMap.map flatMap.map. But in fact these are different. Let's take a look at t3:
Try<Option<String>> t3 =
t1.flatMap(o1 ->
t2.map(o2 ->
o1.flatMap(s1 ->
o2.map(s2 -> s1 + s2)
)
)
);
Towards 2.1.0 we will add more API.For methods (see #1629). Given these, we are able to express t3like this:
Try<Option<String>> t3 = For(t1, t2).yield(this::combine);
I would suggest to refactor the code and pull the combine operation out of the flatMap/map cascade:
Option<String> combine(Option<String> o1, Option<String> o2) {
return For(o1 ,o2).yield((s1, s2) -> s1 + s2);
}
The benefits:
What do you think?
Here is the complete test (compiled using Javaslang 2.1.0-alpha):
| t1 | t2 | t3 | t4 |
| --- | --- | --- | --- |
| Failure(error1) | Failure(error2) | Failure(error1) | Failure(error1) |
| Success(None) | Failure(error2) | Failure(error2) | Failure(error2) |
| Success(Some(Hello)) | Failure(error2) | Failure(error2) | Failure(error2) |
| Failure(error1) | Success(None) | Failure(error1) | Failure(error1) |
| Success(None) | Success(None) | Success(None) | Success(None) |
| Success(Some(Hello)) | Success(None) | Success(None) | Success(None) |
| Failure(error1) | Success(Some(World)) | Failure(error1) | Failure(error1) |
| Success(None) | Success(Some(World)) | Success(None) | Success(None) |
| Success(Some(Hello)) | Success(Some(World)) | Success(Some(HelloWorld)) | Success(Some(HelloWorld)) |
import javaslang.control.Option;
import javaslang.control.Try;
import java.util.Objects;
import java.util.function.BiFunction;
import static javaslang.API.*;
public class TryOptionTest {
private static final Error ERR1 = new Err("error1");
private static final Error ERR2 = new Err("error2");
private static final Object[][] data = new Object[][] {
{ Failure(ERR1), Failure(ERR2) },
{ Success(None()), Failure(ERR2) },
{ Success(Some("Hello")), Failure(ERR2) },
{ Failure(ERR1), Success(None()) },
{ Success(None()), Success(None()) },
{ Success(Some("Hello")), Success(None()) },
{ Failure(ERR1), Success(Some("World")) },
{ Success(None()), Success(Some("World")) },
{ Success(Some("Hello")), Success(Some("World")) }
};
public static void main(String[] args) {
new TryOptionTest().run();
}
@SuppressWarnings("unchecked")
void run() {
System.out.println("| t1 | t2 | t3 | t4 |");
System.out.println("| --- | --- | --- | --- |");
for(Object[] testCase : data) {
final Try<Option<String>> t1 = (Try<Option<String>>) testCase[0];
final Try<Option<String>> t2 = (Try<Option<String>>) testCase[1];
final Try<Option<String>> t3 =
t1.flatMap(o1 ->
t2.map(o2 ->
o1.flatMap(s1 ->
o2.map(s2 -> s1 + s2)
)
)
);
final Try<Option<String>> t4 = For(t1, t2).yield(this::combine);
System.out.printf("| %s | %s | %s | %s |\n", t1, t2, t3, t4);
}
}
Option<String> combine(Option<String> o1, Option<String> o2) {
return For(o1 ,o2).yield((s1, s2) -> s1 + s2);
}
public static <T1, T2> ForTry2<T1, T2> For(Try<T1> ts1, Try<T2> ts2) {
Objects.requireNonNull(ts1, "ts1 is null");
Objects.requireNonNull(ts2, "ts2 is null");
return new ForTry2<>(ts1, ts2);
}
public static <T1, T2> ForOption2<T1, T2> For(Option<T1> ts1, Option<T2> ts2) {
Objects.requireNonNull(ts1, "ts1 is null");
Objects.requireNonNull(ts2, "ts2 is null");
return new ForOption2<>(ts1, ts2);
}
static class ForTry2<T1, T2> {
final Try<T1> try1;
final Try<T2> try2;
ForTry2(Try<T1> try1, Try<T2> try2) {
this.try1 = try1;
this.try2 = try2;
}
<R> Try<R> yield(BiFunction<? super T1, ? super T2, ? extends R> f) {
Objects.requireNonNull(f, "f is null");
return try1.flatMap(t1 -> try2.map(t2 -> f.apply(t1, t2)));
}
}
static class ForOption2<T1, T2> {
final Option<T1> option1;
final Option<T2> option2;
ForOption2(Option<T1> option1, Option<T2> option2) {
this.option1 = option1;
this.option2 = option2;
}
<R> Option<R> yield(BiFunction<? super T1, ? super T2, ? extends R> f) {
Objects.requireNonNull(f, "f is null");
return option1.flatMap(t1 -> option2.map(t2 -> f.apply(t1, t2)));
}
}
static class Err extends Error {
Err(String msg) {
super(msg);
}
@Override
public String toString() {
return getMessage();
}
}
}
Note: Above there is no 'monoid combine', I updated the text accordingly.
Just for the protocol: Option as Monoid would look similar to this (Scala, seen here):
implicit def optionMonoid[A](implicit ev: Semigroup[A]): Monoid[Option[A]] =
new Monoid[Option[A]] {
def empty: Option[A] = None
def combine(x: Option[A], y: Option[A]): Option[A] =
x match {
case None => y
case Some(xx) => y match {
case None => x
case Some(yy) => Some(ev.combine(xx,yy))
}
}
}
I think we have to close this issue because monad transformers are too much for Javaslang at the moment. I suggest to use For-comprehensions instead.
@enelson I will wait for your reply before we close it.
@danieldietrich Totally understand, and I agree. The restrictions of the Java type system limit the usefulness of this specific transformer. It'd be another thing if we could abstract the wrapper type like Scala allows. Figured it was at least worth a mention. :) I haven't made any contributions to Javaslang in a while so I've been wanting to help out again. You guys are making awesome progress on this awesome library!
Thank you for the kind words, Eric! :-)
The work on Javaslang consumes much time and generates no money. But it is so much fun and I am convinced that it is the right direction for Java.
Most probably some of our slang will eventually make it into the language, like pattern matching. We will remove these then in a future major release. We will see...
Most helpful comment
Hi Eric!
thank you for sharing your use-cases, it is very valuable to hear how Javaslang is used in real world applications.
I've thought about it and think expressing compositional characteristics on the type system is too much for Java. As you said, Java's type system is not able to express that level of abstraction.
From the designer-viewpoint composed types like TryOptionT do not scale very well for a library like Javaslang. I'm sure that feature requests will follow because there are use-cases for TryFutureT, OptionTryT, ...
I don't want to say just 'no' without having an alternate solution. When looking at your code we see two times flatMap.map flatMap.map.
But in fact these are different.Let's take a look att3:monoidic combinemonadic bind Operation between o1 and o2Towards 2.1.0 we will add more API.For methods (see #1629). Given these, we are able to express
t3like this:I would suggest to refactor the code and pull the combine operation out of the flatMap/map cascade:
The benefits:
What do you think?
Here is the complete test (compiled using Javaslang 2.1.0-alpha):
| t1 | t2 | t3 | t4 |
| --- | --- | --- | --- |
| Failure(error1) | Failure(error2) | Failure(error1) | Failure(error1) |
| Success(None) | Failure(error2) | Failure(error2) | Failure(error2) |
| Success(Some(Hello)) | Failure(error2) | Failure(error2) | Failure(error2) |
| Failure(error1) | Success(None) | Failure(error1) | Failure(error1) |
| Success(None) | Success(None) | Success(None) | Success(None) |
| Success(Some(Hello)) | Success(None) | Success(None) | Success(None) |
| Failure(error1) | Success(Some(World)) | Failure(error1) | Failure(error1) |
| Success(None) | Success(Some(World)) | Success(None) | Success(None) |
| Success(Some(Hello)) | Success(Some(World)) | Success(Some(HelloWorld)) | Success(Some(HelloWorld)) |