Hacker News new | past | comments | ask | show | jobs | submit login
Java Streams and State (frankel.ch)
82 points by pplonski86 on March 3, 2019 | hide | past | favorite | 31 comments



This is only marginally better, as the computation logic is still "hidden" in the lambda.

This is a very weird statement. It's hidden in exactly the place where it's needed! Where is it hidden from? Where else would you need it? If you're reading the code, it's right there, you don't have to click to definition like you do with Pair::next.

What should I get from reading Pair::next? There's no natural "next" for pairs of numbers. How would I guess that this guy's idea of "next" involves incrementing the first number and then squaring it? To make this equally readable you'd have to rename "Pair" or "next" so that the names were just as informative and easy to read as the lambda itself. So you end up with an unwieldy-but-meaningful name like SquareNumbersIterationState or SquaresSequence.State, which is the classic hallmark of OO programming run amok (aka, the Kingdom of Nouns.)

It's so much nicer to have the lambda inline and see exactly what's happening. One of my favorite things about functional style is that it doesn't force you to noun and name things that are easy to describe but hard to name. Sometimes something can just stand for itself instead of needing an awkward name. I mean, Java isn't the best at this, but this isn't hard to read at all:

  pair -> new Pair(pair.index + 1, Math.pow(pair.index + 1, 2)))


I really like your summary of the situation. I'd add that the author seems to have tried to fix a functional readability problem with OO techniques, ending up with the same kingdom of nouns problem - the worst of both worlds.

I think the functional solution would be to separate increment and transform: Stream.iterate(0, i -> i + 1).map(i -> new Pair(i, Math.Pow(i, 2));


I also believe you can apply the "do one thing per line" here. it's a bit weird to do math directly in the constructor argument.


For the Fibonacci example the author claims:

> Notice how state was introduced? It made the code easier to read.

Correct me if I'm wrong, but the only state in that snippet lives in Stream.iterate(), the Fibonacci object is still immutable and next() on it is a pure function. To me this still looks very much like a functional programming approach.

Which just shows that moving business logic into properly named abstractions is good, no matter the programming paradigm.

The real stateful example is the IncrementSupplier and the impure get() method. I personally don't really like it ( Stream.iterate(0, i -> i + 1) is pretty readable to me), but that's probably just personal taste.


> Notice how state was introduced? It made the code easier to read.

That quote shows that the author kinda misses the point here.

    type FibPair = (Int, Int)

    fibSeed :: FibPair
    fibSeed = (0, 1)

    fibNext :: FibPair -> FibPair
    fibNext (p, v) = (v, v + p)

    fibList :: [FibPair]
    fibList = iterate fibNext fibSeed
The above code does exactly the same as his Fibonacci example, and it's written in pure Haskell. I'd argue the above is way more readable.

I get the following output:

    0 1 1 2 3 5 8 13 21 34
With the following main function:

    main :: IO ()
    main = putStrLn . unwords . map (show . fst) . take 10 $ fibList
Which just takes the first element of each generated tuple, maps it to the string representation, and then adds a space between each number.


I translated your example to Kotlin just for fun, as close as possible. Probably not idiomatic Kotlin, but pretty close, if you ask me.

    typealias FibPair = Pair<Int, Int>
    
    val fibSeed = FibPair(0, 1)
    
    fun fibNext(pv: FibPair) = pv.let { (p, v) -> FibPair(v, v + p) }
    
    fun fibList(): Sequence<FibPair> = generateSequence(fibSeed, ::fibNext)
    
    fun main() {
        fibList().take(10).forEach { (p, _) -> print("$p ") }
    }
Honestly I like Kotlin's Sequence abstraction much more than Java streams. It's extremely simple to implement and understand contrary to streams.


Right, his Fibonacci class is completely immutable, i.e. it's purely functional.

He's confusing different usages of the word "state" - exhortations to avoid state are almost always about mutable state, but he doesn't appear to recognize that distinction.


Tell that to the person at work who recently came back from a FP workshop and has declared war on all state...

Developers to tend to go from extremes.


The question is what he means by "all state".

If we take the OP post as definitional, then eliminating state means eliminating compound data structures, because that's the usage of "state" implied by that Fibonacci example.

Hopefully the person at your work has something less extreme in mind. Ask him to explain what he means by "state".


That code is not at all readable, even knowing Haskell.

Even the recursive python version with memoization is a bit difficult to read, but least it has the recurrence relation in it.

    vals = {}

    def fib(n):
      if n < 2:
        return n
      if n in vals.keys():
        return vals[n]
      val = fib(n-1) + fib(n-2)
      vals[n] = val
      return val


Your code is doing something pretty different from the examples above. You're just calculating the nth fibonacci number, not creating an infinite fibonacci _sequence_. Eliding the explicit recursive structure is the point of the exercise!

Admittedly Python's facilities for doing that with some mutable state are pretty nice:

  from itertools import islice

  def fib():
      a, b = 0, 1
      while True:
          yield a
          a, b = b, a + b

  list(islice(fib(), 10)) == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]


I wasn't speaking to the goal of generating a sequence that can be lazily evaluated.

I was speaking to readability.


The biggest problem with the 1^2...n^2 example is syntactic, honestly. If Java had native tuples and destructuring, the code would look quite clean:

    Stream.iterate((1,1), ((idx, _)) -> (idx+1, Math.pow(idx+1, 2)));


As an aside, as each term of this particular sequence only depends on the index, not the previous term, the cleanest way would be to just map an index sequence:

    // Full overflow prevention left as an exercise to the reader
    IntStream.rangeClosed(0, Integer.MAX_VALUE).map(n -> Math.pow(n, 2))


It's a shame the author never actually ran the code, it has bugs.

For example, the Factorial next() function should be:

  return new Factorial (index + 1, value * (index + 1));
And of course the iterate line should be:

  Stream.iterate (Factorial.SEED, Factorial::next)


As someone who hasn't written Java since college, I am not familiar with streams. Is this similar in principle to a Python/JS generator function?


Streams are a bit more general. They allow you to use functional operations on sequence of elements (e.g. map, filter, reduce, etc.) to build a processing pipeline which can execute async and in parallel.

``` widgets.stream() .filter(b -> b.getColor() == RED) .mapToInt(b -> b.getWeight()) .sum(); ``` https://docs.oracle.com/javase/8/docs/api/java/util/stream/p...

They aren't specific to Java. In fact they require features such as lambdas that made them less practical until Java 8.


It's more comparable to Python iterators in combination with itertools, sequence comprehensions, and a few other things.

It's arguably more powerful in that it's got some more bits and bobs out-of-the-box, and it's easier to do parallel stuff with it. But it's also arguably less powerful in that it's not particularly open for anyone who isn't a JDK developer to adapt for their own purposes. To make your own Python iterator that will interact nicely with anything else that's designed to operate on iterators, you only have one method you have to implement. To make your own implementation of Java's Stream interface, you've got a rather daunting list of 40 you have to implement.


Essentially yes - a Java stream returns an iterable object, which we can perform further operations such as filtering down, mapping to something else or 'collecting' it into another object (et al).


I wonder how people who stopped suff.. writing java before java 8 feel about lambda expressions and streams.


I've been writing Java for 20 years. Streams are a definite improvement, but half the time I find myself rewriting a streams approach to an old-fashioned loop to make the code more readable.


When you say "readable," do you mean to OO programmers? Functional programmers? All programmers?

For example, as a 5-year Clojure convert who wrote Java for 15 years before that, old-fashioned loops are less readable now, as they hide the essentials of what is really happening. For example, you might be mapping a collection from one type of value to another, or reducing it to some other form, etc. But both use loops. You have to scan all over the place in the loop construct to see what it's really doing. I'd much rather see the function that is happening than the procedure by which the function is accomplished.


Readable to me, and I assume most other Java programmers. Here's an example I recently came across [0]:

Option A:

  static <X, Y, Z> Map<X, Z> transform(Map<? extends X, ? extends Y> input,
    Function<Y, Z> function) {
      return input.keySet().stream()
          .collect(Collectors.toMap(Function.identity(),
                                    key -> function.apply(input.get(key))));
  }
Option B:

  static <X, Y, Z> Map<X, Z> transform1(Map<? extends X, ? extends Y> input,
    Function<Y, Z> function) {
      Map<X, Z> result = new HashMap<>();
      input.forEach((k, v) -> result.put(k, function.apply(v)));
      return result;
  }

Personally I find Option B more readable. Another example:

Option A:

  protected boolean isCashBasis(CalendarDate effectiveDate) {
    return priorVatReturns.stream()
      .filter(r -> r.period.contains(effectiveDate))
      .findFirst()
      .map(r -> r.basis == AccountingBasis.CASH_BASIS)
      .orElseGet(() -> super.isCashBasis(effectiveDate));
  }
Option B:

  protected boolean isCashBasis(CalendarDate effectiveDate) {
    for (VatReturn r : priorVatReturns) {
      if (r.period.contains(effectiveDate)) {
        return r.basis == AccountingBasis.CASH_BASIS;
      }
    }
    return super.isCashBasis(effectiveDate);
  }
Again I would choose Option B, although I suppose Option A isn't too bad.

[0] https://stackoverflow.com/a/25905196/76295


everytime I use generic statically typed oop I can't stop thinking about clojure or ml languages.. The amount of noise is deafening.


With a great relief.


That's my opinion too, I don't think I can consider ever touching java prior r8.


Java 5/6 every day of my life and I want to die.


Are you allowed to produce jar libs (possibly written in clojure/kotlin/foo) ?


It would probably be possible, but the business itself is too old and not succeptable to change.


I don't think the stateful approach with .generate() is thread-safe if you make the stream parallel. The API doc says it's an unordered stream, parallelizing it would mean concurrent reads/writes in the supplier.

Stream.iterate() docs on the other hand guarantee happens-before ordering for invocations.


Small want item about Java Streams... I wish this was a language feature. Allow type declarations in lambdas:

currently: collection.stream()....bunch of stuff....forEach(item -> something(item))....

this would be nice: collection.stream()....bunch of stuff....forEach(String item -> something(item))....

When you get 50 levels deep on a really long lambda, this would make debugging the things far easier.


It is definitely possible to specify the type of the parameter(s) in a lambda expression in Java.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: