将命令式 Java 翻译成函数式 Java (游戏)

Translating Imperative Java to Functional Java (A Game)

我正在学习更多关于 Java 8 及其功能的知识,我想用它做更多的练习。例如,我有以下命令式代码,用于在屏幕边界周围环绕一个圆圈:

if (circle.getPosition().getX() > width + circle.getRadius()){
    circle.getPosition().setX(-circle.getRadius());
}else if (circle.getPosition().getX() < -circle.getRadius()){
    circle.getPosition().setX(width + circle.getRadius());
}
if (circle.getPosition().getY() > height + circle.getRadius()){
    circle.getPosition().setY(-circle.getRadius());
}else if (circle.getPosition().getY() < -circle.getRadius()){
    circle.getPosition().setY(height + circle.getRadius());
}
  1. 我怎样才能尝试 "Functionalize" 它呢?也许一些伪代码?在我看来,可变性和状态似乎是这个例子中固有的。
  2. 函数式编程不适合游戏开发吗?两者我都喜欢,所以我想把它们结合起来。

您几乎可以使用任何编程语言编写函数式代码,但是您无法轻松地学习任何语言的函数式编程。 Java 尤其让函数式编程变得非常痛苦,以至于想要在 JVM 中进行函数式编程的人们想到了 Clojure 和 Scalaz。如果你想学习函数式思维方式(它自然地处理什么问题以及如何处理,什么问题比较棘手以及如何管理它们等等),我强烈建议你花一些时间在函数式或大部分是函数式的语。基于语言质量、坚持功能习语的难易程度、学习资源和社区,我的首选是 Haskell,下一个是 Racket。其他人当然会有不同的看法。

对于你的第一点:你 "functionalize" 你的例子通过思考代码应该实现什么。这是,你有一个圆,想根据某些条件计算另一个圆。但出于某种原因,你的命令式教养使你假设输入圆和输出圆应该存储在相同的内存位置!

要发挥作用,首先要忘记记忆位置并接受价值观。以与 intjava.lang.Integer 或其他数字类型相同的方式来思考每种类型。

举个例子,假设某个新手向您展示了这样的代码:

double x = 3.765;
sin(x);
System.out.println("The square root of x is " + x);

并抱怨 sin 似乎不起作用。那你会怎么想?

现在考虑一下:

Circle myCircle = ....;
wrapAroundBoundsOfScreen(myCircle);
System.out.println("The wrapped-around circle is now " + myCircle);

当后一种代码对您来说与前一种代码一样荒谬时,您就已经迈出了函数式编程的第一步。是的,这确实意味着不要使用您正在使用的命令式语言的某些功能,或者非常谨慎地使用它们。

此处不多'functionalization'适用。但至少我们可以和mutability战斗。 首先pure functions。这将有助于分离 logic。使其清晰且易于测试。 回答问题:你的代码是做什么的?它接受一些参数和 returns 两个参数新的 xy。 下一个示例将使用 pseudo scala.

编写

因此您需要一个函数,该函数将被调用两次以进行 x 和 y 计算。

def (xOrY: Int, widthOrHeight: Int, radius: Int): Int = {

if (x > widthOrHeight + radius) -1*radius else  widthOrHeight + radius
// do your calculation here - return x or y values.

}

P.S> 到目前为止,无论您想在何处应用函数式风格:因为您需要执行一些业务逻辑,所以最好采用函数式方法。

但是不要尝试将其过度复杂化,因为它没有帮助。 所以接下来我不会为这个示例做的事情(接下来是伪 scala):

def tryToMakeMove(conditions: => Boolean, move: => Unit) = if (conditions) move()
/// DO NOT DO IT AT HOME :)
tryToMakeMove(circle.getPosition().getX() > width + circle.getRadius(),  circle.getPosition().setX(-circle.getRadius())).andThen()
   tryToMakeMove(circle.getPosition().getX() < -circle.getRadius()), circle.getPosition().setX(width + circle.getRadius()))
).andThen ... so on.

这就是函数式程序的样子。我已经创建了高阶函数(它接受其他函数作为参数并在内部调用它)。 有了这个功能,我已经调用了你必须做的一个操作...... 但这种功能风格并没有真正帮助。完全没有。您应该只在简化代码的地方正确应用它。

在此示例中,对可变性的要求没有内在的要求。必要的方法是通过应用改变现有圈子状态的副作用来修改现有圈子。

函数式方法是拥有一个不可变的数据结构,并创建一个从第一个结构中获取数据并创建一个新结构的函数。在您的示例中,函数式方法会使圆圈不可变,即没有 setX() 或 setY() 方法。

private Circle wrapCircleAroundBounds(Circle circle, double width, double height) {
    double newx = (circle.getPosition().getX() > width + circle.getRadius()) ? -circle.getRadius() : width + circle.getRadius()
    double newy = (circle.getPosition().getY() > height + circle.getRadius()) ? -circle.getRadius() : height + circle.getRadius()
    return new Circle(newx, newy)
}

使用 Java8 的功能特性,您可以想象将圆列表映射到环绕的圆:

circles.stream().map(circ -> wrapCircleAroundBounds(circ, width, height))

命令式方法和函数式方法具有不同的优点,例如,函数式方法由于不可变性而本质上是线程安全的,因此您应该能够更轻松地并行化此类代码。例如,可以同样安全地写:

circles.parallelStream().map(circ -> wrapCircleAroundBounds(circ, width, height))

我认为函数式编程不一定不适合游戏开发,但尽管它已经完成,但它肯定不是标准方法,因此如果您是使用函数式语言。

正如 dfeuer 在他的回答中所说,Java 的功能特性非常原始——您不支持代数数据类型、模式匹配等,这将使表达问题变得容易得多一种功能风格(至少一旦你习惯了这些习语)。我同意至少阅读一些关于 Haskell 的内容,其中有一个很好的教程:http://learnyouahaskell.com/chapters 将是一个很好的入门方式。与 Scala 不同,Scala 是一种多范式语言,您在学习新风格时不会有 OOP 功能可以依赖。

好的,是时候让我们大家了解一下 Java 8 的功能特性看起来多么新颖和闪亮了。 "Functionalizing"有些东西真的不是一个有效的目标。

然而,这里的原始代码有一个很好的面向对象问题:

当您说 circle.getPosition().setX(...) 时,您在不涉及对象本身的情况下弄乱了圆的内部状态(它的位置)。这打破了封装。如果圆圈 class 设计得当,那么 getPosition() 方法将 return 位置的副本或不可变位置,因此您无法执行此操作。

那个是你真正需要用这段代码解决的问题...

那么,你应该怎么做呢?

好吧,你当然可以在 Circle 中想出一些函数式接口,但老实说,如果你有 circle.move(double x, double y);

,你的代码将更具可读性

How could I go about trying to "Functionalize" it? Maybe some pseudo-code? It seems to me that mutability and state seem inherent in this example.

您可以尝试将可变性限制为几个函数,并在函数内部使用最终变量(这会强制您使用表达式而不是语句)。这是一种可能的方法:

Position wrapCircle(Circle circle, int width, int height) {
  final int radius = circle.getRadius();
  final Position pos = circle.getPosition();
  final int oldX = pos.getX();
  final int x = (oldX > width + radius) ? -radius : (
        (oldX < -radius) ? (width + radius) : oldX);

  final int y = // similar

  return new Position(x, y);
}

circle.setPosition(wrapCircle(circle, width, height));

另外,我将 wrapCircle 设为 Circle class 的一个方法,以获得:

circle.wrapCircle(width, height);

或者我可以更进一步并定义一个 getWrappedCircle 方法,即 returns 我一个新的圈子实例:

Circle getWrappedCircle(width, height) {
  newCircle = this.clone();
  newCircle.wrapCircle(width, height);
  return newCircle();
}

.. 取决于您打算如何构建其余代码。

提示:在 Java 中尽可能多地使用 final 关键字。它自动借给一个更实用的风格。

Is functional programming not a good fit for game development? I love the both, so I'm trying to combine them.

纯函数式编程较慢,因为它需要大量的数据复制/克隆。如果性能很重要,那么您绝对可以尝试混合方法,如上所示。

我建议尽可能多地使用不变性,然后进行基准测试,然后仅在性能关键部分转换为可变性。

函数式编程适合游戏开发(为什么不适合呢?)。问题通常更多地是关于性能和内存消耗,或者即使任何功能性游戏引擎都可以在这些指标上击败现有的非功能性游戏引擎。您不是唯一热爱函数式编程和游戏开发的人。似乎 John Carmack 也是如此,观看他在 Quakecon 2013 上关于主题的主题演讲 starting from 02:05. His notes here and here 甚至深入了解如何构建功能性游戏引擎。

抛开理论基础,新手和实际前景通常认为函数式编程中固有的两个概念。它们是数据不变性状态缺失。前者意味着数据永远不会改变,后者意味着每项任务都像第一次执行一样,没有先验知识。 考虑到这一点,你的命令式代码有两个问题:setters mutate 圆圈位置和代码依赖于外部值(全局 statewidthheight。要修复它们,请在每次更新时使您的函数 return 成为一个新圆圈,并将屏幕分辨率作为参数。让我们应用 the first clue from the video 并将对世界静态快照的引用和对实体 "updated" 的引用(这里只是 this)传递给更新函数:

class Circle extends ImmutableEntity {

        private int radius;

        public Circle(State state, Position position, int radius) {
            super(state, position);

            this.radius = radius;
        }

        public int getRadius() {
            return radius;
        }

        @Override
        public ImmutableEntity update(World world) {
            int updatedX = getPosition().getX();
            if (getPosition().getX() > world.getWidth() + radius){
                updatedX = -radius;
            } else if (getPosition().getX() < -radius){
                updatedX = world.getWidth() + radius;
            }

            int updatedY = getPosition().getX();
            if (getPosition().getY() > world.getHeight() + radius){
                updatedY = -radius;
            } else if (getPosition().getY() < -radius) {
                updatedY = world.getHeight() + radius;
            }

            return new Circle(getState(), new Position(updatedX, updatedY), radius);
        }
    }

    class Position {

        private int x;
        private int y;

       //here can be other quantities like speed, velocity etc.

        public Position(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public int getX() {
            return x;
        }

        public int getY() {
            return y;
        }
    }

    class State { /*...*/ }

    abstract class ImmutableEntity {

        private State state;
        private Position position;

        public ImmutableEntity(State state, Position position) {
            this.state = state;
            this.position = position;
        }

        public State getState() {
            return state;
        }

        public Position getPosition() {
            return position;
        }

        public abstract ImmutableEntity update(World world);
    }

    class World {
        private int width;
        private int height;

        public World(int width, int height) {
            this.width = width;
            this.height = height;
        }

        public int getWidth() {
            return width;
        }

        public int getHeight() {
            return height;
        }
    }

现在棘手的部分是如何影响世界和其他实体的状态。您可以遵循 second clue from the video 并使用事件传递机制来回传递此类更改,以便游戏的其余部分了解所有效果。

显然,即使在更改圈子位置时,您也可以只保留事件并完全依赖它们。因此,如果您向您的实体引入某种 id,您将能够传递 MoveEntity(id, newPosition).