Search code examples
haskellfrpelerea

In Elerea, why doesn't stateful return the state after the last state transformation?


Let's take a look at an example given in the hackage documentation for stateful

do
  smp <- start (stateful "" (:))
  res <- forM "olleh~" smp
  print res

the output is:

["","o","lo","llo","ello","hello"]

What it does is push one character after the next into the signal network, first 'o', then 'l', 'l' again, and so on. The state transformation function given to stateful ((:), aka cons) takes an element and a list of elements and returns the list with the single element prepended. (In Haskell a String is a list of Chars)

What happens is this

(:) 'o' "" -- gives "o", then
(:) 'l' "o" -- gives "lo", etc

This is all quite straightforward, but did you notice there is a '~' at the end of the input string? So why does it not give

["","o","lo","llo","ello","hello", "~hello"]
                                    ^^^^^^

?

Probably because it gives the initial value first, then the result of the first invocation of (:) second, etc. So if we push in six characters (including '~') we get out "" after five characters have been prepended. The string "~hello" is actually in there, but on sampling the signal we get the old state (right before it is discarded).

I expected the following two (contrived) examples to produce the same output, but they do not:

-- 1
do
  smp <- start $ do
    n <- input
    return (n*2)
  res <- forM [1,2,3] smp
  print res -- prints [2,4,6]

-- 2
do
  smp <- start $ stateful 0 (\n _  -> n*2) -- ignore state
  res <- forM [1,2,3] smp
  print res -- prints [0,2,4]

-- edit: 3
-- to make the trio complete, you can use transfer to do the same
-- thing as 1 above
-- there is of course no reason to actually do it this way
do
  smp <- start $ transfer undefined (\n _ _ -> n*2) (pure undefined)
  res <- forM [1,2,3] smp
  print res

So my questions are:

  • Why does it do that? Is it important enough we get the initial value that we just have to accept this delay?
  • Could an alternative function return the next value as soon as it is available? (In other words, could this alternative function return "hello" right after the 'h' was sent in, so the '~' could be removed?) The resulting signal would then never produce the initial value.

edit: After Alexander pointed me to transfer I came up with this:

do
  smp <- start (stateful' "" (:))
  res <- forM "olleh" smp -- no '~'!
  print res -- prints ["o","lo","llo","ello","hello"]

stateful' :: a -> (p -> a -> a) -> SignalGen p (Signal a)
stateful' s f = transfer s (\p _ a -> f p a) (pure undefined)

stateful' seems to be very roughly 25% slower than stateful on my machine.


Solution

  • Probably because it gives the initial value first, then the result of the first invocation of (:) second, etc. So if we push in six characters (including '~') we get out "" after five characters have been prepended. The string "~hello" is actually in there, but on sampling the signal we get the old state (right before it is discarded).

    That's exactly how stateful works. Quoting the documentation:

    The initial state is the first output, and every following output is calculated from the previous one and the value of the global parameter

    That answers the first of your questions.

    As for the second, yes, an alternative function could return the new state immediately. In fact, there's such a function already, called transfer:

    The current input affects the current output, i.e. the initial state given in the first argument is considered to appear before the first output, and can never be observed.

    (It also carries state, which you in your examples should just ignore.)

    With this function, the "hello" example can be rewritten like this:

    do
      smp <- start (transfer
                     ""
                     (\character _ state -> character:state)
                     (pure undefined))
      res <- forM "olleh" smp
      print res