emdash / udlang

A practical, functional language for stream processing.
GNU Lesser General Public License v3.0
1 stars 0 forks source link

Should there be a way to specify side-effects that explicitly only appear on the first iteration of a script? #16

Open emdash opened 3 years ago

emdash commented 3 years ago

This is for when a script is intended for a streaming context, and we wants to do some one-time initialization.

Concretely, it's needed so that uDash scripts can re-use resources between frames, like images, patterns, fonts, etc. If we know we're reloading, we can instruct the renderer to clear its cache, otherwise, the resource IDs from the previous frame are still valid -- so we can re-use the hash we computed last time, but we can elide the side-effect that allocated it.

Consider:

let foreground = cairo.rgb(...);
let background = cairo.rgb(...);
set_source(background);
paint();
rect(...);
set_source(foreground);
fill(...);
// etc...

On the first iteration, creating the color objects should produce output that allocates resources. On subsequent iterations, they should not.

Initially

color 0xDEADBEEF ....
color 0xFEEDBEEF ....
begin
set_source 0xDEADBEEF
paint
set_source 0xFEEDBEEF
rect ...
fill

Thereafter

reset
set_source 0xDEADBEEF
paint
set_source_0xFEEDBEEF
rect ...
fill

Here's an idea for a friendly-looking construct.

The idea is that the side effects inside of initially execute only the first iteration, while the effects inside thereafter (which is optional) appear on subsequent iterations.

initially {
  reset();
  let foreground = cairo.rgb(...);
  let background = cairo.rgb(...);
} thereafter {
  present();
}

set_source(background);
paint();
rect(...);
set_source(foreground);
fill(...);

Question:

Really, it's about avoiding having to "close the loop" around uDLang in some very common cases. There are a few reasons for wanting to do this, but the main one is that it makes it hard to factor initialization logic into libraries, since they need would need knowledge of the input type (the libraries should know nothing about the input).

Maybe the bigger question is whether uDLang should be used to manipulate stateful resources downstream in the first place. I'm not sure, but if we can't, it makes it hard for uDashboard to work efficiently with cairo's existing API, and would probably cause problems for an OpenGL backend as well.

If we bite the bullet and "close the loop", we can generate resource IDs downstream and fold them into the next iteration of the state. This works, but it feels an onerous burden to force this pattern on the user:

  resources: {
    field foreground: cairo.Color;
    field background: cairo.Color;
  }?;
};

if (not in.resources) {
  reset();
  rgb("foreground", ...);
  rgb("background", ...);
  // We might forget to initialize a new resource, no way to catch this at compile time.
} else {
  // all further drawing has to take place in here, since only now can we use `in.resources.foreground`;
}

The most conservative approach is to restrict initially to the top-level of the script, allow it to appear only once, perhaps also require precede all other IO if it's used.

Regardless, if this construct is added, a flag should to the caller so that it can control which branch is run, for testing and debugging purposes in the one-shot case.

Here's an even simpler way to implement it, we just provide a variable binding which is set by the implementation:

if (sys.reset) {
  ...
} else {
  ...
}

This would be true in the one-shot case by default (but changable with a flag), and in the streaming case it is set to true on first iteration, and false afterwards. Care must be taken such that things like input and output directives can't appear inside either construct.

I can't decide which I prefer. The first construct would force all this one-time initialization into the user's own code. The second version would favor hiding such behavior in library functions. I can see the pros and cons of each. I can also see the sys record being a generally useful way for a script to be reactive to its environment. As long as it only changes between iterations, and not during, this shouldn't break the language.

emdash commented 3 years ago

another pattern that libraries could try:

// my_lib.ud
type Output: ....;
type StatefulType: UninitializedStatefulType | InitializedStatefulType;

type UninitializedStatefulType: {
   field state: "uninitialized";
   method initialize() -> Initialized {
      // tell downstream to allocate resources
   }
};

type InitializedStatefulType: {
   field state: "initialized";
   field my_resource: DownstreamResource;
   field my_resource: DownstreamResource;
   method frobulate() {
      self.my_resource....;
   };
};
// my_script.ud
import my_lib;
input { field data: [Int], my_lib_state: field needs_init: my_lib.StatefulType};

// This idiom be factored into a template
match in.needs_init.state {
   case "uninitialized" -> in.needs_init.init();
   case "initialized" -> in.needs_init.frobulate();
}