13

我正在学习更多关于 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. 我怎么能尝试“功能化”它?也许是一些伪代码?在我看来,可变性和状态似乎在这个例子中是固有的。
  2. 函数式编程不适合游戏开发吗?我喜欢两者,所以我试图将它们结合起来。
4

7 回答 7

7

在这个例子中,对可变性的要求没有任何内在的东西。命令式方法是通过应用改变现有圆圈状态的副作用来修改现有圆圈。

函数式方法是拥有一个不可变的数据结构并创建一个函数,该函数从第一个结构中获取数据并创建一个新结构。在您的示例中,功能方法将使圆圈不可变,即没有 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 不同,您在学习新风格时不会依赖 OOP 功能。

于 2015-11-04T11:47:24.077 回答
3

对于您的第一点:您通过思考代码应该实现什么来“功能化”您的示例。这就是,你有一个圆,并想根据某些条件计算另一个圆。但是由于某种原因,你的教养让你假设输入圆和输出圆应该存储在相同的内存位置!

为了发挥功能,首先要忘记记忆位置并拥抱价值观。int以与您对其他数字类型或其他数字类型相同的方式来考虑每种java.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);

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

于 2015-11-01T22:09:04.530 回答
2

这里没有太多适用的“功能化”。但至少我们可以与之抗争mutability。首先pure functions。这将有助于分离logic。使其清晰且易于测试。回答问题:你的代码是做什么的?它接受一些参数并返回两个参数 newxy。下一个示例将使用 pseudoscala 编写。

因此,您需要一个函数,该函数将为 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.

}

PS> 到目前为止,无论您想在哪里应用函数式样式:由于您需要执行一些业务逻辑,因此最好使用函数式方法。

但不要试图让它过于复杂,因为它没有帮助。所以接下来我不会为这个样本做的事情(接下来是伪 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.

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

于 2015-11-01T22:36:39.560 回答
1

我怎么能尝试“功能化”它?也许是一些伪代码?在我看来,可变性和状态似乎在这个例子中是固有的。

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

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类的方法,以获得:

circle.wrapCircle(width, height);

或者我可以更进一步,定义一个getWrappedCircle方法,它返回一个新的圆实例:

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

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

提示:final在 Java 中尽可能多地使用关键字。它会自动赋予更实用的风格。

函数式编程不适合游戏开发吗?我喜欢两者,所以我试图将它们结合起来。

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

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

于 2015-11-08T07:31:44.150 回答
1

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

抛开理论基础不谈,通常有两个概念被新手和实际前景感知到函数式编程中固有的概念。它们是数据不变性状态缺失。前者意味着数据永远不会改变,后者意味着每项任务都是在没有先验知识的情况下第一次执行。考虑到这一点,您的命令式代码有两个问题:setter改变了圆的位置,并且代码依赖于 and 的外部值(全局state)。要修复它们,请让您的函数在每次更新时返回一个新圆圈,并将屏幕分辨率作为参数。让我们应用视频中的第一个线索widthheight并将对世界静态快照的引用和对正在“更新”的实体(只是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;
        }
    }

现在棘手的部分是如何影响世界和其他实体的状态。您可以按照视频中的第二条线索,使用事件传递机制来回传递这些更改,以便游戏的其余部分了解所有效果。

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

于 2015-11-08T13:42:07.440 回答
1

您几乎可以使用任何编程语言编写函数式代码,但您无法轻松地学习任何语言的函数式编程。特别是 Java 让函数式编程变得非常痛苦,以至于想要在 JVM 中进行函数式编程的人想出了 Clojure 和 Scalaz。如果你想学习函数式思维方式(它自然地处理哪些问题以及如何处理,哪些问题更尴尬以及如何处理它们等),我强烈建议你花一些时间在函数式或主要函数式上语。基于语言质量、易于使用功能习语、学习资源和社区的结合,我的首选是 Haskell,我的下一个选择是 Racket。其他人当然会有其他意见。

于 2015-10-13T19:27:00.740 回答
0

好的,是时候让我们大家了解一下 Java 8 的新功能特性的外观了。“功能化”某些东西确实不是一个有效的目标。

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

当你说 circle.getPosition().setX(...) 时,你是在搞乱圆的内部状态(它的位置),而不涉及对象本身。这打破了封装。如果圆形类设计得当,那么 getPosition() 方法将返回位置的副本或不可变的位置,因此您无法执行此操作。

是您真正需要使用此代码解决的问题...

那么,你应该怎么做呢?

好吧,你当然可以在 Circle 中提供一些功能接口,但老实说,如果你只有 circle.move(double x, double y); 你的代码将更具可读性。

于 2015-11-07T22:04:37.137 回答