どうやらJavaScript界隈にある状態管理ライブラリを真似た物のようで、中々面白そうだったので練習のためにこれでナベアツを作ってみました。
設計
ナベアツは次の4つの値を用いて実装することにしました。
- 数 (ナベアツが読み上げる数)
- 数が3の倍数であるか否か表す真偽値
- 数が(10進表記で)3を含むか否かを表す真偽値
- 数を読み上げる際に阿呆になるべきか否かを表す真偽値
これらの値の依存関係は次のようになります。
実装
数
数のように他の値に依存していない物にはSignalを用いるようです。
import reaktiv
count = reaktiv.Signal(0)
for i in range(1, 5):
count.set(i)
print(count())
1
2
3
4
3の倍数?
次は数が3の倍数であるかどうかを表す真偽値です。この真偽値のように他の値に依存している物にはComputedを用いるようです。
@reaktiv.Computed
def divisible_by_3():
return count() % 3 == 0
for i in range(1, 7):
count.set(i)
print(i, divisible_by_3())
1 False
2 False
3 True
4 False
5 False
6 True
3を含む?
同様に10進表記で3を含んでいるかどうかを表す真偽値も用意しました。
@reaktiv.Computed
def contains_the_digit_3():
return "3" in str(count())
for i in range(1, 7):
count.set(i)
print(i, contains_the_digit_3())
1 False
2 False
3 True
4 False
5 False
6 False
阿呆?
@reaktiv.Computed
def aho():
return divisible_by_3() or contains_the_digit_3()
for i in range(10, 20):
count.set(i)
print(i, "😜" if aho() else "")
10
11
12 😜
13 😜
14
15 😜
16
17
18 😜
19
reaktivは何をしている?
今の所 reaktiv がやってる事は見えにくいと思います。というのも reaktiv を用いずとも以下のようにすれば同様の物が作れるからです。
count = 0
def divisible_by_3():
return count % 3 == 0
def contains_the_digit_3():
return "3" in str(count)
def aho():
return divisible_by_3() or contains_the_digit_3()
for i in range(10, 20):
count = i
print(i, "😜" if aho() else "")
しかしここで関数がいつ呼ばれるか分わかるようにprintを仕込んであげるとこのライブラリの真価が見えてきます。
count = reaktiv.Signal(0)
@reaktiv.Computed
def divisible_by_3():
+ print("呼 divisible_by_3")
return count() % 3 == 0
@reaktiv.Computed
def contains_the_digit_3():
+ print("呼 contains_the_digit_3")
return "3" in str(count())
@reaktiv.Computed
def aho():
+ print("呼 aho")
return divisible_by_3() or contains_the_digit_3()
Computedは怠惰
>>> count.set(1)
>>> count.set(2)
>>> divisible_by_3()
呼 divisible_by_3
False
>>> count.set(1)
>>> count.set(3)
>>> divisible_by_3()
呼 divisible_by_3
True
上記の結果で分かる通りcount.set()を呼び出した時には"呼 ..."は出力されていませんがdivisible_by_3()の時にはされています。このようにComputedは自身の値が読み出されない限りは値を算出しないようです。
Computedは結果を保存する
>>> count.set(1)
>>> divisible_by_3()
呼 divisible_by_3
False
>>> divisible_by_3()
False
またこのようにcountの更新を挟まずに続けて値を読み出した場合も"呼 ..." の出力はありません。Computedは自身が依存している値が更新された時のみ再計算の必要があると判断するようです。
再計算は"直接"依存している値が変化した場合のみ
面白いのが、Computedが自身が間接的に依存している値に変化があったとしても直接依存している値に変化がなければ再計算を行わない事です。例えばcountが16から17に変わる時、対応するdivisible_by_3とcontains_the_digit_3の値は共にFalseのまま変わりません。16と17は両者3の倍数ではありませんし3のつく数値でもありませんからね。するとahoの再計算はどうなるかというと
>>> count.set(16)
>>> aho()
呼 aho
呼 divisible_by_3
呼 contains_the_digit_3
False
>>> count.set(17)
>>> aho()
呼 divisible_by_3
呼 contains_the_digit_3
False
このように行われません。
循環依存が無いかも調べてくれる
import reaktiv
@reaktiv.Computed
def x():
return y() + 1
@reaktiv.Computed
def y():
return x() + 1
x()
RuntimeError: Circular dependency detected
結び
★の少なさは不安ですがかなり面白いライブラリだと思いました。外部ライブラリに依存していない点も嬉しいです。気になる点があるとすればグローバル変数を多用していることでしょうか。CPythonがGILを無くす方向に向かっている中でこれが仇と成らない事を祈ります。
本記事で触れなかったもの幾つか
-
reaktiv.Effect...SignalとComputedに並ぶ主要部品。 -
reaktiv.set_thread_safety()... 既定値が真なので単スレッド時は偽にしておく?