面向对象语言中的函数式编程

3

不可变对象是可以的,但是非final局部引用是可以的吗?

换句话说,下面的代码片段可以表示为函数式风格吗?

Employee e = new Employee("Lex", 24, 250);
e = Employee.setName(e, "Vasili");
e = Employee.setAge(e, 12);
e = Employee.setSalary(e, 2500);
Employee.log(e);

备注:这里所有Employee的方法都是静态的,setter方法是工厂方法,返回新的实例。


1
你的“OK”是什么意思? - payloc91
为了在Java中模拟纯函数式编程,所有本地变量都必须是final。参数也是如此。 - Andreas
2
一个不可变的风格可能是 new Employee("Lex", 24, 250).withName("Vasili").withAge(12).withSalary(2500),其中每个 with 方法都返回一个新的 Employee 对象。 - Louis Wasserman
1
在纯函数式语言中,不存在“变量”。有引用值的名称,但您无法更改该值。从命令式编程的角度来看,所有变量都是常量,即根本不可变/可变/可更改的。这就是为什么要在Java中模拟相同的行为,使参数和局部变量成为final,以便它们不能被重新分配/更新/修改。 - Andreas
2
在函数式编程中,没有对象或引用,只有值,而且值不会改变。在程序中你不会改变4的值,所以如果你说x=4,那么你也不会改变x的值,因为那就等同于改变4的值。第一行代码说e = Employee "Lex"等等,然后你把e改成了Employee "Vasili"。那么e是Lex还是Vasili呢?它不能同时是两者。在函数式编程中,没有“控制流”的概念,变量在不同时间具有不同的值。 - Paul Johnson
显示剩余15条评论
4个回答

2

由于这个问题被标记为“Java”,我假设这个问题是关于Java中的FP实践(即不可变性)。

如今在Java中的良好实践是使用构建器:

Employee e = Employee.builder()
                     .surname("Lex")
                     .age(24)
                     .name("Vasili")
                     .salary(2500)
                     .build();

或者是静态构造函数:
Employee e = Employee.of("Vasili", "Lex", 24, 2500);

在这两种情况下,“经典”构造函数应该被声明为私有的,以确保对象不能被实例化并在不一致的状态下对客户端可用。
然后,对象变异器应该返回新对象:
Employee.of("Vasili", "Lex", 24, 2500)  // creates an object
        .updateName("Sergey")           // returns 1st modified copy
        .updateSalary(3500);            // returns 2nd modified copy based on 1st copy

遵循这些实践,通常不需要非最终本地引用。
一个非常流行的例子是日期和时间API

现在,关于使用可变的本地变量。那没问题,但是可以通过方法链缩短代码并使其更具表现力。尝试直接链接静态方法将不会看起来很优雅:

Employee e = setSalary(setAge(setName(new Employee("Lex", 24, 250), "Vasili"), 12), 2500);

为了模拟一个单子,可以将对象包装在类似单子的容器中,该容器定义了一个bind方法,该方法接受一个函数,该函数将接受存储在单子中的对象并返回一些结果,该结果将再次被包装在单子中。一个简单的例子如下:

static class Employee {
    public String name;
    public int age;
    public long salary;
}

static class Monad<T> {
    private final T value;

    private Monad(T value) { this.value = value; }

    public static <T> Monad<T> of(T value) {
        return new Monad<>(value);
    }

    public T getValue() { return value; }

    public Monad<T> bind(UnaryOperator<T> operator){
        return of(operator.apply(value));
    }
}

public static void main(String[] args) {
    Employee value = Monad.of(new Employee())
                          .bind(e -> {e.name = "Lex"; return e; })
                          .bind(e -> {e.age = 24; return e; })
                          .bind(e -> {e.salary = 2500; return e; })
                          .getValue();
}

但是从Java 8开始,这可以通过核心Java来完成 - Stream API 可以完成这项任务以及更多的功能:

Stream.of(new Employee())
      .map(e -> {e.name = "Lex"; return e; })
      .map(e -> {e.age = 24; return e; })
      .map(e -> {e.salary = 2500; return e; })
      .findFirst()
      .get();

谢谢回复,@jihor!在您的示例中存储了可变对象 - 但是它可以被不可变对象替换,并使用静态方法引用吗? - Lex Ushakov
你能帮忙理解为什么下面的代码无法工作吗?https://pastebin.com/DMN18EQv - Lex Ushakov
@LexUshakov map() 方法在 T 流上期望 Function<? super T, ?>。工作行将如下所示:.map((e) -> Employee.setName(e, "Vasilii")) - jihor

1

这类语言中的每个函数都返回一个新项,与此类似,在许多函数式编程语言(如Haskell)中,您甚至无法更新值,只能创建新值:

let myBook = beginBook "Haskell"
let myBook' = addChapter (Chapter "Intro" ["Hello","World"]) myBook

所以这里的beginBook函数将返回一本书,然后addChapter将返回另一本书,其中某些字段已经修改。

但是在Haskell中,您是否允许重新“let”变量“myBook”?这就是问题所在,即本地变量是否可以更新。 - Andreas
@Andreas,不,用haskell是绝对不可能的。这里myBookmyBook'是函数定义,而不是像java类一样的数据结构。 - Lex Ushakov

1

虽然我不知道什么是 Haskell,但我认为你想要实现类似这样的东西:

Employee e = new Employee("Lex")
    .setAge(25)
    .setSalary(2500)
    .setGender(Gender.Male);

这只是以下方式中函数“链接”的结果。
public Employee setParam(param){
   this.param = param;
   return this;
}

但是这些方法并不是静态的,它们属于实例。
此外,没有必要将实例作为参数传递。

还有:

  • this 不是必须的关键字;在上面的例子中,两个参数具有相同的名称,因此没有 this,代码基本上会将 param 的值重新分配给它自己。如果参数有不同的名称,则不需要 this。但是返回 this 是必要的,因为它代表当前实例的引用。

E.g:

public Employee setParam(String param) {
    parameter = param; // parameter is a field in class Employee
    return this; // this "this", is still necessary
}
  • final变量

可能会限制您在样式方面所尝试的内容

final Employee e = Employee.setName(e, "Name"); // invalid, e is unkown

// ----------------

final Employee e;
e = Employee.setName(e, "Name"); // invalid, e may not be initialized

// ----------------

final Employee e;
e = Employee.setName("Name"); // valid

// ----------------

final Employee e = null;
e = Employee.setName(e, "Name"); // invalid. e was already initalized to null

// ----------------

final Employee e = Employee.setName("Name"); // valid
e = Employee.setName("Name2"); // invalid, final variable already initialized

谢谢回复,但我已经知道了。换句话说,没有this关键字是不可能制作流畅的API,对吗? - Lex Ushakov
@LexUshakov 不需要使用 this,让我更新我的答案。 - payloc91
当使用非静态方法时,它们简单地通过隐藏的 this 链接绑定到某个结构体上。在我看来,这是面向对象编程语言的基本特征和主要特点。 - Lex Ushakov

1

这种方式对你是否可行(忽略对OptionalifPresent的不良使用,我们可以用更有意义的内容替换它)?

    Optional.of( new Employee("Lex", 24, 250) )
    .map( e -> Employee.setName(e, "Vasili") )
    .map( e -> Employee.setAge(e, 12) )
    .map( e -> Employee.setSalary(e, 2500) )
    .ifPresent( e -> Employee.log(e) );

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接