3

I have been experimenting with Functional Programming in Java for a while now, and noticed that I started to prefer the use of the @FunctionalInterface functions from the java.util.function package such as Functions, BiFunctions, UnaryOperators, Predicates, BiPredicates, etc. instead of simple private methods in my classes. I am aware that their application is much more recommend as to be passed as arguments to another function, and that's how I usually tend to use them, but I now just find them immediate and somehow better.

I in fact now tend to declare some of these as variables to be then used in my classes when needed.

I don't seem to find anywhere guidelines or cons on the use of these instead of simple methods.

So: are there downsides using them this way?

  • An example:

Why prefer:

private boolean foo(final int a, final int b){
    return a < b;
}

instead of:

private final BiPredicate<Integer,Integer> foo = (a,b) -> a < b;

an example of how I tend to use them from my latest project:

    private final BiFunction<BoardPosition, Pair<Integer, Integer>, BoardPosition> sumBoardPosWithPair = (pos,
            pair) -> new BoardPositionImpl(pos.getX() + pair.getX(), pos.getY() + pair.getY());


    private final Function<Pair<Integer, Integer>, UnaryOperator<BoardPosition>> unaryCreator = (
            axis) -> (p) -> this.sumBoardPosWithPair.apply(p, axis);
    /**
     * If you need to call the fromFunction method twice for specular directions use
     * this TriFunction specularNoLimitDirection instead.
     */
    private final TriFunction<Piece, Vectors, Board, Set<BoardPosition>> specularNoLimitDirection = (piece, axis,
            board) -> Stream.concat(
                    this.fromFunction(this.unaryCreator.apply(axis.getAxis()), piece, board,
                            board.getColumns() + board.getRows()).stream(),
                    this.fromFunction(this.unaryCreator.apply(axis.getOpposite()), piece, board,
                            board.getColumns() + board.getRows()).stream())
                    .collect(Collectors.toSet());

    protected final Set<BoardPosition> fromFunction(final UnaryOperator<BoardPosition> function, final Piece piece,
            final Board board, final int limit) {
        /*
         * The "function.apply" at the seed of the Stream.Iterate is used to skip the
         * first element, that's itself, in fact a piece can't have as a possible move
         * it's original position.
         */
        final List<BoardPosition> positions = Stream.iterate(function.apply(piece.getPiecePosition()), function)
                .takeWhile(board::contains)
                .takeWhile(x -> board.getPieceAtPosition(x).isEmpty()
                        || !board.getPieceAtPosition(x).get().getPlayer().equals(piece.getPlayer()))
                .limit(limit).collect(Collectors.toList());

        final Optional<BoardPosition> pos = positions.stream().filter(i -> board.getPieceAtPosition(i).isPresent()
                && !board.getPieceAtPosition(i).get().getPlayer().equals(piece.getPlayer())).findFirst();
        /*
         * The sublist excludes the last n-th element of the high-endpoint, for this
         * reason we need to add 1.
         */
        return pos.isEmpty() ? new HashSet<>(positions)
                : new HashSet<>(positions.subList(0, positions.indexOf(pos.get()) + SINGLE_INCREMENT));
    }

3 Answers3

6

You should do what feels most readable and maintainable. If putting these functions in variables with descriptive names feels like a readable and maintainable way to solve the problem, that's fine. There's nothing wrong with programming like this.

A middle ground that you might also like is putting the logic in ordinary static methods:

private boolean foo(final int a, final int b){
   return a < b;
}

and then referring to it when needed with a method reference: MyClass::foo. This is equivalent in behavior to the lambda you defined.

There's lots of ways to write this code. Everyone has opinions about the "right" way to do it, but in reality there are plenty of "right" ways. (And some not-so-right ways too.)

Brian Goetz
  • 90,105
  • 23
  • 150
  • 161
1

The high level criteria for deciding things like this should be Correctness, Readability and Performance ... with appropriate weighting depending on the project.

Q: So how do we "apply" those criteria here?

A: It is difficult ...

The problem is that choosing lambdas versions function references is so far down at the implementation level that its impact will be almost negligible. (Even assessing the efficiency at that level is probably irrelevant ... except in extreme cases.)

If we pull back to the broader question of functional (e.g. with streams) versus classical (e.g. with loops) it is still difficult to make general recommendations:

  • Correctness: the functional style is typically easier to reason about but that doesn't apply uniformly across all computing problems. (For example, to problems where efficient solutions require mutation.)

  • Readability: for simple problems, a functional / stream solution is often more readable. However, this isn't always the case. Sometimes the functional solution is necessarily convoluted. Sometimes a functional solution can be unnecessarily abstracted in an attempt to impose a general pattern across a range of problems or sub-problems.

  • Efficiency: there is no general answer here either. On the one hand, there will be little performance difference for well optimized code / solutions. On the other hand, it is easy to use streams (or classical approaches) naively. And it is easy for the (in-)efficiency of a given approach to be obscured by over-uses of abstraction ...


With respect to your example code: My primary concern as someone reading and maintaining that code is it is difficult to understand what it actually does. Maybe you (the author) understand it (now). But someone coming in cold is going to struggle. (Specifically, >>I<< am struggling ...)

The meta-problem is that readability is very difficult (and very expensive1) to assess objectively. So we are left with subjective assessment; i.e. just peoples' opinions.

A secondary concern is that if this is part of a game playing algorithm (an "AI" player) then efficiency could be critical to the success of the project. (Assuming that it is a serious project.)

But the flip-side is that if this code is just you "experimenting", then its is moot what other people think, whether they understand the code, or how it performs.


Some musings ...

This relates to the old top-down versus bottom-up design debate. Both approaches can work. Both approaches can fail. One of the failure modes for bottom up is a tendency to build a bunch of complicated abstracted infrastructure "because you think you are going to use it". Then when you actually get to use it, you may discover that either it isn't right ... or you could have just done it more simply without the abstraction.

I have also noticed that some smart programmers go through a "phase" of developing their own toolkits of utility classes or frameworks or whatever ... with the thought that they will be able use them in their future projects. (It is hard to say what causes this, but it may be rooted in boredom.) Anyway, it doesn't scale well, over either time or team size. These artifacts have a tendency to turn into legacy "cruft" that is unloved by the team, and (eventually) regretted by the author. Especially if the author has the chutzpah to publish the artifact and encourage others to use it. (Yet another dead project on GitHub!)


1 - You need to set up an experiment where a cohort of experienced programmers are given (say) 2 different solutions to the same problem, and they are required to read and answer questions about it, or modify it, or something. And then you repeat the process for different problems.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • This is kinda of a serious project, what I was aiming to for the bottom level architecture of the game was extreme reusability and extension. For example, like in the code above, changing the movement strategies of pieces in chess variants would have been extremely quick and easy. But what you said is true, It's true that it's somehow opaque and not very easy to read first glance. I should probably break down the fromFunction method in smaller ones maybe. – StefanoScolari Mar 25 '21 at 09:01
-3

Java as we know is a fairly old programming language (to an extent) that has regular updates to stay ahead of the game when it comes to programming languages to choose from. Having said that, you may have been reading on old documentation explaining interfaces.

private boolean foo(final int a, final int b){
return a < b;

}

Is a valid function and one where you see in a lot in documentation and learning the fundamentals of interfaces.

Now you are comparing the "older" way to a much much "newer" way of handling methods/interfaces. With the release of Java 8, they added lambdas which I believe is to give programmers the ability to write methods with "less" lines and in a quicker fashion (among other things). You want your programming language to be up-to-date with security but also on par with others. Java is somewhat of a clunky programming language compared to say Python.

private final BiPredicate<Integer,Integer> foo = (a,b) -> a < b;

This is also an acceptable method/function to write in Java 8 and anything newer. There is no problem with writing functions in this manner and it may result in writing your code quicker.

The only problems with writing methods in this "newer" way is such

  1. You are using lambdas and some people may not have experience with lambdas and their powers
  2. Someone trying to read your code, may have trouble understanding it

There is nothing wrong with it, the "old" way is only preferred if you are apart of a team that says "No you cannot use lambdas". There is no difference.

Last note, you may find the benchmarks of lambda speed compared to anonymous classes interesting, this can be found here

[EDIT] I would like to add to only write lambdas for simple functions, I know they are very powerful but writing lambdas to search through a list then the conventional for loop is okay but complex methods should be written in a clear fashion. This is only to avoid headaches when debugging programs.

  • 3
    Your assumptions about why we added these features to Java are very much off-base. It is not about concision, but instead about better behavioral abstraction (just as generics were about better data abstraction). A library like `java.util.stream`, which lets users say what they want to compute without being bogged down in the implementation details, would not be practical without lambdas. – Brian Goetz Mar 25 '21 at 00:01
  • I was thinking about that as well. My only thought is that java.util.stream was added in Java 8 as well. Correct me if I am wrong, but they were built in conjunction? – Caelan van Olm Mar 25 '21 at 00:05
  • How much have you actually used lambdas in java? – Thorbjørn Ravn Andersen Mar 25 '21 at 00:15
  • Answering to Brian, writing Streams in Java is one of my favourite things in programming. Thanks. – StefanoScolari Mar 25 '21 at 00:39
  • 1
    @CaelanvanOlm Indeed, they were built in conjunction. Being able to write libraries like Streams was a big part of the motivation for the language feature; co-developing the libraries was critical to validating the language feature. A main goal of adding language features is to enable the development of better libraries, because libraries are where the ecosystem's value comes from. There's nothing magic about `Stream`; it's just the _first_ good designed-for-lambda library. – Brian Goetz Mar 25 '21 at 14:54
  • @BrianGoetz Yeah I see and stream is an awesome library they added. I know my "solution" was going a bit off topic and didn't want to talk about stream and such because of scope but yeah sorry for the not so clear answer as to why. It does put a step up for Java definitely though and creates shorter code. – Caelan van Olm Mar 25 '21 at 15:07
  • @StefanoScolari Well only with stream I suppose – Caelan van Olm Mar 25 '21 at 15:08