[clean-list] Re: Strict lists kill me...

John van Groningen johnvg@cs.kun.nl
Thu, 4 Dec 2003 17:07:20 +0100


Jerzy Karczmarczuk wrote:
>...
>I have one case which I don't *really* understand, although
>I understand it superficially, and there is nothing wrong with
>it. My bombed examples -- the sampled sinusoid and the other
>lazy stream, which implements co-recursively the Karplus-Strong
>algorithm -- are *sound* samples. Converted to *{#Char} and
>played as .wav buffers.
>
>You may imagine that in the case of sound the synchro- asynchro-
>issues are quite important. But sometimes the graph-reduction
>has its own ideas about the order of events. Look at the
>[fragment of the] following program:
>
>// ========================
>Start :: *World -> (*World,*File)
>Start wrld
>  # (console,wrld) = stdio wrld
>  # console = fwrites "beginning of first fragment\n" console
>  # (a,wrld)  = playWav "file1.wav" wrld
>  # console = fwrites "end of first fragment\n" console
>  # (b,wrld) = playWav "file2.wav" wrld
>  # console = fwrites "end of second fragment\n" console
>  = (wrld,console)
>// ========================
>
>Its execution begins by writing:
>(
>
>Then *both* files are played,
>and finally the console displays at once:
>
> 65536,beginning of the first fragment
>end of first fragment
>end of second fragment
>(File -1 4254404)
>
>//////////////////////////////////////////////////////////////////////
>
>Hmmm. I thought then that forcing the strictness of the sharp-let will
>serialize the actions appropriately. It does serialize them, but in
>a way I *didn't*  expect.
>
>Replace all # by #!
>
>The result is: first, the console writes the text:
>
>beginning of the first fragment
>end of first fragment
>end of second fragment
>
>... and then plays both files. And finishes by writing (65536,File -1 4250308)

#! does not serialize the evaluation of the expressions, but forces the
evaluation of those expressions before or during the evaluation of the
root expression, or if #! is used before a guard, before or during the
evaluation of the guard. The compiler does not use the order of the
definitions between '#!' and '=' or '|' to determine the order of evaluation.
Only the expression at the right of the '=' is evaluated, not the variables in
the pattern before the '='.

However, if the result of an expression in a #! is used by another expression
in a #!, the first expression will always be evaluated before the latter, even
if the arguments of the second expression that use the result of the first
expression are not strict.

If the result of an expression in a #! is never used, the compiler will
remove this expression and give a warning.

The compiler will not move expressions inside an alternative of a guard,
even if the result is used only if the guard succeeds (or fails). In that
case the expression in the #! will be evaluated before or during the evaluation
of the guard.

>I imagine that demanding the interaction and forcing the reading of something
>will - perhaps - change this order, but without that, what would be your
>recommended way to serialize (monadify??) the actions above in an intuitive
>(obvious) order?

If you want to evaluate a function f before a function g, make sure that
one of the argument of f uses the result of g, and make one of those
arguments strict. If there is no such argument, you can add a dummy
argument.

In this case make 'playWav' strict in the second argument (wrld),
add a 'fwrites' function with an extra strict wrld argument, and
because wrld is unique, return the wrld as well. Finally make
the result of this function strict in the tuple element that
contains the file, to force the evaluation of 'fwrites' when
this function is called. Otherwise it would only create a closure
for fwrites, because tuples are lazy.

So:

fwrites2 :: {#Char} *File !*World -> (!*File,*World)
fwrites2 s file console = (fwrites s file,console);

Start wrld
  # (console,wrld) = stdio wrld
  # (console,wrld) = fwrites2 "beginning of first fragment\n" console wrld
  # (a,wrld)  = playWav "file1.wav" wrld
  # (console,wrld) = fwrites2 "end of first fragment\n" console wrld
  # (b,wrld) = playWav "file2.wav" wrld
  # (console,wrld) = fwrites2 "end of second fragment\n" console wrld
  = (wrld,console)

Another possibility is to add a function that synchronises both actions,
with strictness annotations for all arguments and results.

In this case, make 'playWav' strict in the second argument (wrld) again,
and add a 'sync_console' function with strict arguments and results:

sync_console :: !*File !*World -> (!*File,!*World)
sync_console console world = (console,world)

Start wrld
  # (console,wrld) = stdio wrld
  # console = fwrites "beginning of first fragment\n" console
  # (console,wrld) = sync_console console wrld
  # (a,wrld)  = playWav "file1.wav" wrld
  # (console,wrld) = sync_console console wrld
  # console = fwrites "end of first fragment\n" console
  # (console,wrld) = sync_console console wrld
  # (b,wrld) = playWav "file2.wav" wrld
  # (console,wrld) = sync_console console wrld
  # console = fwrites "end of second fragment\n" console
  # (console,wrld) = sync_console console wrld
  = (wrld,console)

Or use a lazy synchronise function, without strictness annotations
in the function types, and without making 'playWav' strict in the wrld
argument, but with a #! :

lazy_sync_console console world = (console,world)

Start wrld
  #! (console,wrld) = stdio wrld
     console = fwrites "beginning of first fragment\n" console
     (console,wrld) = lazy_sync_console console wrld
     (a,wrld)  = playWav "file1.wav" wrld
     (console,wrld) = lazy_sync_console console wrld
     console = fwrites "end of first fragment\n" console
     (console,wrld) = lazy_sync_console console wrld
     (b,wrld) = playWav "file2.wav" wrld
     (console,wrld) = lazy_sync_console console wrld
     console = fwrites "end of second fragment\n" console
     (console,wrld) = lazy_sync_console console wrld
  = (wrld,console)

Yet another way to force a specific order of evaluation is by using guards,
because the compiler does not change the order in which guards are
evaluated.

In this case we need some eval functions that evaluate an argument
using a strictness annotation and return False or True. For example:

eval_console :: !File -> Bool
eval_console console = False

eval_world :: !World -> Bool
eval_world world = False

Start wrld
  # (console,wrld) = stdio wrld
  # console = fwrites "beginning of first fragment\n" console
  | eval_console console = undef
  # (a,wrld)  = playWav "file1.wav" wrld
  | eval_world wrld = undef
  # console = fwrites "end of first fragment\n" console
  | eval_console console = undef
  # (b,wrld) = playWav "file2.wav" wrld
  | eval_world wrld = undef
  # console = fwrites "end of second fragment\n" console
  | eval_console console = undef
  = (wrld,console)

Or use the fact that expressions in #! are not moved inside guarded
alternatives:

Start wrld
  #! (console,wrld) = stdio wrld
  #! console = fwrites "beginning of first fragment\n" console
  | False = undef
  #!(a,wrld)  = playWav "file1.wav" wrld
  | False = undef
  #!console = fwrites "end of first fragment\n" console
  | False = undef
  #!(b,wrld) = playWav "file2.wav" wrld
  | False = undef
  #!console = fwrites "end of second fragment\n" console
  | False = undef
  = (wrld,console)

>***
>
>My superficial understanding is that the lines which use console are chained
>independently of instructions which handle wrld, so there is - a priori - no
>intrinsic, explicitly visible relative order among them.
>So I replaced everywhere
>
>console = XXX console
>
>by
>
>(console,wrld) = (XXX console,wrld)
>
>but it didn't help. Perhaps it has been simply optimized away. Perhaps the idea
>is too silly [which I suspect].

You probably did not make the first argument of the tuple strict to
evaluate 'XXX console'. Instead a closure is created that is evaluated later.

Regards,

John van Groningen