exprv4:how-to:use_classes_in_include_files

How To Use Classes In Include Files

Reusable include files should usually define classes and functions, not create a large set of root variables in the caller session.

A good pattern is to put reusable workflow code in classes, include the file with a namespace alias, and create only the objects that the caller actually needs. This keeps include files easier to reuse and also prevents accidental session pollution.


The Problem

This kind of include is convenient at first, but it pollutes the caller session:

// probe_setup.expr
safe_z = 5;
probe_feed = 100;
probe_count = 0;
 
function SetProbeFeed(feed)
{
    probe_feed = feed;
}
 
function ProbeDepth()
{
    probe_count += 1;
    return safe_z;
}

After this file is included, `safe_z`, `probe_feed`, `probe_count`, `SetProbeFeed`, and `ProbeDepth` are all root names in the active session.

That creates several risks:

  • another script can accidentally reuse the same names
  • old values can survive between evaluations
  • multiple probe jobs cannot easily keep independent state
  • clearing the session removes both the caller state and library state

Put State In A Class

A class keeps related state together. The session stores the class definition, and each object instance stores its own fields.

// probe_lib.expr
class ProbeJob()
{
    safe_z = 5;
    probe_feed = 100;
    probe_count = 0;
 
    function SetSafeZ(value)
    {
        safe_z = value;
    }
 
    function SetFeed(feed)
    {
        probe_feed = feed;
    }
 
    function ProbeDepth()
    {
        probe_count += 1;
        return safe_z;
    }
}

The caller creates an object and uses methods on that object:

include "probe_lib.expr" as Probe
 
job = Probe::ProbeJob();
job.SetFeed(120);
 
z = job.ProbeDepth();
print('safe z ', z);
print('probe count ', job.probe_count);

Only `job` and `z` are caller root variables. The probe settings are fields inside `job`.


Use Namespace Aliases For Includes

Include library files with an alias when they define reusable classes or functions.

include "probe_lib.expr" as Probe
include "atc_lib.expr" as ATC
 
probe_job = Probe::ProbeJob();
atc_job = ATC::ToolChangeJob();

The alias makes ownership clear and prevents library names from mixing with caller names.

Without an alias, names from the include are added directly to the current namespace. That is sometimes useful for small scripts, but it is not the best default for reusable libraries.


Keep Include Files Passive

Reusable include files should usually define classes and functions only.

Avoid this in library files:

// avoid in reusable library files
session.create('probe_job');
session.join('probe_job');
 
probe_job = ProbeJob();

A library that changes sessions changes the caller's execution context. That makes the include harder to reuse safely.

Prefer this:

// reusable library file
class ProbeJob()
{
    safe_z = 5;
    function GetSafeZ()
    {
        return safe_z;
    }
}

Then let the caller decide where the object belongs:

include "probe_lib.expr" as Probe
 
probe_job = Probe::ProbeJob();
print(probe_job.GetSafeZ());

Use One Root Object Instead Of Many Root Variables

When state must persist in a session, store one object instead of many separate root variables.

Better:

include "probe_lib.expr" as Probe
 
probe = Probe::ProbeJob();
probe.SetSafeZ(5);
probe.SetFeed(100);

Avoid spreading one workflow across many root variables:

probe_safe_z = 5;
probe_feed = 100;
probe_count = 0;
probe_last_result = none();
probe_last_error = none();

The object form is easier to inspect, pass to functions, replace, and remove.

probe = none();

Multiple Jobs

Classes make it natural to keep two jobs independent in the same session.

include "probe_lib.expr" as Probe
 
left = Probe::ProbeJob();
right = Probe::ProbeJob();
 
left.SetFeed(80);
right.SetFeed(120);
 
print(left.ProbeDepth());
print(right.ProbeDepth());

Each object has its own `probe_feed`, `probe_count`, and other fields.


Callbacks

Objects are also useful for callback state in dialogs, timers, serial handlers, and other host-integrated workflows.

class DialogState()
{
    accepted = false;
    value = none();
 
    function OnResult(result)
    {
        accepted = result.ok;
        value = result;
    }
}
 
state = DialogState();
dlg = msg_ok_cancel();
dlg.message('Continue?');
dlg.on_result(state.OnResult);
dlg.show();

The callback updates `state` instead of scattering callback variables across the session.


When Root Variables Are Still OK

Root variables are not bad. They are useful for:

  • short interactive work
  • one object that represents current workflow state
  • values intentionally shared across several commands
  • simple scripts where persistence is deliberate

The goal is not to avoid root variables completely. The goal is to avoid accidental session state.


For reusable scripts:

  • define classes in include files
  • include library files with namespace aliases
  • keep include files passive
  • store workflow state in object fields
  • keep only a small number of intentional root variables in the caller session
  • keep session switching and cleanup in the caller script

Checklist

Before writing a reusable include file, ask:

  • Does this file create root variables when included?
  • Does this file change the active session?
  • Can two jobs use this code at the same time?
  • Are names protected by a namespace alias?
  • Can caller state be stored in one object instead of many root variables?
  • Can callback state live in an object instance?

See Also

exprv4/how-to/use_classes_in_include_files.txt · Last modified: by 127.0.0.1

Page Tools