0

I'm currently working on a module that allows users to build an arbitrary task network model (for use in a discrete event simulation) by creating instances of task objects (my module provides a task class). Among other things, a task contains logic describing the effects of its completion, such as the initiation of a different task. In this way, an instance of the task class may refer to one or more other instances, with the possibility of cyclical references/mutual recursion.

Here is an extremely simplified version of my code:

TaskModule.py

class Task(object):
    def __init__(self, name, effect):
        self.name = name
        self.effect = effect

def execute(task):
    task.effect()

TaskTest.py

task1 = task("Do the first thing", execute(task2))
task2 = task("Do the second thing", execute(task3))
task3 = task("Do the third thing", execute(task1))

The problem with this implementation is that I refer to task2 and task3 before they have been defined. That wouldn't be the end of the world if I could rule out cyclical references- it would just be a question of rearranging the order in which the objects are instantiated- but I believe I should accommodate that possibility. I have considered a couple potential workarounds- most would involve requiring the user to reference tasks indirectly (i.e. by some unique identifier value)- but I wonder if there is a more elegant solution to this that involves a clever form of abstraction.

Ensuring that the process of instantiating tasks/generating the task network (as seen in TaskTest.py) is as simple and easy as possible is a top priority for this project, since that is what the users of my module will be spending most of their time doing.

I tried searching, but it seems like most questions on the topic of mutual recursion/cyclical references concern functions or classes rather than instances.

mcman
  • 35
  • 1
  • 5
  • Can you make your question more explicit? – Jon Kiparsky Jun 02 '17 at 18:37
  • Basically, how can I allow a user to create instances of tasks with cross-dependencies in the cleanest way possible? If possible, with a single line of code per task, as seen in TaskTest.py. – mcman Jun 02 '17 at 18:44

3 Answers3

2

So, I think the issue here is that names and objects are being conflated. I would maybe use a structure where the task objects are organized in a dictionary and a string or an enumeration are used as keys. This way you can refer to names before they are assigned.

class TaskManager:
    def __init__(self, tasks=None):
        self.tasks = tasks or {}

    def register(self, name, task):
        self.tasks[name] = task

    def execute(self, name):
        self.tasks[name].effect()
Jared Goguen
  • 8,772
  • 2
  • 18
  • 36
  • I was previously considering something along these lines, but the way you explained it finally made it all click. Thanks. – mcman Jun 02 '17 at 19:04
1

You need some kind of placeholder object that can represent a task whose dependencies are not yet known. Then you can do

task1 = Task("Do the first thing", [Placeholder()])
task2 = Task("Do the second thing", [execute(task1)])
task3 = Task("Do the third thing", [execute(task2)])
task1.add_dependency(task3)
task1.remove_placeholders()

It needs to be Task("...", [Placeholder()]) rather than Task("...", []) because the latter represents a task with no dependencies, which you will also want to be able to express.

zwol
  • 135,547
  • 38
  • 252
  • 361
  • Hmmm... yeah, I see how that works. However, my goal (in the interest of user experience) has been to keep the process of instantiating a task to a single line of code. Is there any way to do that? – mcman Jun 02 '17 at 18:40
  • @mcman I believe that that is fundamentally impossible. Each task constructor can only refer to tasks that have already been given names, so to construct a cycle there _must_ be a way of naming a task whose dependencies are not yet known, and then inserting its dependencies later. – zwol Jun 02 '17 at 18:45
1

One way to accomplish this would be to create a TaskList object to manage the tasks. The implementation would be up to you, but essentially you'd want to have a list of Tasks, which are executed in sequence. If some task produces further work (ie, a non-None return) then this resulting Task would be appended to the list.

Obviously, this could easily produce an infinite process, so you might want to consider how you'd like to handle this, but this gets more into UI design.

Jon Kiparsky
  • 7,499
  • 2
  • 23
  • 38