Skip to content
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

For Comprehensions #68

Closed
danieldietrich opened this issue Nov 2, 2014 · 23 comments
Closed

For Comprehensions #68

danieldietrich opened this issue Nov 2, 2014 · 23 comments

Comments

@danieldietrich
Copy link
Member

Haskell: [x^2 | x <- [1..5]]
-- produces [1,4,9,16,25]

Javaslang: Gen.range(1,5).map(x -> x*x).toList()

or Haskell.eval("[x^2 | x <- [1..5]]").asList()

@danieldietrich danieldietrich added this to the BACKLOG milestone Nov 2, 2014
@danieldietrich
Copy link
Member Author

Another example using multiple generators:

Haskell: [(x,y) | x <- [1,2,3], y <- [4,5]]
-- produces [(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)]

Javaslang:

  • Tuple generator:
    • Gen.tuple(Gen.range(1,3), Gen.range(4,5)).toList()
    • or short tuple(range(1,3), range(4,5)).toList()
    • which is the same as Gen.range(1,3).tuple(Gen.range(4,5)).toList()
    • or short range(1,3).tuple(range(4,5)).toList()
  • Combining generator:
    • Gen.combine(Gen.range(1,3), Gen.range(4,5), (x,y) -> Tuple.of(x,y)).toList()
    • or short combine(range(1,3), range(4,5), (x,y) -> Tuple.of(x,y)).toList()
    • which is the same as Gen.range(1,3).combine(Gen.range(4,5), (x,y) -> Tuple.of(x,y)).toList()
    • or short range(1,3).combine(range(4,5), (x,y) -> Tuple.of(x,y)).toList()

or Haskell.eval("[(x,y) | x <- [1,2,3], y <- [4,5]]").asList()

@danieldietrich
Copy link
Member Author

Dependent generators:

Haskell: [(x,y) | x <- [1..3], y <- [x..3]]
-- produces [(1,1),(1,2),(1,3),(2,2),(2,3),(3,3)]

Javaslang:

  • Gen.range(1,3).tuple(x -> Gen.range(x, 3)).asList()
  • or Gen.range(1,3).combine(x -> Gen.range(x, 3), (x,y) -> Tuple.of(x,y)).asList()

or Haskell.eval("[(x,y) | x <- [1..3], y <- [x..3]]").asList()

@danieldietrich
Copy link
Member Author

Another example using more than two dependent generators:

Haskell: [(x,y,z) | x <- [1..3], y <- [x..3], z <- [y..3]]
-- produces [(1,1,1),(1,1,2),(1,1,3),(1,2,2),(1,2,3),(1,3,3),(2,2,2),(2,2,3),(2,3,3),(3,3,3)]

Javaslang:

  • Gen.range(1,3).tuple(x -> Gen.range(x,3)).tuple((x,y) -> Gen.range(y,3)).asList()
  • or Gen.range(1,3).combine(x -> Gen.range(x, 3), (x,y) -> Tuple.of(x,y)).combine((x,y) -> Gen.range(y,3), (x,y,z) -> Tuple.of(x,y,z)).asList()

or Haskell.eval("[(x,y) | x <- [1..3], y <- [x..3]]").asList()

@danieldietrich
Copy link
Member Author

Applications of comprehensions: list concatenation

Haskell:

concat    :: [[a]] -> [a]
concat xss = [x | xs <- xss, x <- xs]

Example:

> concat [[1,2,3],[4,5],[6]]
[1,2,3,4,5,6]

Javaslang:

final List<List<Integer>> xss = List.of(List.of(1,2,3), List.of(4,5), List.of(6));
Gen.of(xss).flatMap(xs -> Gen.of(xs)).toList();

// in this case the same as
xss.flatMap(xs -> xs);

// or just
xss.flatten();

@danieldietrich danieldietrich modified the milestone: BACKLOG Jan 10, 2015
@danieldietrich danieldietrich added this to the 1.3.0 Stream milestone Feb 7, 2015
@danieldietrich danieldietrich removed this from the 1.3.0 Stream milestone Apr 16, 2015
@jonas-grgt
Copy link

I've been thinking about this and I find the following syntax nicer then the initial proposed one.

Haskell: [x^2 | x <- [1..5]]
-- produces [1,4,9,16,25]

Javaslang: List.comprehend(x -> x*x, myList);

This could even be taken a little bit further by adding a predicate.

Javaslang: List.comprehend(x -> x*x, myList, x -> x < 10);

Or if you want to produce a List of a different type then the initial type of the input list:

Javaslang: List.comprehend(x -> String.valueOf(x*x), myList, x -> x < 10, String.class);

The comprehend method could also be an instance method of javaslang.collection.List

myList.comprehend(x -> x*x, x -> x < 10);

@danieldietrich
Copy link
Member Author

I like your suggestion! Do you want to contribute it? :-)

@jonas-grgt
Copy link

Sure!

@danieldietrich danieldietrich modified the milestones: 2.0.0, 2.0.1 Jan 14, 2016
@pablogrisafi1975
Copy link

May I ask a few questions?

  1. I don't get the point of comprehensions over one List. Isn't

    List.comprehend(x -> String.valueOf(x*x), myList, x -> x < 10, String.class);

Exactly the same as

` list.filter(x -> x < 10).map(x -> String.valueOf(x * x));

am I missing something?

  1. I really want to have comprehensions over multiple lists. One imperative pattern I see a lot in my business oriented code is something like

for(Heade header: headerList){
    for(Detail detail: header.getDetails()){
        for(SubDetail subDetail : details.getSubDetail()){
            dosomething(header, detail, subdetail);
        }
    }
} 

I haven't found a way to do that in a more functional way, I wish list comprehensions can transform this into something like

List<Tuple3<Header, Detail, SubDetail>>> tuples = List.comprehend(
     headerList, 
     h -> h.getDetails(),
     d -> d.getSubDetails());
tuples.map(t - > doSomething(t._1, t._2, t._3));

Can that be made? I was expecting an API that accept Iterables and java.util.Stream, so we can use comprehend old java.util.List without pain. Something like

List<Tuple<T1, T2, T3>> = 
List.comprehend(Iterable<T1>, Function<T1, Iterable<T2>>, Function<T2, Iterable<T3>>);

List<Tuple<T1, T2, T3>> = 
List.comprehend(Iterable<T1>, Function<T1, Stream<T2>>, Function<T2, Iterable<T3>>); 

I know that expanding this to 1-to-8-arity and accept every combination of javaslang.List, java.util.Iterable, java.util.Stream and javaslang.Stream would be pretty much imposible without code generation, but it would be so nice to be able to write

List.comprehend(headerList, 
     h -> h.getDetails().stream().filter(d -> h.getType().equals("thisType")),
     d -> d.getSubDetails()
).each(whatever....)

Anyway, thanks for the wonderful job!

@danieldietrich
Copy link
Member Author

@pablogrisafi1975 Hi, thank you for your great suggestions! I think you are right. The proposed list-comprehension API is similar to implement it directly.

I'm planning to create for-comprehensions (from Scala) instead of list-comprehensions (from Haskell). It will look like this

For(
    interable_1,
    ...,
    iterable_n
).yield(
    (param_1, ..., param_n) -> ...
);

If the iterable_k is a Javaslang collection, we may also filter (similar to if-guards in Scala):

For(
    iterable_1.filter(condition_1),
    ...,
    iterable_n.filter(condition_n)
).yield(
    (param_1, ..., param_n) -> ...
);

Note: Option, Try, Future et al. are also Iterable and will work nice with this approach.

We need a static For method for arities 0..8. These will be generated. Because the upcoming structural pattern matching Match has a similar API (also static Match methods), I consider to introduce a class javaslang.Predef (similar to scala.Predef), which will include standard API like Match, For and others.

@danieldietrich
Copy link
Member Author

I push this to 2.0.0 because this will be a quick win and code generation goes along with #1087

@danieldietrich danieldietrich modified the milestones: 2.0.0, 2.0.1 Feb 19, 2016
@danieldietrich
Copy link
Member Author

@jonasgeiregat I think we will move from List comprehensions to For comprehensions as described above.

@pablogrisafi1975 is right, the Haskell term [x^2 | x <- [1..5]] is the same as

List.rangeClosed(1, 5).map(x -> x * x);

and myList.comprehend(x -> x*x, x -> x < 10); is the same as

myList.filter(x -> x < 10).map(x -> x * x);

I will change the name of this issue to For comprehensions.

There is one remaining question: What is the result type of the For comprehension?

@danieldietrich danieldietrich changed the title List Comprehensions For Comprehensions Feb 19, 2016
@danieldietrich
Copy link
Member Author

In Scala the For comprehension operates on filter-monadic types. Internally a for-call is translated to a cascade of flatMap...flatMap...map calls.

If we use Iterables, this is not possible. We could

  • fix the result type by default
  • add a result factory parameter

However, finally the results need to be flattened then.

@pablogrisafi1975
Copy link

Maybe I'm missing something, but isn't the 'natural' return type a Stream<Tuple<T1...Tn>> ?
I think it is a flexible enough approach, and if you want a specific type, you simply map into it.

Am I suggesting Stream Comprenhensions....?

@pablogrisafi1975
Copy link

Maybe event better would be a specific TupleStream<T1...Tn> extends Stream<T1...Tn> that also has a specific method yield(T1 e1 ...... Tn en) so people can use it as a enhaced for without even noticing a tuple beahind, if they want to.

@danieldietrich
Copy link
Member Author

We should do it the Scala way. Everyone has this in mind when it comes to for-comprehensions:

for {
    a_1 <- filterMonadic_1
    a_2 <- filterMonadic_2
    ...
    a_n <- filterMonadic_n
} yield (a_1, a_2, ..., a_n) => ...

which is just syntactic sugar for

filterMonadic_1.flatMap(a_1 =>
    filterMonadic_2.flatMap(a_2 =>
        ...
        filterMonadic_n.map(a_n => ...)...));

I will think about it.

@danieldietrich
Copy link
Member Author

We need to ship 2.0.0 - will bump that to 2.0.1.

@danieldietrich danieldietrich modified the milestones: 2.0.1, 2.0.0 Feb 19, 2016
@danieldietrich
Copy link
Member Author

I've spent a while thinking about For-comprehensions. A full-blown solution would provide this:

  • preserve flatMap/map semantics in order to ensure monadic operations

  • the above implies that yield returns a Value of the first iterable type

  • provide if-guards for each Iterable that allows to filter the current Iterable based on the known elements

    for {
        a_1 <- filterMonadic_1
        a_2 <- filterMonadic_2 if (a_2 == a_2)
        ...
        a_n <- filterMonadic_n if (a_n + "" + a_1 == "test")
    } yield (a_1, a_2, ..., a_n) => ...

I was yet not able to figure out a clean and concise way to express the above with Java. However, this should not be a reason to abandon syntactic sugar for cascaded iteration over multiple iterables. Therefore I prefer a pragmatic solution.

  • We provide multiple Iterables without if-guards. filter may be applied as usual but not in the context of elements of other iterables.

  • The result is an Iterator. Conversions to Javaslang Value types and to java.util types are possible, e.g.

    For(
        iterable_1,
        iteratble_2.filter(condition),
        ...
        iterable_m
    ).yield(a_1, a_2, ..., a_n) -> ...).toOption(); // transforms only the first tuple if present

Please note that m <= 8 because the yield function has a maximum arity of 8.

The interesting part is to define a javaslang.collection.Iterator that iterates over the following cross product:

iterable_1 X iterable_2 X ... X iterable_m

@danieldietrich
Copy link
Member Author

The implementation will look like this:

public interface API {

    static <T1> For1<T1> For(Iterable<T1> ts1) {
        return new For1<>(Stream.ofAll(ts1));
    }

    static <T1, T2> For2<T1, T2> For(Iterable<T1> ts1, Iterable<T2> ts2) {
        return new For2<>(Stream.ofAll(ts1), Stream.ofAll(ts2));
    }

    class For1<T> {

        Stream<T> stream1;

        For1(Stream<T> stream1) {
            this.stream1 = stream1;
        }

        <R> Stream<R> yield(Function<? super T, ? extends R> f) {
            return stream1.map(f);
        }
    }

    class For2<T1, T2> {

        Stream<T1> stream1;
        Stream<T2> stream2;

        For2(Stream<T1> stream1, Stream<T2> stream2) {
            this.stream1 = stream1;
            this.stream2 = stream2;
        }

        <R> Stream<R> yield(BiFunction<? super T1, ? super T2, ? extends R> f) {
            return stream1.flatMap(t1 -> stream2.map(t2 -> f.apply(t1, t2)));
        }
    }
}

@danieldietrich
Copy link
Member Author

Low hanging fruits - I will write the code generator now...

@danieldietrich danieldietrich modified the milestones: 2.0.0, 2.0.1 Feb 21, 2016
@danieldietrich
Copy link
Member Author

Works like a charm:

import static javaslang.API.*;

...

Stream<String> result = For(
        Option.of(1),
        Stream.of("a", "b", "c"),
        List.of('ä', 'ö', 'ü')
).yield((i, s, c) -> i + s + c);

// [1aä, 1aö, 1aü, 1bä, 1bö, 1bü, 1cä, 1cö, 1cü]
System.out.println(result.toJavaList());

I will add tests and merge it into master.

@danieldietrich
Copy link
Member Author

@pablogrisafi1975 @jonasgeiregat thanks for your assistance. I think this feature is really helpful! See #1128 for details on usage.

@pablogrisafi1975
Copy link

Excellent!
I just realize you are basically using Scala to make Java look like Scala...I should learn some Scala.
Thank you!

@danieldietrich
Copy link
Member Author

Yes, it is the convergence of languages. Martin Odersky's free online course Functional Programming Principles in Scala is one of the best learning resources for modern and advanced programming I know - especially for Java developers.

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

No branches or pull requests

3 participants