* * *

Author Topic: Cyclic reference  (Read 1661 times)

Eugene Loza

  • Hero Member
  • *****
  • Posts: 531
    • My "almost daily" development blog
Cyclic reference
« on: February 13, 2017, 01:22:24 am »
Hi all!
I'm back with some stupid questions as usually :)
I've come to a problem of recursive variable definition (or cyclic reference problem) while trying to make a dialogue generator.
In pseudocode it looks like:
Code: [Select]
Unit characters;
type TNPC = class(TObject)
  ...
  function CreateMyContext: TContext;
end;

Unit context;
type TContext = class(TObject)
about: TNPC
...
end;
type TDialogueContext = class(TObject)
  CurrentContext: TContext;
  Speaker,Listener: TNPC;
  ...
end;
So, TDialogueContext should use TNPC and TNPC should use TContext. Due to cyclic reference I can't use both definitions that way. So I see five solution all of which I don't like.
1. Make "generic" types and eventually typecast them.
Code: [Select]
Unit genericNPC
type TGenericNPC
Unit character
type TNPC = class(TGenericNPC)
Unit context
...speaker,listener: TGenericNPC
...
Contex.add((speaker as TNPC).createMyContext);
Lot's of unneeded typecasting, splitting functions into units that logically should be in one unit.
2. Or
Code: [Select]
Unit genericContext
type TContext
Unit character
uses TContext
type TNPC = class(TGenericNPC)
Unit context
...
Contex.add(speaker.createMyContext);
Looks better but also splits "context" unit which logically should be solid
3. Make unsafe typecasting.
Ugly. Unreadable.
4. Make helpers for each object that will "extend" its functionality. (Looks like the most optimal solution at hand)
Code: [Select]
unit characters
type TNPC...
unit context
type TContext
type TNPC = helper...
  function getMyContext: TContext
end;
...
5. Using integer IDs for NPCs. I.e.
Code: [Select]
Unit characters
type TNPC...
  id: integer
  ...
end;
type TNPC_list...
  function findbyId(id: integer): TNPC;
end;
unit context
type TContext...
npc_id: integer;
...
type TDialogueContext
  listener,speaker: integer;
...
But I don't like any of those approaches. They all look ugly and unnatural. Or maybe I'm missing something basic?

P.S. The problem concerns not only this case. When I'll go further and introduce "facts" they will be much more tightly merged to each other. I.e. there will be "fact about some NPC" and NPC will have "memory of facts".
Lazarus 1.9 + FPC 3.1.1 Debian Jessie 64 bit.

My Free and Open Source games in Lazarus/FreePascal/CastleGameEngine:
https://decoherence.itch.io/
(and some ancient games in Turbo Pascal too)
Sources are here: https://github.com/eugeneloza?tab=repositories

Thaddy

  • Hero Member
  • *****
  • Posts: 4807
Re: Cyclic reference
« Reply #1 on: February 13, 2017, 07:29:58 am »
The standard way to solve it with units is to put the unit(s) that cause the cyclic reference in the other units implementation section's uses clause.
So if  unit a references unit b and unit b references unit a then a should put b in the interface uses clause and unit b should put unit a in the implementation uses clause.
Or the other way around. If that does not solve the case, resolve it through a third unit or..... move code. Because in that case you have a design issue.

The standard way to solve it within the same unit is by forward declaration

Code: Pascal  [Select]
  1. program untitled;
  2. {$ifdef fpc}{$mode delphi}{$H+}{$endif}
  3. type
  4.   Tfoo = class;  // forward
  5.   TBar = class;  // forward
  6.  
  7.   TBar = class
  8.   private  
  9.     FFoo:TFoo;  // not fully defined here
  10.   end;
  11.  
  12.   TFoo = class // now define
  13.   private
  14.     FBar:TBar;
  15.   end;
  16. begin
  17. end.
« Last Edit: February 13, 2017, 07:42:58 am by Thaddy »
"Logically, no number of positive outcomes at the level of experimental testing can confirm a scientific theory, but a single counterexample is logically decisive."

Thaddy

  • Hero Member
  • *****
  • Posts: 4807
Re: Cyclic reference
« Reply #2 on: February 13, 2017, 07:48:01 am »
I see some issues with your code by the way:
You write:
Code: Pascal  [Select]
  1. unit characters
  2. type TNPC...
  3.  
  4. unit context
  5. type TContext     // type block
  6. type TNPC = helper... // NEW type block
  7.   function getMyContext: TContext
  8. end;

Opening a new type block for types that depend on each other is a bad idea and will depending on context sometimes not work.
Put them in the same type block if they depend on each other. Preferably the forward declarations too.
Type blocks work like a fence in some cases.
« Last Edit: February 13, 2017, 07:54:14 am by Thaddy »
"Logically, no number of positive outcomes at the level of experimental testing can confirm a scientific theory, but a single counterexample is logically decisive."

Eugene Loza

  • Hero Member
  • *****
  • Posts: 531
    • My "almost daily" development blog
Re: Cyclic reference
« Reply #3 on: February 13, 2017, 11:53:51 am »
Thanks a lot for your response, Thaddy!
Yes, that was just a pseudocode. And yes, those objects are "huge" and otherwise unrelated and can hardly even be located in one unit, not speaking of a single TYPE definition... And yes, I am aware of solving cyclic reference problem in "implementation", but I need those in Interface section...
Looking again at my pseudocode it's horribly hard to read :)
I've made a graphic representation of different variants of solution for better representability...
The most "native" solution looks like a "class helper"... But it still splits TNPC implementation into two units.
« Last Edit: February 13, 2017, 11:55:27 am by Eugene Loza »
Lazarus 1.9 + FPC 3.1.1 Debian Jessie 64 bit.

My Free and Open Source games in Lazarus/FreePascal/CastleGameEngine:
https://decoherence.itch.io/
(and some ancient games in Turbo Pascal too)
Sources are here: https://github.com/eugeneloza?tab=repositories

Thaddy

  • Hero Member
  • *****
  • Posts: 4807
Re: Cyclic reference
« Reply #4 on: February 13, 2017, 12:16:54 pm »
Both in the interface section means you HAVE to use a third unit to expose.
Or combine the units and use forward references.

There is to my knowledge no other way around this. (not even in C++ that doesn't even know about units)
It's a design problem. But it can be solved with the two options above.
« Last Edit: February 13, 2017, 12:20:12 pm by Thaddy »
"Logically, no number of positive outcomes at the level of experimental testing can confirm a scientific theory, but a single counterexample is logically decisive."

argb32

  • Jr. Member
  • **
  • Posts: 71
    • Pascal IDE based on IntelliJ platform
Re: Cyclic reference
« Reply #5 on: February 13, 2017, 12:21:18 pm »
This most probably is a design problem.
Both classes TNPC and TContext are aware of each other. If this is really necessary (the classes are tightly coupled) than put them to single unit and create descendants in separate units which contains unrelated parts.
I can see one usage of TContext in TNPC and it doesn't seem correct:
Code: [Select]
function CreateMyContext: TContext;The function can be put in TContext:
Code: [Select]
function CreateFromNPC(NPC: TNPC): TContext;

Eugene Loza

  • Hero Member
  • *****
  • Posts: 531
    • My "almost daily" development blog
Re: Cyclic reference
« Reply #6 on: February 13, 2017, 01:11:33 pm »
Yes... looks exactly like a design problem.
However, it's not as easy to make CreateFromNPC as it heavily uses NPC's variables and relates to TNPC structure (very tricky, includes "conclusions" which aren't available as direct variables, I believe it'll also use its private fields in future), returning (very simple) TContext.

So... NPC "thinks" and returns some "context" (e.g. list of gender, nationality, disposition towards listener and relation towards subject spoken of, time of the day, place, etc) which governs which dialogue options it'll present to the "world". It'll eventually become dynamic and will return TContext only relating to a specific "request" by TDialogueContext to optimize memory and CPU consumption.

On the other hand TDialogueContext is a "roof-top" or "extension" of the TContext which not only stores the dialogue context, but also governs its creation run-time. The total list of TContext will be made based on many world parameters including NPCs speaking to each other.

So:

It's NPC responsibility to provide its context as a list of TContext (according to an external request: e.g. "What do you think of %npc2?" will provide %npc1 disposition towards %npc2, what he (dis)likes about him, how well they are acquainted, some outstanding facts about %npc2 in memory, etc).

TDialogueContext is the one "requesting" NPCs to provide context for the current dialogue topic.

Quote
Example:
- Hi, Andry! Haven't seen you for ages! (context: %npc1 and %npc2 are friends, %npc1 didn't meet %npc2 for some time, %npc2 request name, generic: greeting (suggest next slot: greeting-response)
- Hi, Jay! Nice to meet you! (context:  %npc2 and %npc1 are friends, accept suggested slot: greeting-response (don't break the dialogue by some "I hate you. Get lost from my sight!" with suggest next slot: end-dialogue), %npc2 is "glad" (or pretending to be glad), suggest next slot: random-chat).
- You know, I've saw Reann yesterday. (select random-chat dialogue slot: gossip. Use memory (%fact1) about %npc3. Suggest next dialogue slot (self): continue) She was wearing a really nice red shoes. (listener didn't interrupt the dialogue (was ok with next dialogue slot), disposition to %npc3 check, use memory (%fact2 related to %fact1), check values of %npc1 (he likes red color), select next slot: react-gossip)
- Red? I never heard her of liking red color! (accept react-gossip slot, check %fact2 with %npc2.memory(about %npc3) and it contradicts result = context:"surprise", suggest next slot react-surprise).
- I tell ya. (accept react-surprise slot, suggest next slot: random-chat or goodbye)

UPD: The overall idea is the following.

There is a huge pool of TPhrase with DEMAND and ALLOW TContext (i.e. context where the phrase might be used).
There is a TDialogueContext with DEMAND and ALLOW TContext based on current game situation (speaker, listener, topic, world parameters).
TDialogueContext scans through the pool of phrases and checks if:
all items in TDialogueContext.DEMAND must be found in TPhrase.DEMAND and TPhrase.ALLOW
all items in TPhrase.DEMAND must be found in TDialogueContext.DEMAND and TDialogueContext.ALLOW
A list of TPhrases that meet these two requirements is generated and a random TPhrase is chosen from the list.

Quote
Example:
TPhrase.text := 'Hi, (request listener:name, nominative)!'; //if speaker and listener are friends "request listener:name" script might return nickname instead
TPhrase.context.DEMAND.add(dialogueslot_generic_greeting); //this will do for greeting_hello and greeting_response dialogue slots
TPhrase.context.DEMAND.add(disposition_above_average); //the speaker must return disposition>0.5 towards listener
TPhrase.context.DEMAND.add(informal_chat,importance=0.9); //This phrase will have to suffer 90% penalty to be used in formal situation, but still possible
« Last Edit: February 13, 2017, 01:35:10 pm by Eugene Loza »
Lazarus 1.9 + FPC 3.1.1 Debian Jessie 64 bit.

My Free and Open Source games in Lazarus/FreePascal/CastleGameEngine:
https://decoherence.itch.io/
(and some ancient games in Turbo Pascal too)
Sources are here: https://github.com/eugeneloza?tab=repositories

 

Recent

Get Lazarus at SourceForge.net. Fast, secure and Free Open Source software downloads Open Hub project report for Lazarus