オレオレデザインパターン：Glocal Variable

## 概要

Pythonのライブラリでたまに見る、「withの中でのみアクセスできるグローバル変数」という設計パターンに、オレオレの名前をつけてまとめてみた。

すでに名前がついてたらすいません。

## 例題

ある設定に基づき実験（関数`experiment`）を行うプログラムを書く。

実験は複数の関数（`first` `second`）に分割して記述されており、どちらも設定を参照してある実験操作を行う。

`config0`と`config1`に基づき二回実験を行いたい。

ストレートに書くとこう。

```python
# 本当はもっといろいろ設定がある
config0 = {
    "id": 0
}


config1 = {
    "id": 1
}


def first(conf):
    # なんかする
    print(f"{conf['id']}: first")


def second(conf):
    # なんかする
    print(f"{conf['id']}: second")


def experiment(conf):
    first(conf)
    second(conf)


def main():
    experiment(config0)
    experiment(config1)


main()
```

ただこの書き方だと、プログラムが複雑になったとき、設定をバケツリレーしていくのがやや面倒。なしですませられないか？

一つの解法はグローバル変数を使うことだが……。

```python
conf = config0


def first():
    print(f"{conf['id']}: first")


def second():
    print(f"{conf['id']}: second")


def experiment():
    first()
    second()


def main():
    global conf
    experiment()
    conf = config1
    experiment()


main()
```

あきらかにこれはやばやば。

* グローバル変数を使ったことで、変数に処理がどう依存するか追跡が難しくなる。
* 上に加え、グローバル変数を途中で変化させていることで、状態の変化過程が追跡しきれずバグになりがち
  * 例えば今回の場合、`main`を呼び出した後`conf`が`config1`になってることを忘れて、`config0`のつもりで再度`main`を実行したりするとやばい

## パターンの導入

バケツリレーを避け、グローバル変数の導入も避けたいということで、その中間的な書き方としてGlocal Variableパターンを紹介する。

```python:config.py
from contextlib import contextmanager


_config = None
_initialized = False


@contextmanager
def configure(data):
    global _config
    global _initialized
    before = _config
    before_initialized = _initialized
    _config = data
    _initialized = True
    try:
        yield
    finally:
        _config = before
        _initialized = before_initialized


def get_config():
    if _initialized:
        return _config
    else:
        # 本当はもうちょっと真面目に例外投げるべき
        raise RuntimeError
```
```python
from config import get_config, configure


def first():
    print(f"{get_config()['id']}: first")

          
def second():
    print(f"{get_config()['id']}: second")

          
def experiment():
    first()
    second()

          
def main():
    with configure(config0):
          experiment()
    with configure(config1):
          experiment()


main()
```

* バケツリレーは避けることができた
* グローバル変数に比べると安全
  * `configure`のコンテキストの中でしか変数が使えないので、自由度に制限がある
  * configを直接変化させる術がなく、withを通してしか設定できない
      * withの前後で必ずconfigが初期化・解放されるので、わけのわからない値が残っていてバグを起こす心配がない
  * ただし、「スコープ」（withの中）以外で`get_config`を呼び出しても、静的解析でエラーは拾えない

このように、

* withの前後でグローバル変数を設定・初期化し
* そのグローバル変数を読み出す関数を用意する

パターンを**Glocal Variable**と呼ぶことにする。

## どういう時に使うか？

* 多くの関数で共有したいデータが存在する
  * 面倒くさくないならバケツリレーを使えばいい
* そのデータを動的に決める・変える需要がある
  * なければグローバル変数にしたほうがシンプル

## 実例

Pythonのライブラリではいくつか使われている。

* 深層学習パッケージの[mxnet](https://mxnet.apache.org/)では、行列を計算するコンテキスト（CPU, GPU）をGlocal Variableで設定できる
* Webフレームワークの[flask](https://palletsprojects.com/p/flask/)では、リクエストパラメータをグローバル変数のように参照できるが、コード上で設定する場合はGlocal Variableパターンを使う

Racketだと、[`parametrize`](https://docs.racket-lang.org/guide/parameterize.html)というシンタクスが存在し、これが汎用のGlocal Variable機能を提供する。

## バリエーション

### デフォルト値

初期値をあらかじめ決めることもできる。先ほど言及したmxnetでは、CPUでの計算がデフォルト値になっている。

### 変更

セッターも用意すれば、with内でGlocal Variableを変更することもできる。

```python:param.py 
_param = None
_initialized = False


@contextmanager
def parametrize(data):
    global _param
    global _initialized
    before = _param
    before_initialized = _initialized
    _param = data
    _initialized = True
    try:
        yield
    finally:
        _param = before
        _initialized = before_initialized


def set_param(data):
    if _initialized:
        global _param
        _param = data
    else:
        raise RuntimeError
    
    
def get_param():
    if _initialized:
        return _param
    else:
        raise RuntimeError
```
```python
from param import parametrize, set_param, get_param

with parametrize(3):
    with parametrize(4):
        print(get_param())
        set_param(2)
        print(get_param())
    print(get_param())
get_param()

# 4
# 2
# 3
# RuntimeError
```

読み取りしかできない場合に比べると、状態を追う努力が必要な分危険度は高まる。

だが状態変化の影響はwith内に限定できるので、グローバル変数に比べれば安全。

パーサなどを書く場合は、「まだ読んでいない文章」をGlocal Variableにして、少しずつ先頭から消費していくような書き方をすると、バケツリレーなしで書けて便利かも。

## 注意

Glocal Variableの値は、getterが**記述**された場所ではなく、**実行**されたタイミングで決まることに注意しよう。

```python
def get_print_config():
    # この2ではなく
    with configure(2):
        def print_config():
            print(get_config())
        return print_config


print_config = get_print_config()
# この3が参照される
with configure(3):
    print_config()
# 3
```

## 備考

元々はPythonでも状態モナドのdo記法みたいな感じで書けないかなーと思って、flaskやmxnetのことを思い出し、こういうパターンあるなと気づいたのだった。