NAME

Chorus::Frame - Frame-based knowledge representation with inheritance and procedural attachments.

VERSION

2.01

SYNOPSIS

use Chorus::Frame;

my $f1 = Chorus::Frame->new(
    color => { _DEFAULT => 'blue' },
);

my $f2 = Chorus::Frame->new(
    _ISA  => $f1,
    label => sub { 'I am ' . $SELF->color },   # $SELF = frame calling get()
);

print $f2->color;   # 'blue'   (inherited _DEFAULT)
print $f2->label;   # 'I am blue'

# Inheritance mode affects how _VALUE/_DEFAULT/_NEEDED are resolved
Chorus::Frame::setMode(GET => 'Z');
Chorus::Frame::setMode(GET => 'N');   # back to default

# Select frames by slot (uses internal registry for fast lookup)
my @colored = fmatch(slot => 'color');
my @both    = fmatch(slot => ['color', 'score']);

# Select the best-matching prototype frame given observed properties
my $proto = fselect(color => 'blue', can_fly => 1);   # highest-scoring prototype
my @all   = fselect(color => 'blue', _all => 1);      # all candidates, ranked

# Frame networks: restrict fselect to a prototype and its declared alternatives
my $Bird  = Chorus::Frame->new(can_fly => 1, legs => 2, _ALTERNATIVES => [$Bat]);
my $match = fselect(can_fly => 1, _alternatives => $Bird);

# Terminal slots: declare which slots must hold real data for a frame to be complete
my $proto = Chorus::Frame->new(_TERMINAL_SLOTS => ['color', 'size']);
my $inst  = Chorus::Frame->new(_ISA => $proto, color => 'blue', size => 'large');
$inst->complete;   # 1

# _ON_DELETE hook (if-removed demon, completes the Minsky triad)
my $f = Chorus::Frame->new(
    tag       => 'active',
    _ON_DELETE => sub { print "Slot '${\$_[0]}' removed from frame\n" },
);
$f->delete('tag');   # → prints "Slot 'tag' removed from frame"

DESCRIPTION

A frame is a Perl hash blessed into Chorus::Frame. Its entries are called slots.

Key features:

  • Inheritance via the _ISA slot (single frame or arrayref of frames).

  • Procedural slots -- any slot value may be a sub {}, evaluated lazily on access. The variable $SELF holds the frame on which get() was originally called, making it available inside coderefs.

  • Target-information slots -- _VALUE, _DEFAULT and _NEEDED are tested in that order to resolve the value of a frame.

  • Lifecycle hooks -- _BEFORE, _AFTER and _REQUIRE intercept writes.

  • Global registry -- every frame is registered automatically; fmatch() uses this registry for O(1) slot-based lookups.

Special slots

The following names are reserved. Never use them as application slot names (except _ISA and _NOFRAME):

_KEY          Unique MD5 key assigned at construction.  Never set manually.
_PARENT_KEY   Tracks sub-frame ownership for Copy-on-Write inside set().
_ISA          Inheritance: a single frame or an arrayref of frames.
_VALUE        Primary target information of this frame.
_DEFAULT      Fallback when _VALUE is absent.
_NEEDED       Last-resort coderef called when both _VALUE and _DEFAULT are absent
              (backward chaining).
_BEFORE           Hook called before a slot value changes.
_AFTER            Hook called after a slot value changes (forward propagation).
_ON_DELETE        Hook called after a slot is deleted; receives the slot name
                  (if-removed demon, completing the Minsky triad).
_REQUIRE          Validation hook: return REQUIRE_FAILED to block the write.
_NOFRAME          Prevents automatic promotion of a nested hash to a frame.
_TERMINAL_SLOTS   Arrayref of slot names that must hold a real (_VALUE) value
                  for the frame to be considered complete (see complete()).
_ALTERNATIVES     Arrayref of sibling prototype frames used by fselect()
                  as an alternative candidate pool (Minsky frame network).

Inheritance modes N and Z

The global mode controls how get() walks the inheritance chain.

Mode N (default) -- each valuation key is scanned across the whole inheritance tree before the next key is tried:

_VALUE  on (frame, frame._ISA, frame._ISA._ISA, ...)
_DEFAULT on the same tree
_NEEDED  on the same tree

Mode Z -- the full sequence (_VALUE, _DEFAULT, _NEEDED) is tried on each frame before descending to its parents:

frame     : _VALUE, _DEFAULT, _NEEDED
frame._ISA: _VALUE, _DEFAULT, _NEEDED  ...

Switch with Chorus::Frame::setMode(GET => 'Z') or setMode(GET => 'N').

Exports

Chorus::Frame exports by default:

$SELF          Current frame context, updated by get() and set().
&fmatch        Slot-based frame selection function.
&fselect       Prototype selection by scored slot/value matching (Minsky-style).
&setMode       Switches the inheritance mode (N or Z).
REQUIRE_FAILED Constant (-1) returned by _REQUIRE to abort a write.

FUNCTIONS

setMode

Sets the global inheritance mode used by get() to resolve target information (_VALUE, _DEFAULT, _NEEDED).

Chorus::Frame::setMode(GET => 'N');   # Mode N -- default
Chorus::Frame::setMode(GET => 'Z');   # Mode Z
Chorus::Frame::setMode('N');          # short form

See "Inheritance modes N and Z" in the DESCRIPTION for a detailed comparison.

METHODS

_keys

Returns the slot names of the frame, excluding the internal keys _KEY and _PARENT_KEY.

my @slots = $frame->_keys;

Use this instead of keys %$frame when you need to iterate over application slots without exposing internal bookkeeping entries.

_push

Appends one or more elements to a slot, converting it to an arrayref when necessary.

$frame->_push('tags', 'red', 'big');

_inherits

Adds one or more parent frames to the inheritance chain outside the constructor. Each parent is added at most once; duplicates are silently ignored.

$frame->_inherits($parent1, $parent2);

Used internally by Chorus::Engine::new() to wire agent instances to the engine prototype.

new

Constructor. Converts a flat list of key/value pairs into a Chorus::Frame object.

my $f = Chorus::Frame->new(
    slot_a => 'value',
    slot_b => sub { 'computed: ' . $SELF->slot_a },   # procedural slot
    nested => {
        _ISA    => $proto,
        _NEEDED => sub { compute_default() },
    },
);

All nested plain hashes are automatically and recursively promoted to Chorus::Frame, unless they contain the _NOFRAME flag. Every frame receives a unique _KEY and is registered in the global repository so that fmatch() can find it.

Do not set _KEY or _PARENT_KEY manually.

_reset

Clears all global registries (%FMAP, %REPOSITORY, %INSTANCES), resets the $SELF context stack and restores the inheritance mode to N.

Chorus::Frame::_reset();

For testing only. Call between test cases to guarantee frame isolation.

get

Returns the value associated with a space-separated slot path.

$frame->get('slot')
$frame->get('slot_a slot_b')   # traverse slot_a, then resolve slot_b
$frame->slot_a                 # AUTOLOAD short form -- equivalent to get('slot_a')

The last step in the path is resolved through _VALUE, _DEFAULT and _NEEDED in that order (subject to the current "Inheritance modes N and Z").

get() pushes the current frame onto a stack and sets $SELF to it, so procedural slots can refer back to the calling frame:

my $f = Chorus::Frame->new(
    name  => 'Chorus',
    label => sub { 'Module: ' . $SELF->name },
);
print $f->label;   # "Module: Chorus"

The short form $f->slotname is equivalent to $f->get('slotname'). Arguments are forwarded when the resolved value is a coderef:

$f->slotname(@args);   # calls get('slotname'), then invokes the result with @args

delete

Removes a slot and unregisters it from the global repository.

$frame->delete('slot');
$frame->delete('slot_a slot_b');   # deletes slot_b inside slot_a

Always use this method instead of delete $frame-{slot}>. Direct hash deletion bypasses the registry, causing fmatch() to return stale results for that slot.

After the slot is removed, the _ON_DELETE hook is invoked (if present), receiving the deleted slot name as its argument. This is the if-removed demon that completes the Minsky triad (_NEEDED / _AFTER / _ON_DELETE).

set

Associates a value to a slot (or slot path) and updates the global registry.

$frame->set('slot', $value);
$frame->set('slot_a slot_b', $value);

Always use this method instead of $frame->{slot} = $value. Direct hash assignment bypasses _registerSlot(), so fmatch(slot => 'slot') will not return this frame -- the slot exists on the hash but is invisible to the inference engine.

Lifecycle hooks -- when the terminal slot has them, they fire in order:

1. _REQUIRE is called with the new value.
   Return REQUIRE_FAILED (-1) to abort the write.
2. _BEFORE is called with the new value.
3. The value is stored in _VALUE and registered.
4. _AFTER is called with the new value (forward chaining).

Copy-on-Write -- when traversing a sub-frame whose _PARENT_KEY differs from the current frame's _KEY, a shadow frame (_ISA => $shared) is created locally before writing. This prevents mutations from affecting shared structures.

my $f = Chorus::Frame->new(a => { b => { _VALUE => 'old' } });
$f->set('a b', 'new');
print $f->get('a b');   # "new"

fmatch

Returns all frames that provide the given slot(s), either directly or by inheritance, using the internal registry for efficient lookups.

my @frames = fmatch(slot => 'color');                      # all frames having 'color'
my @frames = fmatch(slot => ['color', 'score']);           # intersection
my @frames = fmatch(slot => 'color', from => \@list);      # restricted space
my @high   = grep { $_->score > 5 } fmatch(slot => 'score');

slot may be a scalar or an arrayref. Multiple slot names return only frames providing all of them. The optional from arrayref narrows the search to a known subset.

complete

Returns 1 if every slot listed in _TERMINAL_SLOTS (on this frame or inherited) holds a defined value via get(), undef otherwise.

my $proto = Chorus::Frame->new(_TERMINAL_SLOTS => ['color', 'size']);
my $inst  = Chorus::Frame->new(_ISA => $proto, color => 'blue', size => 'large');

$inst->complete;   # 1

my $partial = Chorus::Frame->new(_ISA => $proto, color => 'red');
$partial->complete;   # undef  (size not filled)

_TERMINAL_SLOTS is inherited: a child frame that does not redeclare it will use its parent's list. Each slot is resolved with get(), so procedural slots (sub {}) and _DEFAULT values count as filled.

Relationship to Minsky's model: terminal slots correspond to Minsky's terminal nodes -- positions in the frame that must be grounded in actual observed data for the frame to describe a real situation rather than a generic prototype.

fselect

Selects the best-matching frame(s) from the global registry given a set of observed slot/value pairs. This implements the frame-selection mechanism described by Minsky (1974): given a situation described by a set of properties, find the prototype that fits it best.

# Best single match (highest score)
my $proto = fselect(color => 'blue', can_fly => 1);

# All candidates with a positive score, ranked best-first
my @ranked = fselect(color => 'blue', can_fly => 1, _all => 1);

# Restrict the search space
my $proto = fselect(color => 'blue', _from => \@candidates);

# Frame network: search seed + its declared _ALTERNATIVES
my $Bird = Chorus::Frame->new(can_fly => 1, _ALTERNATIVES => [$Bat, $Insect]);
my $best = fselect(can_fly => 1, legs => 6, _alternatives => $Bird);

# Accept candidates with zero matching slots (score >= 0)
my @all = fselect(color => 'blue', _all => 1, _min => 0);

Scoring -- for each candidate frame, one point is awarded for each slot => value pair where the frame provides the slot and its resolved value (via get()) matches the observed value. Frames with a score strictly below _min (default: 1) are excluded.

Options (prefixed with _ to avoid collision with slot names):

_all          If true, return all matching frames ranked by descending score
              instead of just the best one.  In list context, the return value
              is a list of frames.  In scalar context, it is an arrayref.

_from         Arrayref -- restrict the candidate pool to this list of frames.
              If absent, all registered frames are considered.

_alternatives Seed frame -- restrict the candidate pool to the seed frame plus
              all frames listed in its C<_ALTERNATIVES> slot.  This implements
              Minsky's frame network: when a prototype does not fit, try its
              declared siblings.  Cannot be combined with C<_from>.

_min          Minimum score to be included in the result (default: 1).
              Pass C<_min =E<gt> 0> to include frames that match none of the
              observed slots.

Returns undef (scalar) or () (list) when no candidate meets the minimum score.

AUTHOR

Christophe Ivorra

BUGS

Please report bugs via the CPAN request tracker: http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Chorus-Frame

SUPPORT

perldoc Chorus::Frame

SEE ALSO

Chorus::Engine, Chorus::Expert, Chorus::Collection::List, Chorus::Collection::Filter

LICENSE AND COPYRIGHT

Copyright 2013 Christophe Ivorra.

This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.