恐怕这会变得自以为是。无论如何,我尝试一下。
...在我意识到 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 中,您会以这种方式遇到 For 和 yield。
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 以及其中的 map 和 flatMap 方法很接近,这使它成为称为 monad 的 FP 概念的化身。几年前,它在一系列高度混乱的博客文章中臭名昭著,通常以“什么是单子?”开头。 (例如this one)。它们花费了我几个星期的时间,而仅通过使用 Java 流或 Optionals 就很容易对这个问题有一个很好的直觉。 Miran Lipovaca 的“Learn Yourself a Haskell For Great Good”后来在一定程度上弥补了这一点,Martin Odersky 的Scala 语言。
拥有of、map 和flatMap,粗略地说,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;
}
}
}