【问题标题】:logic using functional-style exception handling with java and Vavr使用 java 和 Vavr 的函数式异常处理的逻辑
【发布时间】:2020-02-17 10:30:19
【问题描述】:

我正在尝试使用 Java 8 进行函数式编程的基础知识,我有一个简单的任务是在对象上设置一个属性,然后将其持久化。数据库正确类型是ltree,因此如果它包含不允许的字符,它可能会失败。我想逐个处理项目并记录异常/成功。

我选择使用Vavr 库是因为Try.of() 异常处理,我想学习使用它,因为它看起来很有帮助。

这是我想出的,但我还不够满意:

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));
    }
}

目标很简单,你能教我正确的方法吗?

【问题讨论】:

    标签: java functional-programming vavr


    【解决方案1】:

    恐怕这会变得自以为是。无论如何,我尝试一下。

    ...在我意识到 Vavr 实际提供之前发生的事情。它试图涵盖这里提到的所有内容,例如 immutable data structures 和 monad 语法糖化(使用 For 语句),并通过提出 pattern matching 来超越这些。它需要一套全面的 FP 概念并使用 Java 重新构建它们,因此 Scala 会出现在人们的脑海中也就不足为奇了(“Vavr is greatly inspired by Scala”)。

    现在,单个 SO 帖子无法涵盖函数式编程的基础。用像 Java 这样不适合它的语言来熟悉它们可能会有问题。因此,也许最好在它们的自然栖息地接近它们,例如 Scala 语言,它仍然与 Java 有一定的距离,或者 Haskell,它不是。

    从这条弯路回来,应用 Vavr 的功能对于初学者来说可能更直接。但对于办公室里坐在你旁边的 Java 开发人员来说可能不是这样,他们不太愿意加倍努力,提出不能随便驳回的论点,比如这个:“如果我们想那样做,我们将成为一家 Scala 商店”。因此我会说,应用 Vavr 需要务实的态度。

    为了证实 Vavra-Scala 的论点,让我们采用 Vavra 的 For 构造(所有提到的 Lists 都是 io.vavr.collection.List),它看起来像这样:

    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))));
    

    在 Scala 中,您会以这种方式遇到 Foryield

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

    所有 monad 机器都保留在引擎盖下,Vavra 带来了更多的近似值,必然会泄漏一些内部结构。出于学习的目的,从 Vavra 的混合生物开始可能会令人费解。

    所以我的帖子剩下的内容是对一些 FP 基础知识的小时间处理,使用 OP 的示例,详细说明不变性和沟槽级别的 Try,但省略了模式匹配。我们开始:

    defining characteristics of FP 之一是没有副作用的函数(“纯函数”),它自然(可以这么说)伴随着不可变的数据结构/对象,这听起来有点奇怪。一个明显的回报是,您不必担心您的操作会在其他地方产生意想不到的变化。但是 Java 并没有以任何方式强制执行,它的 immutable collections 也只是表面上的。从 FP 签名特征来看,Java 仅提供带有 java-lambdas 的高阶函数。

    我在处理复杂结构的工作中使用了相当多的函数式风格,我坚持这两个原则。例如。从数据库加载对象树 T,对其进行一些转换,这意味着生成另一棵对象树 T',类似于一个大的 map 操作,将更改放在用户面前以接受或拒绝它们。如果被接受,则将更改应用到相关的 JPA 实体并保留它们。所以在功能转换后应用了两个突变。

    我建议在这个意义上应用 FP,并尝试使用不可变的 DocVersion 类来制定相应版本的代码。为了示例,我选择简化元数据部分。

    我还试图强调,“无异常”Try 方法(其中一些被挖走from here)可以如何制定和利用更多。它是Vavr's Try 的小版本,希望专注于基本要素。请注意它与 Java 的 Optional 以及其中的 mapflatMap 方法很接近,这使它成为称为 monad 的 FP 概念的化身。几年前,它在一系列高度混乱的博客文章中臭名昭著,通常以“什么是单子?”开头。 (例如this one)。它们花费了我几个星期的时间,而仅通过使用 Java 流或 Optionals 就很容易对这个问题有一个很好的直觉。 Miran Lipovaca 的“Learn Yourself a Haskell For Great Good”后来在一定程度上弥补了这一点,Martin Odersky 的Scala 语言。

    拥有ofmapflatMap,粗略地说,Try 将符合您在 C# (linq-expressions) 或 Scala for-expressions 中找到的语法糖化条件。在 Java 中没有等价物,但列出了一些至少补偿一点的尝试here,而Vavr 看起来像另一个。我个人偶尔会使用jool 库。

    在我看来,将流作为函数结果传递似乎不太规范,因为流是not supposed to get reused。这也是在process() 中创建一个List 作为中间结果的原因。

    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) {
    }
    
    }
    

    试试看起来像这样

    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;
            }
        }
    }
    

    【讨论】:

    • 有趣的阅读,并没有让我更清楚,因为我想使用这个 vavr,我只是偷偷摸摸的 FP
    • 是的,我读到它们是指南,但我试图在这里找到的内容确实更具概念性。比如我应该从logException返回什么,因为它希望我返回一个Stream?;将process() 包裹在Try.of() 中是否正确,或者我应该在其他地方使用它吗?如何在logException 中访问来自DocVersion 的信息?我应该在save() 中抛出一些异常的自定义实例吗?但后来我需要将它包含在 try-catch 中,这一切对我来说似乎很困惑。只是在概念上感觉不对
    • 你知道,在这种情况下我无法验证它。它在数据库中是否接受字符。我试图修改process 以返回带有可抛出和docVersion 的元组流,但recover 中的x 仍然只是Throwable 而不是元组.. 当我将logException 更改为返回void 时,它不会'不编译,因为 logException '不是功能接口'
    • 是的,我真的不需要它。我只是想体验一下 FP,但这对我来说似乎过于复杂。可能会对您的示例 tmrw 大喊大叫,但是,我可能会再次坚持 OO 概念:/感谢您分享您的知识,通过,看看这个线程是如何发展的
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-18
    • 2016-11-14
    • 2015-02-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多