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
Choose a base branch
from
Open

Add loop #22

wants to merge 3 commits into from

Conversation

zwilias
Copy link
Member

@zwilias zwilias 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
Copy link

JoelQ 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
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
Copy link
Member Author

zwilias 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
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants