什么是初始化 Elm 应用程序的正确方法?

18

Elm的Random模块文档写道:

获取一个意外的种子的好方法是使用当前时间。http://package.elm-lang.org/packages/elm-lang/core/1.1.0/Random

然而,我没有看到如何在FRP应用程序中执行这样的初始化逻辑的好例子。 我应该对哪个信号做出反应? 如何用最少的代码和最大的清晰度来完成这项工作。


我们现在正在 Elm 邮件列表上讨论这个问题:https://groups.google.com/d/msg/elm-discuss/ilDp3-ekJ98/cA-oiAAzWskJ - Craig Stuntz
4个回答

27

有不同的方法可以做到这一点。每种方法都有自己的好处。我将为您提供我知道的三种方法,每种方法都有一个类似的示例。

1)添加时间记时器输入

你可以在程序的输入中添加时间。以下是一个使用当前时间每秒生成一个随机数的微型程序示例:

import Time
import Time (Time, second)
import Text (asText)
import Mouse
import Signal
import Signal (Signal, (<~), (~))
import Random
import Random (Seed)
import Graphics.Element (Element)

randomInt : Seed -> Int
randomInt seed = seed |> (Random.generate <| Random.int 1 10) |> fst

otherInput : Signal (Int,Int)
otherInput = Mouse.position

timeSeed : Signal Seed
timeSeed = Random.initialSeed << round <~ Time.every second

inputs : Signal (Seed,(Int,Int))
inputs = (,) <~ timeSeed ~ otherInput

update : (Seed, (Int,Int)) -> (Int,Int) -> (Int,Int)
update (seed,(x,y)) (x',y') =
  let num = randomInt seed
  in (x-x'-num,y'-y+num) -- this update function is nonsense

main : Signal Element
main = asText <~ Signal.foldp update (0,0) inputs
如果你需要时间作为输入,并且基于这个时间采样其他输入,那么这是最简单的方法。(如果你已经使用Time.fps,请使用Time.timestamp来获取实际时间)

2) 使用signal在启动时

如果你正常情况下不需要将时间作为程序输入,那么前面的解决方案就不是理想的选择。你可能更喜欢使用程序启动时间初始化程序状态,而不必在程序运行时忽略时间触发器。
可能最容易的方法是使用signal-extra package*。使用Signal.Time.startTime获取一个信号,它不会tick,只有程序的初始值作为初始值。使用Signal.Extra.foldp',这样你就可以使用你的输入的初始值。
import Time
import Time (Time, second)
import Text (asText)
import Mouse
import Signal
import Signal (Signal, (<~), (~))
import Random
import Random (Seed)
import Graphics.Element (Element)
import Signal.Extra as SignalE
import Signal.Time as Time

randomInt : Seed -> (Int,Seed)
randomInt seed = (Random.generate <| Random.int 1 10) |> fst

otherInput : Signal (Int,Int)
otherInput = Mouse.position

startTimeSeed : Signal Seed
startTimeSeed = Random.initialSeed << round <~ Time.startTime

inputs : Signal (Seed,(Int,Int))
inputs = (,) <~ startTimeSeed ~ otherInput

update (x,y) (seed,(x',y')) =
  let (num,seed') = randomInt seed
  in (seed',(x-x'-num,y'-y+num))

main : Signal Element
main = asText <~ SignalE.foldp' (snd >> update) identity inputs

*因为我是链接包的作者,所以可能存在偏见。但我不知道其他提供相同功能的包。

3) 带有端口的启动

如果您认为之前的解决方案不令人满意,因为您需要向输入添加不变的 Signal,那么这个解决方案适合您。在这里,我们使用JavaScript交互获取程序的启动时间,并将其作为常量值(无需 Signal)发送到 Elm 中。Elm 代码如下:

import Time
import Time (Time, second)
import Text (asText)
import Mouse
import Signal (Signal, (<~))
import Random
import Random (Seed)
import Graphics.Element (Element)

port startTime : Float

randomInt : Seed -> (Int,Seed)
randomInt seed = (Random.generate <| Random.int 1 10) |> fst

startTimeSeed : Seed
startTimeSeed = Random.initialSeed <| round startTime

update (x,y) (seed,(x',y')) =
  let (num,seed') = randomInt seed
  in (seed',(x-x'-num,y'-y+num))

main : Signal Element
main = asText <~ Signal.foldp update (startTimeSeed, (0,0)) Mouse.position

那么这里的缺点是什么?你需要编写一些 JavaScript 代码。而不是标准的

<script>Elm.fullscreen(Elm.<YourModule>)</script>

你需要在你的HTML文件中添加类似这样的内容:

<script>Elm.fullscreen(Elm.<YourModule>, {startTime: Date.now()})</script>

如果你选择这种方式,或许使用 JavaScript 中的随机数作为初始种子是个好主意。我读过一些资料,说这样更加安全(免责声明:我对这方面不是很了解)。所以你需要一个 port aRandomNumber : Int{aRandomNumber: Math.floor((Math.random() - 0.5) * 4294967295)}


5

我重新修改了@Apanatshka上面的第三个示例,试图得到更简单的代码,使其更像标准架构,至少在Mike Clark的培训视频中看起来是这样的,并且可以在Elm 0.16下运行。以下是我提出的重构版本:

module PortBasedRandom where

import Mouse
import Signal exposing (Signal, map)
import Random exposing (Seed)
import Graphics.Element exposing (Element, show)

port primer : Float


firstSeed : Seed
firstSeed =
  Random.initialSeed <| round primer


type alias Model =
  { nextSeed : Seed
  , currentInt : Int
  }


initialModel : Model
initialModel =
  { nextSeed = firstSeed
  , currentInt = 0
  }


randomInt : Model -> Model
randomInt model =
  let
      (i, s) = Random.generate (Random.int 1 10) model.nextSeed
  in
      { model | nextSeed = s, currentInt = i }


update : (Int, Int) -> Model -> Model
update (_, _) model =
  randomInt model


main : Signal Element
main =
  Signal.foldp update initialModel Mouse.position
    |> map (\m -> show m.currentInt)

这需要在HTML文件中进行特殊处理,因此这里有一个包含两个示例的文件:

<html>
  <head>
    <title></title>
    <script src="port_based_random.js"></script>
  </head>
  <body>
    <p>Move your mouse to generate new random numbers between 1 and 10 inclusive.</p>
    <script>Elm.fullscreen(Elm.PortBasedRandom, {primer: Date.now()})</script>
    <script>Elm.fullscreen(Elm.PortBasedRandom, {primer: Math.floor((Math.random() - 0.5) * 4294967295)})</script>
  </body>
</html>

1
如果您正在使用StartApp,则需要使用自定义HTML文件。
<script type="text/javascript">
    var yourPgm = Elm.fullscreen(Elm.Main, {startTime: Date.now()});
</script>

然后使用startTime作为种子:
startTimeSeed : Seed
startTimeSeed = Random.initialSeed <| round startTime

app =
  StartApp.start
    { init = (init startTimeSeed, Effects.none)
    , update = update
    , view = view
    , inputs = []
    }

然后在代码中,你需要做类似这样的事情:

init : Seed -> List Int
init seed = fst <| Random.generate intList seed

例如,

在哪里:

intList : Random.Generator (List Int)
intList =
    Random.list 5 (Random.int 0 100)

1

对于像我一样从Google过来的人,这里有一个更新:现在推荐使用flags而不是ports。其他答案中的代码现在甚至无法编译。

https://guide.elm-lang.org/interop/javascript.html

HTML(超文本标记语言)
<script>
  var app = Elm.Main.fullscreen({myRandomValue: Date.now()});
</script>

Elm
type alias Model = {
  mySeed : String
}

type alias Flags = {
  myRandomValue : String
}

init : Flags -> ( Model, Cmd Msg )
init flags =
  {
    mySeed = flags.myRandomValue
  }

...

main : Program Flags Model Msg
main = programWithFlags
  {
    view = view,
    init = init,
    update = update
  }

仍然是非常低质量的回答。你应该直接在代码中添加一些示例。 - timiTao
@timiTao 感谢您的反馈 - 我在我的答案中添加了一些示例代码。 - Tyler Yasaka

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接