【问题标题】:Mutually dependant signals相互依赖的信号
【发布时间】:2014-03-07 21:49:59
【问题描述】:

纯问题:

有没有办法在 Elm 中定义一对相互依赖的信号?

序言:

我正在尝试编写一个小型 Cookie-clicker 风格的浏览器游戏,玩家在其中收集资源,然后用它们购买自主资源收集结构,这些结构在购买时会变得更昂贵。这意味着三个相关信号:gathered(玩家收集了多少资源)、spent(玩家已经花费了多少资源)和cost(升级成本)。

这是一个实现:

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 ]

这编译得很好,但是当我将它嵌入到一个 HTML 文件中并连接了相应的按钮以将消息发送到上面的相应端口时,我收到了错误

s2 is undefined
    Open the developer console for more details.

问题似乎是,正如写的那样,cost 依赖于canAfford,它依赖于balance,它依赖于spent,它又依赖于cost

如果我这样修改成本线

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

它开始按预期工作(除了允许玩家花费负资源,这是我想避免的)。

有什么想法吗?

【问题讨论】:

    标签: elm


    【解决方案1】:

    回答你的简单问题

    ,Elm 中没有定义相互递归信号的通用方法。
    问题在于 Elm 中的 Signal 必须始终具有值的约束。如果cost 的定义需要canAfford,但canAfford 是根据cost 定义的,那么问题是从哪里开始解析信号的初始值。当您根据相互递归的信号进行思考时,这是一个难以解决的问题。

    相互递归的信号与信号的过去值有关。 foldp 构造允许您指定直到一个点的相互递归信号的等价物。初始值问题的解决方案是通过为foldp 提供一个显式参数来解决的,即初始值。但约束是foldp 只接受纯函数。

    这个问题很难用不需要任何先验知识的方式清楚地解释。所以这里有另一种解释,基于我用你的代码制作的图表。

    花点时间找出代码和图表之间的联系(请注意,我省略了main 以简化图表)。 foldp 是一个带有环回的节点,sampleOn 有一个闪电等(我将sampleOn 重写为always 的恒定信号)。有问题的部分是向上的红线,在cost的定义中使用canAfford
    如您所见,基本的foldp 有一个带有基值的简单循环。实现这一点比像你这样的任意环回更容易。

    我希望你现在明白这个问题。限制在于 Elm,这不是你的错。
    我正在解决 Elm 中的这个限制,尽管这样做需要一些时间。

    解决您的问题

    虽然命名信号并使用它们会很好,但在 Elm 中实现游戏时,使用different programming style 通常会有所帮助。链接文章中的想法归结为将您的代码拆分为:

    1. 输入:MouseTime 和端口在您的情况下。
    2. 模型:游戏状态,在您的情况下为 costbalancecanAffordspentgathered 等。
    3. 更新:游戏的更新功能,您可以将这些更新功能组合成更小的更新功能。这些应该尽可能是函数。
    4. 查看:查看模型的代码。

    使用main = view &lt;~ foldp update modelStartValues inputs 之类的东西将它们联系在一起。

    特别是,我会这样写:

    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
    

    【讨论】:

    • 你是如何解决信号类型限制的信号的?
    • @CMCDragonKai 你的意思是我打算如何解决 Elm 的一般限制?或者只是我帖子中的代码如何解决 OP 的问题?
    • 总的来说,我很好奇! Kafka/Eventsourcing 也有这个问题。
    • @CMCDragonkai 最近关于邮件列表的讨论在这里:groups.google.com/d/msg/elm-discuss/AQpYNAO5Y8g/pcKgx7vqz9gJ。较早的讨论使用名称“信号循环”来表示我正在开发的功能。但这些都是关于它应该如何工作的讨论,而不是关于它如何工作的明确解释。
    • 谢谢,我会处理的。
    猜你喜欢
    • 2014-02-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-18
    • 2013-04-06
    • 1970-01-01
    • 2011-01-25
    相关资源
    最近更新 更多