【问题标题】:Translating Imperative Java to Functional Java (A Game)将命令式 Java 转换为函数式 Java(游戏)
【发布时间】:2016-01-08 14:29:55
【问题描述】:

我正在学习更多关于 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. 函数式编程不适合游戏开发吗?我喜欢两者,所以我试图将它们结合起来。

【问题讨论】:

    标签: java functional-programming java-8 imperative-programming declarative-programming


    【解决方案1】:

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

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

    【讨论】:

    • circles.stream().map(circ -&gt; wrapCircleAroundBounds(circ)) 不只是返回一个新流吗?我最终是否必须修改circles 列表?也许重新分配?
    • @Michael 您可以使用.collect(Collectors.toList()) 将流转换回列表,如果这是您需要结果的形式。在函数式编程中,您不倾向于修改或重新分配事物,您通常只会将计算结果存储到一个新的列表中。
    • wrapCircleAroundBounds 依赖于 widthheight 的外部值,因此不是纯函数式的。
    • @MaximPlevako 这是一个很好的观点。将更新我的答案。
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

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

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

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

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

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

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

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

      【讨论】:

        【解决方案4】:

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

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

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

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

        那么,你应该怎么做呢?

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

        【讨论】:

          【解决方案5】:

          这里没有太多适用的“功能化”。但至少我们可以和mutability抗争。 首先pure functions。这将有助于分离logic。使其清晰且易于测试。 回答问题:你的代码是做什么的?它接受一些参数并返回两个新参数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.
          

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

          【讨论】:

          • 那么,您认为这种情况不适合函数式编程吗?命令式更适合它?
          • 我认为你应该编写干净的代码。我刚刚展示了丑陋的功能示例(第二部分)。但它可以稍微清理一下,并且与以命令式编写的方式更好或相同。
          • 这不是惯用的函数式代码,它是具有一流函数的命令式代码。您正在传递一个执行移动的函数,但该函数是单位类型的,即它是一个副作用。纯函数式代码完全避免了副作用,这意味着 X 和 Y 不应该在任何地方发生变异。
          【解决方案6】:

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

          为了发挥功能,首先要忘记记忆位置并接受价值观。以与 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);
          

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

          【讨论】:

          • 谢谢,很棒的答案!此外,你的弗雷格语言看起来很棒,干得好! @Ingo
          【解决方案7】:

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

          【讨论】:

            猜你喜欢
            • 2011-09-30
            • 1970-01-01
            • 1970-01-01
            • 2011-05-24
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-10-20
            • 2018-03-13
            相关资源
            最近更新 更多