Question

Bare question:

Is there a way of defining a pair of signals that depend on each other in Elm?

Preamble:

I'm trying to write a tiny Cookie-clicker-style browser game in which the player is gathering resources, then spending them to purchase autonomous resource-gathering constructs which get more expensive as they're purchased. That implies three relevant signals: gathered (how much resources the player has gathered), spent (how much resource the player has already spent) and cost (how much an upgrade costs).

Here's an implementation:

module Test where

import Mouse
import Time

port gather : Signal Bool
port build : Signal String

costIncrement = constant 50
cost = foldp (+) 0 <| keepWhen canAfford 0 <| sampleOn build costIncrement
nextCost = lift2 (+) cost costIncrement

spent = foldp (+) 0 <| merges [ sampleOn build cost ]

gathered = foldp (+) 0 <| merges [ sampleOn gather <| constant 1, sampleOn tick tickIncrement ]

balance = lift round <| lift2 (-) gathered spent

canAfford = lift2 (>) balance <| lift round nextCost

tickIncrement = foldp (+) 0 <| sampleOn cost <| constant 0.01
tick = sampleOn (every Time.millisecond) <| constant True

main = lift (flow down) <| combine [ lift asText balance, lift asText canAfford, lift asText spent, lift asText gathered, lift asText nextCost ]

This compiles fine, but when I embed it in an HTML file with the appropriate buttons hooked up to send messages to the appropriate ports above, I get the error

s2 is undefined
    Open the developer console for more details.

The problem seems to be that as written, cost depends on canAfford, which depends on balance, which depends on spent, which depends on cost again.

If I modify the cost line such that

...
cost = foldp (+) 0 <| sampleOn build costIncrement
...

it starts working as expected (except that the player is allowed to spend into negative resources, which is what I'd like to avoid).

Any ideas?

Was it helpful?

Solution

Answer to your bare question

No, there is no general way in Elm to define mutually recursive signals.
The problem lies in the constraint that a Signal in Elm must always have a value. If the definition of cost requires canAfford but canAfford is defined in terms of cost, the problem is where to start with resolving the initial value of the signal. This is a tough problem to solve when you think in terms of mutually recursive signals.

Mutually recursive signals have everything to do with past values of signals. The foldp construct allows you to specify the equivalent of mutually recursive signals up to a point. The solution to the initial value problem is solved by having an explicit argument to foldp that is the initial value. But the constraint is that foldp only takes pure functions.

This problem is hard to clearly explain in a way that doesn't require any prior knowledge. So here's another explanation, based on a diagram I made of your code.

signal graph of the code given by the OP

Take your time to find the connections between the code and the diagram (note that I left out main to simplify the graph). A foldp is a node with a loop back, sampleOn has a lightning bolt etc. (I rewrote sampleOn on a constant signal to always). The problematic part is the red line going up, using canAfford in the definition of cost.
As you can see, a basic foldp has a simple loop with a base value. Implementing this is easier than arbitrary loop-back like yours.

I hope you understand the problem now. The limitation is in Elm, it's not your fault.
I'm resolving this limitation in Elm although it will take some time to do so.

Solution to your problem

Although it can be nice to name signals and work with those, when implementing games in Elm it usually helps to use a different programming style. The idea in the linked article comes down to splitting your code up in:

  1. Inputs: Mouse, Time and ports in your case.
  2. Model: The state of the game, in your case cost, balance, canAfford, spent, gathered etc.
  3. Update: The update function of the game, you can compose these out of smaller update functions. These should be pure functions as much as possible.
  4. View: Code to view the model.

Tie it all together by using something like main = view <~ foldp update modelStartValues inputs.

In particular, I would write it like:

import Mouse
import Time

-- Constants
costInc      = 50
tickIncStep  = 0.01
gatherAmount = 1

-- Inputs
port gather : Signal Bool
port build : Signal String

tick = (always True) <~ (every Time.millisecond)

data Input = Build String | Gather Bool | Tick Bool

inputs = merges [ Build  <~ build
                , Gather <~ gather
                , Tick   <~ tick
                ]

-- Model

type GameState = { cost          : Float
                 , spent         : Float
                 , gathered      : Float
                 , tickIncrement : Float
                 }

gameState = GameState 0 0 0 0

-- Update

balance {gathered, spent} = round (gathered - spent)
nextCost {cost} = cost + costInc
canAfford gameSt = balance gameSt > round (nextCost gameSt)

newCost input gameSt =
  case input of
    Build _ -> 
      if canAfford gameSt
        then gameSt.cost + costInc
        else gameSt.cost
    _ -> gameSt.cost

newSpent input {spent, cost} = 
  case input of
    Build _ -> spent + cost
    _ -> spent

newGathered input {gathered, tickIncrement} = 
  case input of
    Gather _ -> gathered + gatherAmount
    Tick   _ -> gathered + tickIncrement
    _ -> gathered

newTickIncrement input {tickIncrement} =
  case input of
    Tick _ -> tickIncrement + tickIncStep
    _ -> tickIncrement

update input gameSt = GameState (newCost          input gameSt)
                                (newSpent         input gameSt)
                                (newGathered      input gameSt)
                                (newTickIncrement input gameSt)

-- View
view gameSt = 
  flow down <| 
    map ((|>) gameSt)
      [ asText . balance
      , asText . canAfford
      , asText . .spent
      , asText . .gathered
      , asText . nextCost ]

-- Main

main = view <~ foldp update gameState inputs
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top