2011-01-17 4 views
13

Долгое время Java-программист, медленно изучающий scala (любящий это кстати), и я думаю, что мой разум все еще обертывается вокруг концепции написания вещей функционально. Прямо сейчас я пытаюсь написать простой визуализатор для некоторых движущихся текстур 2d. Императивный подход достаточно прост, и я уверен, что большинство из вас признают это относительно повсеместного блок кода (материал был изменен, чтобы защитить невинных):программирование scala: продвижение позиции объекта в функциональном стиле

class MovingTexture(var position, var velocity) extends Renders with Advances { 
    def render : Unit = {...} 
    def advance(milliseconds : Float) : Unit = { 
     position = position + velocity * milliseconds 
    } 
} 

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

У кого-нибудь есть удивительное, элегантное, функциональное решение этой простой проблемы? Кто-нибудь знает об источнике, где я мог бы больше узнать о решении таких проблем?

ответ

11

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

Однако, в этом случае, есть несколько простых решений:

case class MovingTexture(position: VecXY, velocity: VecXY) extends Renders with Advances { 
    def advance(ms: Float) = copy(position = position + ms*velocity 
    def accelerate(acc: Float, ms: Float) = copy(velocity = velocity + ms*acc) 
    ... 
} 

То есть, вместо того, чтобы ваши классы обновить себя, у них возвращают новые копии себя. (Вы можете видеть, как это может стать дорогостоящим быстро. Для Tetris нет ничего сложного. Для Crysis? Может быть, это не так умно.) Кажется, что это просто подталкивает проблему на один уровень: теперь вам нужен var для MovingTexture, правильно? Совсем нет:

Iterator.iterate(MovingTexture(home, defaultSpeed))(_.advance(defaultStep)) 

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

В качестве альтернативы, вы можете

class Origin extends Renders { 
    // All sorts of expensive stuff goes here 
} 
class Transposed(val ori: Origin, val position: VecXY) extends Renders with Advances { 
    // Wrap TextureAtOrigin with inexpensive methods to make it act like it's moved 
    def moving(vel: VecXY, ms: Float) = { 
    Iterator.iterate(this).(tt => new Transposed(tt.ori, position+ms*vel)) 
    } 
} 

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

+1

Общее согласие (среди всех ответов) состоит в том, чтобы просто создавать копии моих объектов, которые представляют текущее состояние. Это не совсем то, что я искал, но вот этот ответ представляет собой действительно интересную пищу для размышлений, такую ​​как определение итераторов, которые выражают, как изменяются ценности (а не выражают состояние ценностей) ... Я надеялся на ответ, который найти какой-то способ выразить позицию как некоторую функцию (или итерацию), которая выражает изменение переменной (вроде интеграла в исчислении). Спасибо, что ввел меня в классные вещи! – jtb

14

Есть еще один путь к этому ответу, который может быть помещен в пространство одного ответа stackoverflow, но лучший и наиболее полный ответ на такие вопросы - использовать что-то, называемое функционально-реактивным программированием. Основная идея состоит в том, чтобы представлять каждое изменяющееся во времени или интерактивное количество не как изменяемую переменную, а как неизменный поток значений, по одному для каждого кванта времени. Тогда трюк заключается в том, что, хотя каждое значение представляет собой потенциально бесконечный поток значений, потоки лениво вычисляются (чтобы память не занималась до тех пор, пока это не понадобится), а значения потоков не учитываются для квантов времени в (так что предыдущие вычисления могут быть собраны в мусор). Расчет хорошо функциональный и неизменный, но часть вычисления, которое вы «смотрели на», меняется со временем.

Все это довольно сложно, и объединение потоков, подобных этому, является сложным, особенно если вы хотите избежать утечек памяти и все работает в потокобезопасном и эффективном режиме. Существует несколько библиотек Scala, которые реализуют функциональное реактивное программирование, но зрелость еще не очень высока. Наиболее интересным является, вероятно, scala.react, описанный here.

+0

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

+0

Ну, я на самом деле не очень много работаю на интерактивной стороне и никогда не использовал FRP в гневе. Совершено чтения, знаю теорию, но на самом деле не нарезать код. –

+0

Прохладный, ну, я, конечно, даже не слышал о FRP, прежде чем вы упомянули об этом, поэтому, по крайней мере, я сейчас более информирован, и у меня есть интересная статья для чтения. Благодаря! – jtb

1

Вот пример какого-то кода, над которым я работал, использует подход, позволяющий напрямую возвращать копию, а не мутировать состояние.Приятная вещь в этом виде подхода, по крайней мере на стороне сервера, заключается в том, что он позволяет мне легко реализовать семантику типа транзакции. Если что-то пойдет не так, когда вы делаете обновление, для меня тривиально иметь все, что было обновлено в согласованном состоянии.

Код, приведенный ниже, с игрового сервера, над которым я работаю, что делает что-то похожее на то, что вы делаете, это для отслеживания объектов, которые перемещаются во временных срезах. Этот подход не столь впечатляющий, как предлагает Дейв Гриффит, но он может быть полезен вам для созерцания.

case class PosController(
    pos: Vector3 = Vector3.zero, 
    maxSpeed: Int = 90, 
    velocity: Vector3 = Vector3.zero, 
    target: Vector3 = Vector3.zero 
) { 
    def moving = !velocity.isZero 

    def update(elapsed: Double) = { 
     if (!moving) 
      this 
     else { 
      val proposedMove = velocity * elapsed 
      // If we're about to overshoot, then stop at the exact position. 
      if (proposedMove.mag2 > pos.dist2(target)) 
       copy(velocity = Vector3.zero, pos = target) 
      else 
       copy(pos = pos + proposedMove) 
     } 
    } 

    def setTarget(p: Vector3) = { 
     if (p == pos) 
      this 
     else { 
      // For now, go immediately to max velocity in the correct direction. 
      val direction = (p - pos).norm 
      val newVel = direction * maxSpeed 
      copy(velocity = direction * maxSpeed, target = p) 
     } 
    } 

    def setTargetRange(p: Vector3, range: Double) = { 
     val delta = p - pos 
     // Already in range? 
     if (delta.mag2 < range * range) 
      this 
     else { 
      // We're not in range. Select a spot on a line between them and us, at max range. 
      val d = delta.norm * range 
      setTarget(p - d) 
     } 
    } 

    def eta = if (!moving) 0.0 else pos.dist(target)/maxSpeed 
} 

Одна хорошая вещь о конкретных классов в Scala является то, что они создают метод копирования() для you-- просто пройти, в котором параметры изменились, а другие сохраняют ту же величину. Вы можете закодировать это вручную, если вы не используете классы case, но вам нужно помнить об обновлении метода копирования всякий раз, когда вы меняете значения, присутствующие в классе.

Что касается ресурсов, то для меня действительно что-то изменилось, когда я проводил время в Эрланге, где в принципе нет выбора, кроме как использовать неизменяемое состояние. У меня есть две книги Эрланг, над которыми я работал, и внимательно изучил каждый пример. Это, плюс, заставляя меня кое-что сделать в Erlang, сделало меня намного более удобным с работой с неизменяемыми данными.

+0

Как ни странно, Эрланг - это следующий язык, который я собирался забрать (или, может быть, Haskell), хотя мой слабый мозг все еще много работает с Scala. Мне нравятся ваши примеры! Я все еще думаю, что должен быть более общий способ представить, как переменные изменяются, а не просто копировать объекты на каждую итерацию. Вы когда-нибудь экспериментировали с представлением позиции как функции положения во времени, а не как var? – jtb

+0

У меня нет. Я начал с сохранения состояния для актера в пределах одного var, состоящего из нескольких val. Например, контроллер положения - один val, а состояние мозга - другое значение val. Все они перевернуты в одну переменную состояния.Если у меня есть метод, который меняет один, меняет другой и, возможно, выдает исключение посередине, я гарантирую, что либо все 3 ops работают, либо все 3 сбой, а состояние сохраняется. Причина, по которой я не выражала ее как функцию времени, заключается в том, что целевые позиции иногда контролируются классом мозга, а иногда и игроком. – Unoti

+0

Вы упомянули, что в Scala есть много возможностей обернуть ваш мозг. Для меня я обнаружил, что Скала намного сложнее Эрланг - сначала. Другие люди чувствуют себя по-другому, но я написал мысли об Эрланге против Скалы во время учебного процесса здесь http://tango11.com/news/scala-complexity-vs-erlang-complexity/, который может вас очень заинтересовать. – Unoti

2

Есть брошюра под названием «Как создавать миры» (авторы «Как разрабатывать программы»), которая идет в некоторой степени о чисто функциональном подходе к программированию интерактивных приложений.

В принципе, они представляют собой «мир» (тип данных, содержащий все состояние игры), а также некоторые функции, такие как «тик» (типа world -> world) и «onkeypress» (типа key * world -> мир). Затем функция «рендеринга» берет мир и возвращает сцену, которая затем передается в «реальный» рендерер.

0

Эта серия коротких статей помогла мне в качестве новичка в мышлении Функционально в решении проблем программирования. В игре есть Ретро (Pac Man), но программист - нет. http://prog21.dadgum.com/23.html

Смежные вопросы