Python task manager from scratch, part 36: Preparing for recurring tasks
Here's a cluster of phenomena that I've never properly managed with productivity software:
- Some of my tasks recur.
- Completing one instance or part of a recurring task is independent of completing other parts of it.
- Sometimes, but only sometimes, scheduling these instances depends on when the first ones are completed.
- Sometimes, but only sometimes, these tasks should "pile up" if not completed.
Here are some examples:
- I need to take out the trash every week. It is impermissible to do this before 5p Wednesday. It must be done before 8a Thursday. Perhaps 5 weeks a year, the schedule varies and the trash day is moved back by a day. It is metaphysically incoherent to be committed to two different "take out the trash" tasks at once. (I mean, to taking out our trash twice in parallel; in theory I could be covering the neighbors' while they're on vacation, or something.)
- I take my vitamins every day. In some cases I should slightly adjust future doses if I miss a day, but for the most part it's like the trash: if I miss it, I miss it.
- The plants need to be watered periodically. The time of the next watering depends not on any predefined schedule but on the last time the watering was completed.
- I've occasionally set goals of reading X pages per day, where I do have to make it up on future days if I fall behind. (By the way, this worked fairly well, but it's not what I do now.)
- There are some screws that need to be tightened: this is like the plants, where the next occurrence depends on the last completion date.
- If I haven't talked to my mother in a while, I ought to call her.
And here are some reflections on those data:
- Some feel like sequences of tasks (e.g., the trash) whereas others feel like temporally extended tasks with parts (e.g., plant maintenance).
- Sometimes the completion of a task generates a new task and/or determines when a task ought to be done; sometimes not.
- Sometimes I ought to do something only when some unusual circumstance (e.g., not having talked to my mother in a while) obtains.
And here are some meta-level reflections on thinking through a software problem:
- However much the limitations of a system might constrain the implementation or manifestation of some object, do not let it constrain your understanding or description of the object. So, for example, Veery will probably never ingest a log of all my conversations with my mother, generating tasks automatically if it notices it's been a while (with "a while" depending on the season and other life features). It's still useful to say what the situation in my life actually is.
- Try to describe the phenomena faithfully but, as much as possible, unencumbered by a metaphysical framework (or an elaborate framework of software domain objects). The descriptions are there to help you make such a framework! Here, for example, it's tempting to somehow distinguish between occurrences, tasks, projects, and more right from the start. Eventually we'll need to do that, but when you're in this sort of capture/research stage, it's best to describe things neutrally where that's possible. Sometimes, the hard part is that the whole reason you're doing the project is that you think all the other software is using the wrong model for something and yours is better. If so, great--just make sure that your model is growing out of how things really are. Avoid the temptation to describe everything in terms of some limited technical vocabulary.
- There are two basic warnings one often hears: First, if you do too much planning up front, you're going to have to rewrite a lot of those plans as new requirements and lessons come up. Second, if you do too little planning up front, you're liable to go in the wrong direction ("weeks of coding can save hours of planning"). Striking a balance between these is context-dependent. As a one-person team happy to let Veery evolve more organically, I can afford to skip some of the planning I'd do if I were on a team building specific inventory-management software. But that doesn't mean I don't need to plan at all.
The reflections above make these (tentative) conclusions seem relevant and useful:
- Veery will not do everything. There is some "manage everything" software (DevonTHINK, JIRA, arguably Reminders as integrated in MacOS, arguably Slack for extreme power users). This is not that.
- I am not (yet) interested in attempting to convert anyone else in my life to Veery. This is software for a single person, for now at least.
- I use Apple hardware and am basically happy with Reminders for alerts and reminders. It's the nuts-and-bolts to-do-list stuff I want to be cleaner and snappier.
In light of all that, I'll take on a workstream of implementing recurring tasks, both those that are like the trash (where the tasks come on a schedule) and those that are more like the plants (where completing one task kicks off another).
I could implement both of these by modifying
Task as implemented: make some system for generating new tasks when old ones finish and cancelling ones that are too far past a deadline. So, the trash
Task could cancel itself at 2p on the day of trash collection (whether or not I did it), and this could kick off a process of scheduling the next week's trash task. (That task probably shouldn't show up on my list until 5p the day before trash collection, but let's deal with that separately.) Meanwhile, the plant-watering task could generate a new task, using different logic, (only) upon its successful completion. (That task should probably get more urgent over time after its due date, but let's deal with that separately.)
This scheme would work, for now, but I think it's dangerous--it's a sort of solution that is very common and also a disastrous mistake. What's wrong with this?
- It elides an important real-world difference between the trash and the plants tasks. Again, one seems to be a set of tasks that happen to be on a schedule whereas the other seems to be a bunch of tasks that are unified in being parts of some higher-level task (keeping a plant healthy).
- It jams a bunch of logic into the central domain object. Sometimes that's unavoidable, but having over-complicated domain objects is one of the main failure modes of real-world software. (When the domain and persistence layers are not cleanly separated, this gets even worse.) This mistake is quadratically bad: the extra methods and logic on the domain object interfere with each other proportionally to their number. It only gets worse from there: misplaced functionality is almost always more complicated.
So, what will I actually do?
- Notice that I already have the tools for trash-like and vitamin-taking-like tasks: any number of mechanisms can invoke the
add_task() method, including built-in scheduling infrastructure like
cron. An appropriate way to analyze these cases, for now, is: "I already have
Tasks, and these situations involve a lot of tasks that occur on a schedule. So the fundamentally missing thing here is the scheduling part, and the scheduling part only. Veery is not itself scheduling software; it's more appropriate for it to integrate with scheduling software. So I'm not going to build out any new domain objects here; the next step is to integrate existing infrastructure with
cron or similar."
- Notice that plant maintenance and similar are not like that. In this case there's a higher-level task or project or goal that coordinates the lower-level "water this plant today" sorts of tasks. Veery's job is to represent those higher-level items, whatever they are.
- I don't know yet exactly what those higher-level items are. But what do they need to do? They need to coordinate the scheduling of lower-level tasks, sometimes generating them not just because it's a certain time but because of facts about the task/project/goal.
A reasonable pragmatic approach again involves interfaces. My plan is:
- Define an interface: TaskCoordinator. Anything that's in the business of coordinating tasks can implement it.
- Define an object implementing the behavior we described for the plant and vitamin cases. That object will implement the TaskCoordinator interface (in Python, this is a matter of subclassing an abstract class).
- Figure out the appropriate relationship between a given water-this-plant-today
Task object and the keep-the-plants-watered coordinating object. This might be tricky.
My first steps to enact that plan are to define an Event object (with a subclass representing task-completion events), the coordinator interface, and an implementer of that interface. First drafts of those objects and their tests (and some formatting tweaks) now exist.
Here's the current commit in the