Fighting with FRP
Asked Answered
A

2

6

I've read about FRP and was very excited. It looks great, so you can write more high-level code, and everything is more composable, and etc.

Then I've tried to rewrite my own little game with a few hundreds sloc from plain js to Bacon.

And I found that instead of writing high-level logic-only code, I actually beating with Bacon.js and its adherence to principles.

I run into some headache that mostly interfere clean code

  1. .take(1)

Instead of getting value, I should create ugly constructions.

  1. Circular dependencies

Sometimes they should be by logic. But implementing it in FRP is scary

  1. Active state

Even creator of bacon.js have troubles with it.


As example here is the peace of code to demonstrate the problem:

Task is to not allow two players stay at same place

Implemented with bacon.js

http://jsbin.com/zopiyarugu/2/edit?js,console

function add(a) {return function(b){return a + b}}
function nEq(a) {return function(b){return a !== b}}
function eq(a) {return function(b){return a === b}}
function always(val) {return function(){return val}}
function id(a){return a}

var Player = function(players, movement, initPos){
    var me = {};
    me.position = movement
        .flatMap(function(val){
            return me.position
                .take(1)
                .map(add(val))
        })
        .flatMap(function(posFuture){
            var otherPlayerPositions = players
                .filter(nEq(me))
                .map(function(player){return player.position.take(1)})
            return Bacon
                .combineAsArray(otherPlayerPositions)
                .map(function(positions){
                    return !positions.some(eq(posFuture));
                })
                .filter(id)
                .map(always(posFuture))
        })
        .log('player:' + initPos)
        .toProperty(initPos);
    return me;
}

var moveA = new Bacon.Bus();
var moveB = new Bacon.Bus();

var players = [];
players.push(new Player(players, moveA, 0));
players.push(new Player(players, moveB, 10));

moveA.push(4);
moveB.push(-4);
moveA.push(1);
moveB.push(-1);
moveB.push(-1);
moveB.push(-1);
moveA.push(1);
moveA.push(-1);
moveB.push(-1);

What I want to demonstrate is:

  1. me.positions have dependency on its own
  2. It's not easy to understand this code. Here is imperative implementation. And it looks much easier to understand. I spent much more time with bacon implementation. And in result I'm not sure that it will works as expected.

My question:

Probably I miss something fundamental. Maybe my implementation is not so in FRP style?

Maybe this code looks ok, and it just unaccustomed with new coding style?

Or this well-known problems, and I should choose best of all evil? So troubles with FRP like described, or troubles with OOP.

Amphitheater answered 10/4, 2015 at 14:25 Comment(0)
R
4

I've had similar experiences when trying to write games with Bacon and RxJs. Things that have a self-dependency (like player's position) are tough to handle in a "pure FRP" way.

For example, in my early Worzone game I included a mutable targets object that can be queried for positions of players and monsters.

Another approach is to do as the Elm guys do: model the full game state as a single Property (or Signal as it's called in Elm) and calculate the next state based on that full state.

So far my conclusion is that FRP is not so well-suited for game programming, at least in a "pure" way. After all, mutable state might be the more composable approach for some things. In some game projects, like the Hello World Open car race, I've used mutable state, like the DOM for storing state and EventStreams for passing events around.

So, Bacon.js is not a silver bullet. I suggest you find out yourself, where to apply FRP and where not to!

Rumpus answered 11/4, 2015 at 10:26 Comment(5)
I don't think in Elm you have to base your player actions on the "full state", you can create small self-dependent "modules" as well. Of course, as in any pure language, you have to combine them to a global, full program.Sheritasherj
Sure, but in Elm, you cannot have circular deps, which kinda pushes you to this "world as Signal" model. And I'm not saying this is bad, just pointing out that it might be the only valid model when writing a game in a fully functional style. See elm-lang.org/edit/examples/Intermediate/Pong.elm for example.Rumpus
Ah, they endorse this "single pure world state" model, where only a single world signal does depend on itself (gameState in the pong example). But yes, you can have circular dependencies, just use Signal.foldp anywhere.Sheritasherj
Well sort of circular, in the sense that your Signal can depend on its own past value. But you cannot have signals A and B which depend on each other. Which is often the case in games.Rumpus
Ah, that's what you were referring to. Though a true circular dependency cannot really be resolved at all, what you mean is that A does depend on the previous A and previous B just as B does depend on both previous values. For those, you need a common "world signal" that includes A and B indeed.Sheritasherj
K
1

I have a similar filling sometimes. For me the experience of programming with FRP is mostly solving puzzles. Some of them are easy, some not. And those that I find easy might be harder form my colleagues and vice versa. And I don't like this about FRP.

Don't get me wrong, I like solving puzzles, this is a lot fun! But I think programming at payed work should be more... boring. More predictable. And code should be as straightforward as possible, even primitive.

But of course global mutable state also not the way we should go. I think we should find a way to make FRP more boring :)


Also a remark on your code, I think this will be more FRP'ish (a not tested draft):

var otherPlayerPositions = players
    .filter(nEq(me))
    .map(function(player){return player.position});

otherPlayerPositions = Bacon.combineAsArray(otherPlayerPositions);

me.position = otherPlayerPositions
    .sampledBy(movement, function(otherPositions, move) {
        return {otherPositions: otherPositions, move: move};
    })
    .scan(initPos, function(myCurPosition, restArgs) {
        var myNextPosition = myCurPosition + restArgs.move;
        if (!restArgs.otherPositions.some(eq(myNextPosition))) {
            return myNextPosition;
        } else {
            return myCurPosition;
        }
    });
Kurtzman answered 20/4, 2015 at 23:40 Comment(1)
I'd say straightforward is one thing, "primitive" or "boring" is another... FP claims itself to lead to easier-to-understand and thus supposedly more straightforward code, but of course as we can see, especially in FRP, that's not always true. No one silver bullet then.Eller

© 2022 - 2024 — McMap. All rights reserved.