#bottle
(name *) red bottle
(dict *) crimson decanter
(heads *) bottle decanter
#cap
(name *) red bottle cap
(heads *) cap
Now, if the player types EXAMINE BOTTLE, this is unambiguously interpreted as a request to examine the bottle, not the bottle cap, because one of the heads of #bottle was given.
This explanation in the quote is actually misleading. More correct phrasing would be:
Now, if the player types EXAMINE BOTTLE, this is unambiguously interpreted as a request to examine the bottle, not the bottle cap, because one none of the heads of #cap was not given but one of #bottle’s was.
In fact we don’t even need the heads rule for the bottle object for this disambiguation convenience to work (all dictionary words of an object are effectively heads if we don’t declare head nouns for that object explicitly).
When we give an object explicit head nouns, it doesn’t mean “if the player types one these words then they mean this object probably.” It means rather “if the player hasn’t typed one of these words then they probably don’t mean this object.”
Therefore if we want examine bottle to prefer the #bottle over the #cap, then the minimum we have to do is to give the #cap object its head noun cap.
Edit: The explanation sentence was changed into proper English with correct meaning.
That phrasing is the reason why it is that on the wiki the Dialog example of disambiguating code contains no use of the (heads $) predicate, relying totally on (unlikely $) instead.
True, likelihood mechanism offers finer control over the auto-disambiguation process. Declaring heads can cause cascading weird disambiguation by the parser as a side effect, because the significance of the non-head words of an object drops.
Edit: One case where noun heads trump the likelihood declarations is when one objects nouns are a subset of another ones, e.g., if we have fish and fish hook together, how do we refer to the fish without also referring to the fish hook? With noun heads it is trivial:
#fish
(name *) fish
(item *)
#hook
(name *) fish hook
(item *)
(heads *) hook
The (heads $) predicate is quite useful, as the code you shared illustrates well. But like you, I did not find the language of the manual helpful. It was some time after I wrote the code on the wiki that I tried something new and finally understood how (heads $) worked.
I had some spare time, and wanted more surgical control over disambiguation. I would like to implement this scenario playing exactly like it is given in the transcript:
The office
It’s an office.
You see a fish here.
You see a red fish here.
You see a metal rod here.
You see a metal fish hook here.
> get metal
Did you want to take the metal rod or the metal fish hook?
> get fish
You take the fish.
> get fish
You take the red fish.
> get fish
You take the metal fish hook.
> get metal
You take the metal rod.
No matter how I juggled the noun heads statically, this transcript seemed to be impossible.
Behold, the abomination!
#player
(current player *)
(* is #in #office)
#office
(name *) the office
(room *)
(look *) It's an office.
#rod
(name *) metal rod
(item *)
#redfish
(name *) red fish
(item *)
(heads *) (with #fish) red
#fish
(name *) fish
(item *)
#hook
(name *) metal fish hook
(item *)
(heads *) (with [#fish #redfish]) hook
(#rod/#redfish/#fish/#hook is #in #office)
((item $) is handled)
%% Library override
(consider action candidates $List $Words) (just)
(if) ($List = [$Single]) (then)
%% Optimize the common case.
(try-complex $Single)
(else)
%% added to support candidate membership query %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
(exhaust) {
*($Action is one of $List)
*($Obj is one of $Action)
(object $Obj)
(now) ($Obj is in candidates)
}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Eliminate options where the head noun is missing.
(now) (head noun is required)
(collect $A)
*(understand $Words as $A)
($A is one of $List)
(into $ShortListWithDup)
(remove duplicates $ShortListWithDup $ShortList)
%% added to support candidate membership query %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
(now) ~($ is in candidates)
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
(if) ($ShortList = [$Single]) (then)
(try-complex $Single)
(else)
(if) (empty $ShortList) (then)
(disambiguate action $List $ComplexAction)
(else)
(disambiguate action $ShortList $ComplexAction)
(endif)
(try-complex $ComplexAction)
(endif)
(endif)
%% Succeeds if one of the members of the list is in the candidates,
%% not all of them need to be (or if the object is one of the candidates
%% for single object parameter). Utility rule to save typing in (heads $).
(with $O)
(if) (list $O) (then)
*($Obj is one of $O)
($Obj is in candidates)
(else)
($O is in candidates)
(endif)
Is it worth the trouble? Hell no! But it’s been good practice.
Edit: Just streamlining the code for less typing in the head noun rules, no functional change.