ひみつノート 3
状態遷移(じょうたいせんい)
2024-08-08 作成 福島
TOP > asob > note03
[ TIPS | TOYS | OTAKU | LINK | MOVIE | CGI | AvTitle | ConfuTerm | HIST | AnSt | Asob | Shell ]

様々な「状態」

形があってもなくても、ものには必ず「状態」があります。

たとえば、地球の空気 (大気) は以下の様に認識されていますが、その時々によって「状態」が変化します。
大気の状態
標高 0m における標準的な大気の状態 (Wikipedia より引用)
要素備考
気圧1013.25hPa天気が悪いと低い値になる
窒素78.08%窒素はほぼ無害。
酸素の割合が減ると人体に影響が出る。
アルゴン、二酸化炭素は少量なら無害。
酸素20.95%
アルゴン0.93%
二酸化炭素0.03%
水蒸気0 ~ 4%雨が降ると高い値になる

分かりやすいのは気圧と水蒸気で、気圧が低くなれば天気が悪くなり、雨が降れば水分の割合が増加します。
それぞれの圧力や物質の混合比率により「大気」の状態が変化します。
ひとつの物体があったとき、その成分の割合を「状態」と言い換えることができます。
化学反応では核分裂や核融合など、成分そのものが物理的に変化するものもあります。

他の例だと、地球は自転していますが、真夜中から 12 時間後には昼になります。
これは、各地の状態が変化しています。
地球が 180°自転したとき
日本00:00(よる)12:00(ひる)
アルゼンチン12:00(ひる)00:00(よる)
「状態」は変化することが前提で、変化しなくても「初期状態」という状態があります。
状態は考え方で枠組みが変わる
物事のとらえ方によって状態の枠組みが異なるので例えば、
  1. A-1.右足を前に出す
  2. A-2.右足を戻す
  1. B-1.左足を前に出す
  2. B-2.左足を戻す
という、一連の状態があったとき、右足・左足それぞれに着目すれば、
  1. A.右足を前後に滑らせる。
  2. B.左足を前後に滑らせる。
という状態になるし、A, B を交互に行うと「歩く」という状態になります。
プログラミングにおける「状態」
自然現象や物理現象の状態が、いつどのように変化するかを厳密に定義することは不可能ですが、
プログラミングにおいては、すべての状態と変化のタイミングを厳密に定義し、その移り変わりを制御します。
これを「状態遷移(じょうたいせんい) 」と呼びます。
高等専門学校や大学の一部で教わることがありますが、それ以外の学校では教わりません。

数学で「点 P」がよく出てきますが、これに似ています。
数学では自然数や式そのものを解とするのに対し、プログラミングでは主に、時系列に従った各時点の値を取り扱います。

状態の変化するタイミングと内容を定義することを「ソフトウェアの設計」と言い、
それを具現化してコンピュータ用に翻訳することを「プログラミング」と呼びます。
(実はその次の段階として「コーディング」という作業がありますが、大抵はコーディングも含めてプログラミングと呼びます)
移動する点を描く
例として、移動する点を描きます。
コンピュータで図形を表示するときは、ピクセル (ドットともいう) という点の集まりで表現します。
方眼紙のマスを鉛筆で塗りつぶすと考えてください。
数学では実数を扱いますが、ここでは説明のため整数を使います。

点 (0,0) から (4,4) へ直線的に移動する点を描いてみます。
この直線の傾きは 1 (X を +1 すると Y に +1 される) ですね。
X 座標の増加をもとに各点の状態を整数で表すと、以下になります。
< 直線的に移動する点の座標 >
数学と異なり、コンピュータでは下方向に Y 座標が伸びます。(英文が左上 → 右下へ続くのと同様)
状態操作備考
状態 1X ← 0初期値: 0(X,Y) = (0,0)  
Y ← 0初期値: 0
状態 2X ← (状態 1 の X) + 1(X,Y) = (1,1)  
Y ← (状態 1 の Y) + 1傾き: 1
状態 3X ← (状態 2 の X) + 1(X,Y) = (2,2)  
Y ← (状態 2 の Y) + 1傾き: 1
状態 4X ← (状態 3 の X) + 1(X,Y) = (3,3)  
Y ← (状態 3 の Y) + 1傾き: 1
状態 5X ← (状態 4 の X) + 1(X,Y) = (4,4)  
Y ← (状態 4 の Y) + 1傾き: 1
これを、状態遷移として図示してみます。
< 直線的に移動する点の状態遷移図 (状態機械図)>
 (初期状態*1) ─→〔 移動状態*2 | 座標を保持する 〕─→⦅終了状態*3⦆ 
                     ↑                          │
                     └─────────────┘
                             [座標を更新する]
*1上記「状態 1」に相当。
*2上記「状態 2」~「状態 5」に相当。
*3上記「状態 N」には相当するものがない。
… かなり情報を削ってしまいました。
この図には座標 X, Y の記述が無いし、移動の状態は 2 ~ 5 がひとつに集約されています。

「状態遷移」とは読んで字のごとく、状態の遷移だけを扱うため、詳細 (実装方法) は後から考えます。
いきなり「状態を遷移する」と言っても日本語には直接の概念がなく、プログラミングに結び付けることが困難なため、ここでは順序を逆にして説明しました。
なので、「情報を削った」と言うより、「情報が追加される前の段階に戻った」と言う方が合っています。

この考え方に慣れてきたら、状態遷移 → プログラミング という風に考えるようにしてください。
(「抽象化」→「具現化」とも呼ばれます)

状態遷移を先に考え、プログラミング (実装) を後に考えるのが通常の作業です。
そのようにすれば、特定のプログラミング言語によって考え方が制限されることがなくなります。
(しかし、プログラミングの知識がなければ実装可能な状態遷移を考えることができないので、要件定義と同様、ここにも因果性のジレンマがあります)
実例 1 - 自動ドア
自動ドアを状態遷移で考えてみます。
< 自動ドアの状態遷移図 >
(初期状態) ─→〔ドア閉鎖状態 | 継続してドアを閉じる〕───────────→〔ドア開放状態 | 継続してドアを開ける〕
                  ↑                                   [センサーに何かを検知]                                     │
                  │                                                                                              │
                  └───────────────────────────────────────────────┘
                                                          [3 秒後]

説明用に捏造した、融通の利かない自動ドアです。
掃除のときに必要な「常時開放状態」もありません。

普段はドアを閉めていて、センサーに誰か (猫かもしれません) を検知するとドアを 3 秒間開きます。
3 秒後にはドアを閉めた状態に戻ります。
(説明のため 3 秒にしています。現実世界で 3 秒にすると、誰かが怪我をするでしょう)
実例 2 - 電卓
電卓を状態遷移で考えてみます。
< 電卓の状態遷移図 >
                  ┌─────────────────────────────────────────────────┐
                  │┌─────────────────────────────────────────┐            │
                  ││                                                                                  │            │
                  ││                            ┌─────────→〔 数値蓄積状態 | 数値を蓄積する 〕           │
                  ││                            │  [数字キー]                                                      │
                  ↓↓                            │                                                                  │
(初期状態) ─→〔 キー入力状態 | キーを受け付ける 〕─────────→〔 演算状態     | 蓄積した数値を使って計算する 〕
                                                  │  [四則演算キー    ]
                                                  │
                                                  └─────────→〔 数値表示状態 | 蓄積した数値を使って計算し、結果を表示する 〕─→⦅終了状態⦆
                                                      [イコールキー ]

説明用に捏造した、かなり簡素な電卓です。イコールキー を押すと終了してしまいます。
こんな電卓を販売したら、購入者からクレームの嵐になるでしょう。
実物の電卓は、これよりずっと複雑な状態遷移になりますが、基本は同じです。
(実物に⦅終了状態⦆は存在しません。その代わり「電源 OFF」があります)
オブジェクト指向で実装
「状態遷移」は、考え方が「もの」(正しくは事象) を中心としているので、同じ考え方の「オブジェクト指向」と相性がいいまとめ方になっています。
同じと思いがちですが、実は「状態遷移」と「オブジェクト指向」の考え方は発生元が異なります。

オブジェクト指向言語でおなじみの Python を使ってそれぞれをプログラミングしてみます。

自動ドア < AutomaticDoor.py >
#!/bin/python

"""自動ドアクラス"""
class AutomaticDoor:

    """自動ドアオブジェクトを初期化"""
    def __init__(self):
        self.openTimer = 0      # ドア解放のカウンタ
        self.open = False       # ドア状態

    """ドア開放カウンタを進める"""
    def tick(self, n=1):
        self.openTimer += n
        if self.openTimer >= 3: # 3 秒 (3 tick) でドアを閉める  
            self.openTimer = 0
            self.open = False

    """ドアの開閉状態を返す"""
    def isOpen(self):
        return self.open

    """何かを訪問させる"""
    def visit(self):
        self.open = True        # ドアを開ける
        self.openTimer = 0      # タイマーを初期化する


if __name__ == '__main__': door = AutomaticDoor() # 自動ドアインスタンスの用意 worldTime = 0 while True: # 自動ドアの状態を調査する if door.isOpen(): print('自動ドアは開いています。') else: print('自動ドアは閉まっています。') door.tick() # 自動ドアを少し進める。 import time time.sleep(1.0) # 1 秒待つ。 # 5 秒に一回、誰かが訪問する。 worldTime += 1 if (worldTime % 5) == 0: door.visit()
電卓 < Calculator.py >
#!/bin/python

"""電卓クラス"""
class Calculator:

    """電卓オブジェクトを初期化"""
    def __init__(self):
        self.value = 0      # 加減乗除される数
        self.answer = 0     # 加減乗除する数 (答えを兼ねる)     
        self.operator = 0   # 四則演算の指定

    """現在の答えを返す"""
    def getAnswer(self):
        return self.answer

    """キーを受け付ける"""
    def keyIn(self, k):
        n = '0123456789+-*/='.find(k)   # キーを判別する

        if 0 <= n and n <= 9:     # [0]~[9] が入力された
            self.answer *= 10
            self.answer += n
        elif n == 10:             # [+] が入力された
            self.value = self.answer
            self.answer = 0
            self.operator = 10
        elif n == 11:             # [-] が入力された
            self.value = self.answer
            self.answer = 0
            self.operator = 11
        elif n == 12:             # [*] が入力された
            self.value = self.answer
            self.answer = 0
            self.operator = 12
        elif n == 13:             # [/] が入力された
            self.value = self.answer
            self.answer = 0
            self.operator = 13
        elif n == 14:             # [=] が入力された

            # 指定されていた四則演算によって計算を実施する
            if self.operator == 10:     # [+] が指定されていた
                self.answer = self.value + self.answer
            elif self.operator == 11:   # [-] が指定されていた
                self.answer = self.value - self.answer
            elif self.operator == 12:   # [*] が指定されていた
                self.answer = self.value * self.answer
            elif self.operator == 13:   # [/] が指定されていた
                self.answer = self.value / self.answer

            self.value = 0      # 加減乗除される数を初期化する
            self.operator = 0   # 四則演算の指定を初期化する

            print(self.answer)  # 結果を表示する
            exit()              # 終了する


if __name__ == '__main__': calc = Calculator() # 電卓インスタンスの用意 calc.keyIn('1') # 数値 10 を入力する calc.keyIn('0') calc.keyIn('+') # 四則演算の指定 [+] を入力する calc.keyIn('3') # 数値 30 を入力する calc.keyIn('0') calc.keyIn('=') # イコール [=] を入力する
自動ドアプログラム、電卓プログラムともに、内部に保持している変数で挙動を制御 (遷移) していることが分かると思います。
また、メインプログラム (各プログラムの下半分) でクラスから生成したインスタンスを操作していますが、
呼び出しがシンプルなことに気がつくと思います。