どうやらJavaScript界隈にあるライブラリを真似た物のようで、中々面白そうだったので練習のためにこれで圧力鍋を作ってみました。
圧力鍋を作る?
圧力鍋と言っても調理器具の事ではなく人の名前です。そしてこの人は何故か3の倍数と(10進表記で)3を含む数値を聞くとアホっぽい顔(😜)、それ以外の数値を聞くと真顔(😑)になる性質を持っています。
只のカウンターを作る
まずは圧力鍋に聞かせるための数値を用意しました。
from reaktiv import Signal
count = Signal(0)
for i in range(1, 5):
count.set(i)
print(count())
1
2
3
4
このように値の書き込みはSignal.set()で、値の読み出しはSignal.__call__()で行うようです。読み出しはSignal.get()でもいけました。
3の倍数
次はこのカウンターが3の倍数であるかどうかを表す真偽値を用意しました。この真偽値のように他の値(count)に依存している物はSignalではなくComputedを用いるようです。
from reaktiv import Signal, Computed
count = Signal(0)
@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を含んでいるかどうかを表す真偽値も用意しました。
from reaktiv import Signal, Computed
count = Signal(0)
@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
圧力鍋もどき
もう圧力鍋は作れますが、ライブラリのとある重要な特徴を確かめる為に一旦3の倍数でのみ😜になる圧力鍋もどきを作りました。
from reaktiv import Signal, Computed
count = Signal(0)
@Computed
def divisible_by_3():
print("divisible_by_3 呼")
return count() % 3 == 0
@Computed
def 圧力鍋もどき():
print("圧力鍋もどき 呼")
if divisible_by_3():
return "😜"
else:
return "😑"
for i in range(1, 7):
count.set(i)
print(i, 圧力鍋もどき())
圧力鍋もどき 呼
divisible_by_3 呼
1 😑
divisible_by_3 呼
2 😑
divisible_by_3 呼
圧力鍋もどき 呼
3 😜
divisible_by_3 呼
圧力鍋もどき 呼
4 😑
divisible_by_3 呼
5 😑
divisible_by_3 呼
圧力鍋もどき 呼
6 😜
出力結果を見るとdivisible_by_3は毎回呼ばれてますが圧力鍋もどきはそうではありません。その理由はこのライブラリは不必要な再計算を行わないからです。どういう事かというと、まずdivisible_by_3が毎回呼ばれたのは、直接依存しているcountが毎回書き換えられたからです。それ故にライブラリはdivisible_by_3の再計算が必要であると毎回判断しました。
しかしそのdivisible_by_3の計算結果はどうなっていたかというとFalse False Trueの繰り返しです。つまりFalseからFalse、値が変わらない時がありました。この時ライブラリは圧力鍋もどきが直接依存しているdivisible_by_3に変化は無かったから圧力鍋もどきの再計算は必要無しと判断したようです、賢い。それ故に圧力鍋もどきが呼ばれない事があったのでした。
値の算出は怠惰
また次のコードで分かるように実際に値が読み出されるまで計算を行わない事もわかります。
count.set(1)
count.set(5)
print(divisible_by_3())
count.set(4)
count.set(3)
print(divisible_by_3())
divisible_by_3 呼
False
divisible_by_3 呼
True
このような設計は Pull-Model というらしい。(ライブラリ全体ではなくあくまで値の算出の部分がPull-Model)。
圧力鍋
だいぶ脱線したので本線に戻ります。圧力鍋の実装は以下のようになりました。
@Computed
def 圧力鍋():
if divisible_by_3() or contains_the_digit_3():
return "😜"
else:
return "😑"
ところでもしかすると圧力鍋のようなASCII以外の文字を含む識別子を喜ばない人が居るかもしれません。かと言ってatsuryokunabeは長くて読みづらいです。なので短くnabeatsuにでもしておきます。また数も「40ぐらいまで数えてあげたら?」という天の声が聞こえたのでそうします。
from reaktiv import Signal, Computed
count = Signal(0)
@Computed
def divisible_by_3():
return count() % 3 == 0
@Computed
def contains_the_digit_3():
return "3" in str(count())
@Computed
def nabeatsu():
if divisible_by_3() or contains_the_digit_3():
return "😜"
else:
return "😑"
for i in range(1, 41):
count.set(i)
print(i, nabeatsu())
1 😑
2 😑
3 😜
4 😑
5 😑
6 😜
7 😑
8 😑
9 😜
10 😑
11 😑
12 😜
13 😜
14 😑
15 😜
16 😑
17 😑
18 😜
19 😑
20 😑
21 😜
22 😑
23 😜
24 😜
25 😑
26 😑
27 😜
28 😑
29 😑
30 😜
31 😜
32 😜
33 😜
34 😜
35 😜
36 😜
37 😜
38 😜
39 😜
40 😑
結び
★の少なさは不安ですがかなり面白いライブラリだと思いました。外部ライブラリに依存していない点も嬉しいです。一応気になる点があるとすればグローバル変数を多用していることでしょうか。CPythonがGILを無くす方向に向かっている中でこれが仇と成らない事を祈ります。
本記事で触れなかったもの幾つか
-
reaktiv.Effect... おそらくSignalとComputedに並ぶ主要部品。 -
reaktiv.set_thread_safety()... 既定値が真なので単スレッド時は偽にしておく? -
reaktiv.Signal(equal=...)。値に変化が起きたかどうかの判定は既定ではis演算子でなされるようなので、本記事のcountはそれではまずそう。なのでcount = Signal(0, equal=operator.eq)とすべきかも。