詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

カレンダーでタスクを管理することとその実装

昔からタスクを次々こなしていくことはすごく苦手で、Todoist とか Remember The Milk とか Google タスクとか Hiveminder とか! を使ってみることはあってもタスクがどんどん溜まっていく一方で、一向に捌ける様子はなく、そういう状態を続けていると TODO リストは腐っていてしまって、開くことすら億劫になってしまう。そうやっていくつものタスク管理ツールを荒廃したまま捨てていった結果、久しぶりに Remember The Milk を開いてみると学生のときのタスクがまだ残っていてウッとなったりするのもよくあることです。

そういうタスク管理ツールの何がよくないのかというと、ツールはタスクの期限を管理してくれるものは多いけれど、どのタスクをいつやるべきかということに関しては管理できないというか指示してくれない、というのが自分の性格においては問題なのらしい。自分は意志の力がないので、この並んだタスクの中から何をどんな順番でやるべきかなんてことをいちいち考えるのは嫌なのです。そうした結果、重要ではないが手っ取り早く面白そうなタスクばかりが先に消化されて、大きくて重たい問題というのはどんどん先送りにされていくアンチパターンがある。

カレンダーでタスクを管理する

一方で日々のタスクのうち達成できていることもあって、それはどういう類のものかというと、これはもう単純にミーティング。いつ始まっていつ終わるかの時間も場所も決まっているし、他人を巻き込んでいる強制力もある。どんなに意志力がないときでもカレンダーの通知があればそれにしたがって決まった場所に行くだけである。意志力はすぐに枯渇する。

自分のこの性質を利用して編み出したのは、ストレートに、カレンダーを使ってタスクを管理すること。Google カレンダーに30分単位でやることを予定しておき、あとはカレンダー通知に従って決まっていたことをやる。終わったら予定に ✓ をつけとく。それだけ。

f:id:motemen:20190620191620p:plain

青がメインのカレンダー。紫が自分専用タスクカレンダー。こんな感じになるわけです。

Slack と Fitbit の通知でケツを叩かれるイメージ。

f:id:motemen:20190620191447p:plain

運用

すばらしいアイデアだが、これをうまくやっていくにはいくつかコツが必要だった。

ひとつはメインのカレンダーを使わないこと。メインのカレンダーを使ってしまうと自分の予定を完全にブロックすることになるので、他人から見るとモテメンさん忙しそうですね……となってしまう。マネージャとしては自分の時間はできるだけ外に解放しておきたいので、他の人に見えない自分用のカレンダーにタスクを積んでいた。積んだところに新しく予定が入ったらタスクのほうをずらす。もちろん延期することができないタスクであればメインのカレンダー入れて明に自分の時間を確保する。

もうひとつは、時間の余裕を持っておくこと。調子よく30分単位で仕事を詰めていくとたしかに進んでいくんだけど、結果めちゃくちゃ疲れる。これは30分くらいで終わりそうだなーってタスクは60分で確保しておいて、余った時間は自分の自由時間にしちゃう。この自由時間ができることで、やりたくないタスクを棚上げにして、面白そうなことに時間を使ってしまうその衝動をなだめることもできる。

そして、タスクのゴールでなく細かなアクションを先に設定しておくこと。タスクを作ったときの脳内コンテキストは失われているので、〇〇を決める、じゃなくてこの URL を見て〇〇をする、などと予定の詳細に書いておく。そうすると考えるより先に行動から始められるので、初速を稼げる。そしてこの初速こそが重要である。

実装

ベストプラクティスやノウハウはソフトウェアにするというのは原理原則なので今回もそのように行った。もちろんプラットホームは Google Apps Script である。

f:id:motemen:20190620191529p:plain:h400

ウェブアプリとして実装していて、以下の簡単な機能をもっている。

  • カレンダーに追加したタスクの一覧
  • タスクの追加
    • メインのカレンダーの予定を見つつ、空いている時間にタスクの予定を追加する。
  • タスクの ✓
  • タスクのリスケジュール
    • タスクの予定を未来に設定しなおす。
    • あとからメインのカレンダーに予定が入ったときとか、タスクを消化しきれなかったときとか。

https://github.com/motemen/gas-TaskCal

デプロイ方法はREADMEにシュッと書いているとおり。

見どころはサーバサイドの関数定義をフロントエンドでも参照してるところと、カレンダー詳細へのパーマリンクの生成(https://stackoverflow.com/a/33302612)かな~。

考察

とは言うものの、いかに仕組みを整えてもそこに血が通わなければ価値がないのは世の常で、つまり元気がないと何もできない。それはもはや前提というほかなくて、それはとにかく睡眠を取ることでだけ実現できる。睡眠重要……。

参考文献として挙げるなら、以下。

ヒトはなぜ先延ばしをしてしまうのか

ヒトはなぜ先延ばしをしてしまうのか

  • 作者: ピアーズ・スティール,池村千秋
  • 出版社/メーカー: CCCメディアハウス
  • 発売日: 2012/06/28
  • メディア: 単行本(ソフトカバー)
  • 購入: 2人 クリック: 4回
  • この商品を含むブログ (8件) を見る

自分の課題に対しては半分くらいしか参考にできなかった本だけど、ここで紹介した方法はこの本の言葉で言うとこうである。

  • プレコミットメント
    • 先に自分を縛っておく方法。時間を取って、やることを決めておくのはまさにこれ。
  • 非スケジュール
    • あらかじめ余暇の時間を取っておく方法。この時間があることで、今の仕事に集中できる。ちょっと違うけど、余った時間を好きに使うことを許すのは、これに近いかな(先のタスクを余った時間にこなして、丸々自由時間にしたりもする)。

Go の関数を context 対応するツール

最初から完璧な設計と実装ができているなら苦労はないわけだけど、実際にはそうもいかない。具体的にはある程度の規模になってくると「あーこの関数 context.Context 対応したい!」みたいな気持ちが湧いてくるわけです。context 対応ってのは、第一引数に ctx context.Context を追加することですね。

そういうことをやるツールを書きました。

GitHub - motemen/go-ctxize: Rewrite functions to have "Context"s

go get github.com/motemen/go-ctxize/cmd/goctxize で、goctxize というバイナリが手に入ります。

サンプル

README やテストにある例だけど、

// $GOPATH/src/example.com/foo/foo.go
package foo

func F() {
}

// $GOPATH/src/example.com/foo/foo_test.go
package foo

import "testing"

func TestF(t *testing.T) {
    F()
}

// $GOPATH/src/example.com/bar/bar.go
package bar

import (
    "example.com/foo"
)

func bar() {
    foo.F()
}

という感じのコードにおいて、foo.F() のシグネチャが foo.F(ctx context.Context) であってほしい! という場合。

goctxize example.com/foo.F

としてやると foo.go が以下のように書き換わります。

// $GOPATH/src/example.com/foo/foo.go
package foo

import "context"

func F(ctx context.Context) {
}

// $GOPATH/src/example.com/foo/foo_test.go
package foo

import (
    "context"
    "testing"
)

func TestF(t *testing.T) {
    ctx := context.TODO()

    F(ctx)
}

ほんとは example.com/foo.F を呼び出しているほかのパッケージも書き換えたいわけなので、第2引数以降に追加のパッケージを指定してやると:

goctxize example.com/foo.F example.com/bar

bar.go もこうなります。

// $GOPATH/src/example.com/bar/bar.go
package bar

import (
    "context"
    "example.com/foo"
)

func bar() {
    ctx := context.TODO()

    foo.F(ctx)
}

コンテキストの推測

呼び出し側のファイルを書き換える際、*gin.Context みたいに context.Context を実装している変数がすでに呼び出し側のスコープに存在しているときはその変数を使ってくれます。かしこいですね。スコープ周辺の判定はけっこう雑。

// before
package web

import (
    "example.com/foo"
    "github.com/gin-gonic/gin"
)

func web(c *gin.Context) {
    foo.F()
}

// after
package web

import (
    "example.com/foo"
    "github.com/gin-gonic/gin"
)

func web(c *gin.Context) {
    foo.F(c)
}

また、コンテキスト化される関数の中に context.TODO() で初期化されている変数があったら、その変数を消し去ることもしてくれます。

// before
package bar

import (
        "context"

        "example.com/foo"
)

func alreadyHasCtxInside() { // これを ctx 化する
        ctx := context.TODO()
        _ = ctx
}

// after
package bar

import (
        "context"

        "example.com/foo"
)

func alreadyHasCtxInside(ctx context.Context) {

        _ = ctx
}

パッケージの指定

フルパスでパッケージを指定するのは煩雑なので、相対パスによる指定もできます:

# PWD=$GOPATH/src/example.com
goctxize ./foo.F ./bar ./baz

とか。go list と同じ解釈をするので、./... もいけます。

これは x/tools/go/packages パッケージによって実現されていて、作ってる途中で浦島太郎的にこの存在を知ったんだけど(Go 1.11 からの登場)、これがすげー便利。これまで静的解析をおこなうツールを書くとき、複数のパッケージにまたがって型情報を読み込みたいときには x/tools/go/loader を使うのが定石だった(と思う)のだけど、これは ./... 的な引数をうまく解釈することができない不都合があって、やや物足りなかったのです。Go モジュールに対応していることもあり、今後はこれを使うのが標準となる予定みたいですね。

どうぞご利用ください。

めかぬか

ピタゴラスイッチの人気コーナー「めかぬか」をHTMLで作ってみた。

mekanuka

  • 書き順つき SVG として KanjiVG というプロジェクトがあった。これのおかげで実現できてる。
  • SVGのパスをアニメーションで描く方法はなんか有名なのがあるらしい: How SVG Line Animation Works | CSS-Tricks
  • CSS を JavaScript で生成するのは JSS でやった。@keyframe も動的に生成できて便利。

はてなで一緒に働きませんか?