0

I'm trying to get into basics of functional programming with Java 8 and I have a simple task which is to set a property on the object and then persist it. The database proper type is ltree so it might fail if it contains not allowed characters. I want to process items one-by-one and log exceptions/successes.

I choose to use the Vavr library because Try.of() exception handling and I want to learn to just use it as it seems very helpful.

here is what I came up with but I'm not satisfied enough:

public class PathHandler {

    private final DocVersionDAO dao;

    public void processWithHandling() {
        Try.of(this::process)
                .recover(x -> Match(x).of(
                        Case($(instanceOf(Exception.class)), this::logException)
                ));
    }

    private Stream<Try<DocVersion>> logException(Exception e) {
        //log exception now but what to return? also I would like to have DocVersion here too..
        return null;
    }

    public Stream<Try<DocVersion>> process() {
        return dao.getAllForPathProcessing()  //returns Stream<DocVersion>
                .map(this::justSetIt)
                .map(this::save);
    }

    public DocVersion justSetIt(DocVersion v) {
        String path = Optional.ofNullable(v.getMetadata().getAdditionals().get(Vedantas.PATH))
                .orElse(null);

        log.info(String.format("document of uuid %s has matadata path %s; setting it", v.getDocument2().getUUID(), path));

        v.getDocument2().setPath(path);

        return v;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Try<DocVersion> save(DocVersion v) {
        return Try.of(() -> dao.save(v));
    }
}

the goal is quite simple so could you teach me proper way to do it?

Curiosa Globunznik
  • 3,129
  • 1
  • 16
  • 24
greengold
  • 1,184
  • 3
  • 18
  • 43

1 Answers1

0

I'm afraid, this will become highly opinionated. Anyway, I try something.

... which happened before I realized, what Vavr actually provides. It attempts to cover everything mentioned here, like immutable data structures and monad syntax sugaring (with the For statement), and goes beyond that by coming up even with pattern matching. It takes a comprehensive set of FP concepts and rebuilds them using Java and it is no surprise Scala comes into one's mind seeing this ("Vavr is greatly inspired by Scala").

Now the foundations of functional programming can't be covered by a single SO post. And it might be problematic to get familiar with them in a language like Java which isn't geared towards it. So perhaps it is better to approach them in their natural habitat like the Scala language, which is still in some proximity to Java, or Haskell, which is not.

Coming back from this detour applying the features of Vavr may be more straight foward for the initiated. But likelely not for the Java developer sitting next to you in the office, who is less willing to go the extra mile and comes up with arguments that can't be just dismissed, like this one: "If we wanted to it that way, we would be a Scala shop". Therefore I'd say, applying Vavr asks for a pragmatic attitute.

To corroborate the Vavra-Scala argument, let's take Vavra's For construct (all Lists mentioned are io.vavr.collection.List), it looks like this:

Iterator<Tuple2<Integer, String>> tuples =
            For(List.of(1, 2, 3), i ->
                    For(List.of(4, 5, 6))
                            .yield(a -> Tuple.of(i, String.valueOf(a))));

In Scala you'd encounter For and yield this way.

val tuples = for {
                  i <- 1 to 3
                  a <- 4 to 6
             } yield (i, String.valueOf(a))

All the monad machinery remains under the hood, where Vavra brings more of an approximation, necessarily leaking some internals. For the purpose of learning it might be puzzling to start with Vavra's hybrid creatures.

So what remains of my post is a small time treatment of some FP basics, using the example of the OP, elaborating on immutability and Try on a trench-level, but omitting pattern matching. Here we go:

One of the defining characteristics of FP are functions free of side effects ("pure functions"), which naturally (so to speak) comes along with immutable data structures/objects, which may sound kind of weird. One obvious pay off is, that you don't have to worry, that your operations create unintended changes at some other place. But Java doesn't enforce that in any way, also its immutable collections are only so on a superficial level. From the FP signature characteristics Java only offers higher order functions with java-lambdas.

I used the functional style quite a bit on the job manipulating complicated structures where I stuck to those 2 principles. E.g. load a tree T of objects from a db, do some transformations on it, which meant producing another tree of objects T', sort of one big map operation, place the changes in front of the user to accept or reject them. If accepted, apply the changes to the related JPA entities and persist them. So after the functional transformation two mutations were applied.

I'd propose, to apply FP in this sense and tried to formulate an according version of your code, using an immutable DocVersion class. I chose to simplify the Metadata part for the sake of the example.

I also tried to highlight, how the "exception-free" Try approach (some of it poached from here) could be formulated and utilized some more. Its a small time version of Vavr's Try, hopefully focusing on the essentials. Note its proximity to Java's Optional and the map and flatMap methods in there, which render it an incarnation of the FP concept called monad. It became notorious in a sweep of highly confusing blog posts some years ago usually starting with "What is a monad?" (e.g. this one). They have cost me some weeks of my life, while it is rather easy to get a good intuition of the issue just by using Java streams or Optionals. Miran Lipovaca's "Learn Yourself a Haskell For Great Good" later made good for it to some extent, and Martin Odersky's Scala language.

Boasting with of, map and flatMap, Try would, roughly speaking, qualify for a syntax-sugaring like you find it in C# (linq-expressions) or Scala for-expressions. In Java there is no equivalent, but some attempts to at least compensate a bit are listed here, and Vavr looks like another one. Personally I use the jool library occasionally.

Passing around streams as function results seems not quite canonical to me, since streams are not supposed to get reused. That's also the reason to create a List as an intermediary result in process().

public class PathHandler {

class DocVersionDAO {
    public void save(DocVersion v) {

    }

    public DocVersion validate(DocVersion v) {
        return v;
    }

    public Stream<DocVersion> getAllForPathProcessing() {
        return null;
    }
}

class Metadata {
    @Id
    private final Long id;
    private final String value;

    Metadata() {
        this.id = null;
        this.value = null;
    }

    Metadata(Long id, String value) {
        this.id = id;
        this.value = value;
    }

    public Optional<String> getValue() {
        return Optional.of(value);
    }

    public Metadata withValue(String value) {
        return new Metadata(id, value);
    }

}

public @interface Id {
}

class DocVersion {
    @Id
    private Long id;

    private final Metadata metadatata;

    public Metadata getMetadatata() {
        return metadatata;
    }

    public DocVersion(Long id) {
        this.id = id;
        this.metadatata = new Metadata();
    }

    public DocVersion(Long id, Metadata metadatata) {
        this.id = id;
        this.metadatata = metadatata;
    }

    public DocVersion withMetadatata(Metadata metadatata) {
        return new DocVersion(id, metadatata);
    }

    public DocVersion withMetadatata(String metadatata) {
        return new DocVersion(id, this.metadatata.withValue(metadatata));
    }
}

private DocVersionDAO dao;


public List<DocVersion> process() {

    List<Tuple2<DocVersion, Try<DocVersion>>> maybePersisted = dao.getAllForPathProcessing()
            .map(d -> augmentMetadata(d, LocalDateTime.now().toString()))
            .map(d -> Tuple.of(d, Try.of(() -> dao.validate(d))
                    .flatMap(this::trySave)))
            .peek(i -> i._2.onException(this::logExceptionWithBadPracticeOfUsingPeek))
            .collect(Collectors.toList());

    maybePersisted.stream()
            .filter(i -> i._2.getException().isPresent())
            .map(e -> String.format("Item %s caused exception %s", e._1.toString(), fmtException(e._2.getException().get())))
            .forEach(this::log);

    return maybePersisted.stream()
            .filter(i -> !i._2.getException().isPresent())
            .map(i -> i._2.get())
            .collect(Collectors.toList());
}

private void logExceptionWithBadPracticeOfUsingPeek(Exception exception) {
    logException(exception);
}

private String fmtException(Exception e) {
    return null;
}

private void logException(Exception e) {
    log(fmtException(e));
}

public DocVersion augmentMetadata(DocVersion v, String augment) {
    v.getMetadatata().getValue()
            .ifPresent(m -> log(String.format("Doc %d has matadata %s, augmenting it with %s", v.id, m, augment)));

    return v.withMetadatata(v.metadatata.withValue(v.getMetadatata().value + augment));
}

public Try<DocVersion> trySave(DocVersion v) {
    return new Try<>(() -> {
        dao.save(v);
        return v;
    });
}

private void log(String what) {
}

}

Try looks like this

public class Try<T> {
    private T result;
    private Exception exception;

    private Try(T result, Exception exception) {
        this.result = result;
        this.exception = exception;
    }

    public static <T> Try<T> of(Supplier<T> f)
    {
        return new Try<>(f);
    }

    T get() {
        if (result == null) {
            throw new IllegalStateException();
        }
        return result;
    }

    public void onException(Consumer<Exception> handler)
    {
        if (exception != null)
        {
            handler.accept(exception);
        }
    }

    public <U> Try<U> map(Function<T, U> mapper) {
        return exception != null ? new Try<>(null, exception) : new Try<>(() -> mapper.apply(result));
    }

    public <U> Try<U> flatMap(Function<T, Try<U>> mapper) {
        return exception != null ? null : mapper.apply(result);
    }

    public void onError(Consumer<Exception> exceptionHandler) {
        if (exception != null) {
            exceptionHandler.accept(exception);
        }
    }

    public Optional<Exception> getException() {
        return Optional.of(exception);
    }

    public Try(Supplier<T> r) {
        try {
            result = r.get();
        } catch (Exception e) {
            exception = e;
        }
    }
}
Curiosa Globunznik
  • 3,129
  • 1
  • 16
  • 24
  • interesting reading, didn't made me much more vise through as I want to utilize this vavr and I am just sneak-peaking FP – greengold Oct 21 '19 at 19:41
  • yeah, I read they're guide, but what I trying to find here is really more conceptual. like what should I return from `logException` asi it wants me to return a Stream?; is it right to wrap `process()` in `Try.of()` or should I use it elsewhere? how do I access information from `DocVersion` in `logException`? should I throw custom instance of some exception in `save()` to have it? but then I need to enclose it in try-catch and all of this seems puzzled to me. just doesn't conceptually feels right – greengold Oct 21 '19 at 21:01
  • you know, in this case I cannot validate it. it's on database whether it will accept the characters or not. I tried to modify `process` to return a stream of tuples with throwable and docVersion, but `x` in `recover` is still only the `Throwable` not the tuple.. and when I change `logException` to return void it doesn't compile because logException 'is not a functional interface' – greengold Oct 21 '19 at 21:15
  • yes, I don't really need it.. I just wanted to get a taste of FP, but this seems overcomplicated to me. might give a shout to your example tmrw but, probabbly I will stick to OO concept once again :/ thanks for sharing your knowledge, through, will see how this thread develops – greengold Oct 21 '19 at 21:54