Подтвердить что ты не робот

Перевод императивной Java на функциональную Java (игра)

Я изучаю намного больше о 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());
}
  • Как я могу попытаться "Функционализировать" его? Может быть, какой-то псевдокод? Мне кажется, что изменчивость и состояние кажутся присущими этому примеру.
  • Функциональное программирование не подходит для разработки игр? Мне нравятся оба, поэтому я пытаюсь их объединить.
4b9b3361

Ответ 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, которые будут возвращаться, когда вы изучаете новый стиль.

Ответ 2

Для вашего первого момента: вы "функционируете" на своем примере, думая о том, что должен делать код. И это, у вас есть круг, и вы хотите вычислить другой круг, основанный на некоторых условиях. Но по какой-то причине ваше императивное воспитание заставляет вас предположить, что входной круг и выходной круг должны храниться в одних и тех же ячейках памяти!

Для того, чтобы быть функциональным, первое, что нужно, - забыть места памяти и принять значения. Подумайте о каждом типе так же, как вы думаете о 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);

Вы бы поднялись на первый шаг к функциональному программированию, когда последний код кажется вам абсурдным, как первый. И да, это означает не использовать некоторые особенности императивного языка, который вы используете, или использовать их чрезвычайно экономно.

Ответ 3

Здесь не так много "функционализации". Но по крайней мере мы можем бороться с mutability. Прежде всего pure functions. Это поможет отделить logic. Сделайте его понятным и легким для тестирования. Ответьте на вопрос: что делает ваш код? Он принимает некоторые параметры и возвращает два параметра new x и y. Следующие образцы будут записаны с помощью 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.

Как выглядят функциональные программы. Я создал функцию более высокого порядка (которая принимает другие функции в качестве аргументов и вызывает ее внутри). С помощью этих функций я вызывал одну операцию, которую вы должны выполнить... Но такой функциональный стиль действительно не помогает. Вообще. Вы должны применять его правильно только в местах, где это упрощает код.

Ответ 4

Вы можете написать функциональный код практически на любом языке программирования, но вы не можете легко изучить функциональное программирование на любом языке. Java, в частности, делает функциональное программирование достаточно болезненным, что люди, которые хотели выполнять функциональное программирование в JVM, придумали Clojure и Scalaz. Если вы хотите изучить функциональный способ мышления (с какими проблемами он сталкивается естественно и как, какие проблемы более неудобны и как они управляют ими и т.д.), Я настоятельно рекомендую вам провести некоторое время с функциональным или главным образом функциональным язык. Основываясь на сочетании качества языка, простоты использования функциональных идиом, учебных ресурсов и сообщества, моим лучшим выбором будет Haskell, а мой следующий будет Racket. У других, конечно же, есть и другие мнения.

Ответ 5

Как я могу попытаться "Функционализировать" его? Может быть, некоторые псевдо-код? Мне кажется, что изменчивость и состояние кажутся присущими этот пример.

Вы можете попытаться ограничить изменчивость несколькими функциями, а также использовать конечные переменные внутри функций (что заставляет вас использовать выражения, а не инструкции). Вот один из возможных способов:

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. Это автоматически придает более функциональный стиль.

Функциональное программирование не подходит для развития игры? Мне нравятся оба, поэтому я пытаюсь их объединить.

Чисто функциональное программирование происходит медленнее, поскольку требует много копирования/клонирования данных. Если производительность важна, вы можете определенно попробовать смешанный подход, как показано выше.

Я бы предложил использовать максимально возможную неизменность, а затем провести бенчмаркинг, а затем преобразовать в изменчивость только в критичных для производительности разделах.

Ответ 6

Функциональное программирование подходит для развития игры (почему бы и нет?). Вопрос, как правило, связан скорее с производительностью и потреблением памяти, или даже если любой функциональный движок игры может превзойти существующий нефункциональный в этих показателях. Вы не единственный человек, который любит функциональное программирование и разработку игр. Похоже, что и Джон Кармак тоже смотрит свои лейтмотивы на темы в Quakecon 2013 начиная с 02:05. Его примечания здесь и здесь даже дают представление о том, как функциональный игровой движок может быть структурирован.

Установив теоретическую основу в сторону, обычно есть две концепции, которые воспринимаются новичками, присущими новичкам, и с практической точки зрения. Это неизменность данных и отсутствие состояния. Первое означает, что данные никогда не меняются, а последнее означает, что каждая задача выполняется так, как если бы она впервые не имела никаких предварительных знаний. Учитывая, что у вас императивный код имеет две проблемы: сеттеры мутировать позицию круга и код полагается на внешние значения (глобальное состояние) width и height, Чтобы исправить их, ваша функция возвращает новый круг для каждого обновления и принимает разрешения экрана в качестве аргументов. Позвольте применить первый ключ из видео и передать ссылку на статический снимок мира и ссылку на объект, который обновляется (он просто 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;
        }
    }

Теперь сложная часть - как повлиять на состояние мира и других объектов. Вы можете следить за вторым ключом из видео и использовать механизм передачи событий, чтобы передавать такие изменения туда и обратно, поэтому остальная часть игры знает обо всех эффекты.

Очевидно, что вы можете сохранить только события и полностью опираться на них, даже если вы меняете свои круглые позиции. Итак, если вы вводите свой идентификатор своим сущностям, вы сможете пройти MoveEntity(id, newPosition).

Ответ 7

Хорошо, нам пора все пережить, как выглядят новые и блестящие функциональные возможности Java 8. "Функционализация" - это действительно нецелесообразная цель.

Однако исходный код здесь имеет хорошую объектно-ориентированную проблему:

Когда вы говорите circle.getPosition(). setX (...), вы возитесь с внутренним состоянием круга (его положением) без привлечения самого объекта. Это нарушает инкапсуляцию. Если класс круга был правильно спроектирован, метод getPosition() вернул бы копию позиции или неизменяемой позиции, чтобы вы не могли этого сделать.

Это проблема, которую вам действительно нужно исправить с помощью этого кода...

Как же вы должны это сделать?

Ну, вы, возможно, придумали какой-то функциональный интерфейс в Circle, но, честно говоря, ваш код будет более читабельным, если у вас просто есть circle.move(double x, double y);