Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add `loop` #22

Open
wants to merge 3 commits into
base: master
from

Conversation

@zwilias
Copy link
Member

commented Aug 23, 2019

Works similarly to
https://package.elm-lang.org/packages/elm/parser/latest/Parser#loop and allows
indefinitely looping generators without blowing the stack.

This also makes it easier to implement combinators like chain and repeat as described in #20 without any risky business.

I'm not entirely happy with the example usage I came up with, especially because it's really just Random.list... I originally wrote these functions as part of a slack conversation where @ryannhg was running into issues with the stack being blown (try bumping up the depth on line 26). I think @JoelQ may also have some feedback on this.


Example implementations of chain and repeat:

chain : a -> List (a -> Generator a) -> Generator (List a)
chain seed generators =
    loop ( seed, [], generators ) chainHelp


chainHelp :
    ( a, List a, List (a -> Generator a) )
    -> Generator (Step ( a, List a, List (a -> Generator a) ) (List a))
chainHelp ( seed, acc, generators ) =
    case generators of
        [] ->
            constant (Done (List.reverse (seed :: acc)))

        generator :: rest ->
            map (\v -> Loop ( v, seed :: acc, rest )) (generator seed)
repeat : Int -> a -> (a -> Generator a) -> Generator (List a)
repeat depth seed toGenerator =
    loop ( depth, seed, [] ) (repeatHelp toGenerator)


repeatHelp :
    (a -> Generator a)
    -> ( Int, a, List a )
    -> Generator (Step ( Int, a, List a ) (List a))
repeatHelp toGenerator ( depth, seed, acc ) =
    if depth > 1 then
        map (\v -> Loop ( depth - 1, v, seed :: acc )) (toGenerator seed)

    else
        constant (Done (List.reverse (seed :: acc)))

(this is assuming I understand their intent based on the type signatures)
(repeat could also be chain seed (List.repeat depth toGenerator))

Works similarly to
https://package.elm-lang.org/packages/elm/parser/latest/Parser#loop and allows
indefinitely looping generators without blowing the stack.
@JoelQ

This comment has been minimized.

Copy link

commented Aug 23, 2019

I'm not entirely happy with the example usage I came up with, especially because it's really just Random.list

I think a big win here is that it would allow us to implement Random.Extra.sequence and Random.Extra.traversable in a stack-safe manner. Currently they both stack overflow with a large enough list.

Maybe showing a stack-safe sequence would be a more interesting example usage?

src/Random/Extra.elm Outdated Show resolved Hide resolved
zwilias added 2 commits Aug 23, 2019
This seems like a good example because it's a real-life, useful case. The
downside is that the original example uses `map2`, so the `andThen`/`recursion`
situation is not as clear...
@zwilias

This comment has been minimized.

Copy link
Member Author

commented Aug 23, 2019

I like using sequence as an example: it's a real life use-case of where a naive approach might end up blowing the stack for sufficiently large input.

There are some downsides, tho!

  • because the original example uses map2 with the recursion hidden behind foldr, it might be less clear how/why this is problematic
  • similarly, the "rewrite" procedure is a bit obscured.

Imagine if the original sequence were defined with explicit recursion like so:

sequence : List (Generator a) -> Generator (List a)
sequence = sequenceHelp []

sequenceHelp : List a -> List (Generator a) -> Generator (List a)
sequenceHelp acc generators =
    case generators of
        [] ->
            constant (List.reverse acc)

        generator :: rest ->
            andThen (\v -> sequenceHelp (v  :: acc) rest ) generator

Note: this is closer to what List.foldl (::) (constant []) >> map List.reverse boils down to

Then the "translation" to using loop is much clearer: It's wrapping the terminal case in Done and using map Loop for the recursive case, rather than having explicit recursion.

Interested in how to improve the docs here, and in @mgold's thoughts about (ab)using independentSeed like that...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants
You can’t perform that action at this time.