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.
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:
- Inputs:
Mouse
, Time
and ports in your case.
- Model: The state of the game, in your case
cost
, balance
, canAfford
, spent
, gathered
etc.
- 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.
- 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
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…