1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

reaktiv: Pythonでリアクティブプログラミング

1
Last updated at Posted at 2026-04-23

どうやら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は既定では値が変化したか否かを値の同一性(is演算子)で判断しますが、今回の例のcountは同値性(==演算子)を変化の判断基準とすべきでしょう。

import operator

count = reaktiv.Signal(0, equal=operator.eq)

他の3つは真偽値なので同一性を判断基準としたままで問題無いと思います。

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, equal=operator.eq)

 
 @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(equal=operator.eq)
def x():
    return y() + 1

@reaktiv.Computed(equal=operator.eq)
def y():
    return x() + 1

x()
RuntimeError: Circular dependency detected

結び

★の少なさは不安ですがかなり面白いライブラリだと思いました。外部ライブラリに依存していない点も嬉しいです。気になる点があるとすればグローバル変数を多用していることでしょうか。CPythonがGILを無くす方向に向かっている中でこれが仇と成らない事を祈ります。

本記事で触れなかったもの幾つか

  • reaktiv.Effect ... SignalとComputedに並ぶ主要部品。
  • reaktiv.set_thread_safety() ... 既定値が真なので単スレッド時は偽にしておく?
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?