Note: In this question I am using Python syntax and Python terminology, but I am asking for the conceptual approach, independent of actual programming language, to make sure that answers would be useful for broader audience.
Level 0
Let's start with few functions:
def dog_tell(name, target, what):
print(f"{name} the dog tells {what} to {target}")
def dog_bark(name, target):
print(f"{name} the dog says 'Bark at you, {target}'")
def dog_wag(name, target):
print(f"{name} the dog wags tail at {target}")
def dog_woof(name):
print(f"{name} the dog woofs")
To use these functions, first argument is expected to be the dog name, so it would do something useful like this:
>>> dog_bark("Jake", "Finn")
Jake the dog says 'Bark at you, Finn'
Level 1
Now, if we are using these functions with the same dog a lot, it makes sense to group these functions together, and only provide the dog name once. There are at least two approaches to do that.
OOP
OOP approach is to declare the Dog class and provide it with name:
class Dog(object):
def __init__(self, name):
self.name = name
def tell(self, target, what):
print(f"{self.name} the dog tells {what} to {target}")
def bark(self, target):
print(f"{self.name} the dog says 'Bark at you, {target}'")
def wag(self, target):
print(f"{self.name} the dog wags tail at {target}")
def woof(self):
print(f"{self.name} the dog woofs")
>>> jake = Dog("Jake")
>>> jake.bark("Finn")
Jake the dog says 'Bark at you, Finn'
Functional
An alternative approach would be currying:
name = "Jake"
jake_tell = lambda *args: dog_tell(name, *args)
jake_bark = lambda *args: dog_bark(name, *args)
jake_wag = lambda *args: dog_wag(name, *args)
jake_woof = lambda *args: dog_woof(name, *args)
>>> jake_bark("Finn")
Jake the dog says 'Bark at you, Finn'
Both approaches serve the same function — they essentially attach some repetitive information to all function invocations.
Level 2
Now, we are getting to the point of my question. What if Jake talks to Finn very often, and I would like to make their conversation an entity of some sort? I can easily do that with functional approach:
target = "Finn"
jake_tell_finn = lambda *args: dog_tell(name, target, *args)
jake_bark_at_finn = lambda *args: dog_bark(name, target, *args)
jake_wag_at_finn = lambda *args: dog_wag(name, target, *args)
>> jake_bark_at_finn()
Jake the dog says 'Bark at you, Finn'
This made one more step as compared to the instantiation, and attached more information to function calls.
But what would be the correct OOP approach for this scenario? What if I continue this chain of adding partial state information to the class methods, effectively "instantiating the instances", "instantiating the instanciated instances" and so on?
To clarify the intent, I think of something like this:
>>> jake = Dog("Jake")
>>> jake_and_finn = jake.with_buddy("Finn")
>>> jake_and_finn.tell("dirty secrets")
Jake the dog tells dirty secrets to Finn
While I know how to implement this "as is", my concrete implementation lacks the scalability, abstractness and beauty. It hurts my feelings of beautiful, and I feel I lack the understanding of something important at the very basic theory behind this.
If I understand you correctly, I think you are talking about an abstract question about
graph
.jake
andfinn
are two nodes in the graph, and their communication channel is the edge in this graph. So if we construct every "dog" and their communication channel as a big graph, we can use two class to describe these two instances.One is
Dog
as node, and another isTell
as edge. We instantiate every dog as aDog
.Dog
has a function receives one parameter to specify his buddy, and returns an instance ofTell
as a directed edge(determined by your need, it can be undirected.)Finally, we use the instance of
Tell
to communicate.For example: