BASIC (ベーシック) であそぼう 7
〜 プログラムを使って会話 〜
2022-12-12 作成 福島
TOP > asob > decbasic-talk
[ TIPS | TOYS | OTAKU | LINK | MOVIE | CGI | AvTitle | ConfuTerm | HIST | AnSt | Asob | Shell ]

情報交換にはルールが必要

最近のスマートフォンや PC にはネットワークが必要で、これに繋がっていないと何もできないと言っても過言ではないでしょう。
何か作業をするとき、ひとりでそのすべてを処理することは稀で、ともすると「ぼっち」と揶揄されてしまいます。
これを解決する道具がコンピュータネットワークです。

例えば、誰かが文章を考え、それを違う誰かが校正したり、感想を述べたりして分業、協調します。
昔は紙に印刷したり、フロッピーディスクを渡したりしましたが、21 世紀の今はコンピュータネットワークを使って、
効率的かつ迅速に情報をやり取りします。

ひとりで作業をする場合は気にしなくて良いのですが、何人か集まるとルールが必要になります。
みんなが自分勝手に行動していたのでは、物事がうまく進まず混沌としてしまいます。
特にコンピュータネットワークは動作速度が速いので、あっという間に情報が混乱するのです。

ルールは多くありません。多くするとルールが守れなくなります。具体的には以下の 2 つです。
  1. 誰かの発言が相手に届くまで、他の人は発言しない。
  2. 自分が発言するときは相手を指名する。
他に発言中の人がいるのに自分がかぶせて発言するのは下品なだけでなく、情報の伝達が崩れてしまいます。
相手を指名しないで発言するのは、受容を他人に忖度させることに他ならず、これは即ち傲慢な行動です。
他人を困らせる人が会話に参加してはいけません。

本稿のアルゴリズムは、バス共有型に代表される非同期半二重通信方式で、これを機能制限して*1実装しています。
*1機能制限: データが自動的に消滅するように作ると、学習目的を大きく逸脱してしまうため。

参加者全員がひとつの場所を共有してメッセージをやり取りします。
周波数 (チャンネル) がひとつだけの無線機と考えると良いかもしれません。
(無線機と異なり、置かれたメッセージが自動的に消滅することはないので、自分宛てのメッセージも出せます)

便宜上この場所を「伝言台」と呼称します。
伝言台ではデータを頻繁に書き換えるので SSD のシステムの場合は RAM ドライブを利用するようにしてください。
Linux は tmpfs、Windows は RAM ディスクが RAM ドライブです。
ハードディスクなら特に気にする必要はありません。


0. 事前準備

0-1. この章では、十進 BASIC を使います。
インストールがまだなら、ここ (ラズパイ400)ここ (CentOS) の gtk2 版か、
ここ (Windows10) を参考にしてインストールしておいてください。
0-2. この章では、別のテキストエディタを使いません。
十進 BASIC には専用のエディタ画面があるので、これを使います。
0-3. 未経験者向けの情報を省いています。
プログラミング未経験の方は、予めこちら (BASIC であそぼう 1) を学習しておくことを推奨します。
0-4. ひとつの PC で 十進 BASIC を複数起動します。
狭い画面でも実行できますが、一度に複数のプログラムを起動するので、見易くするためには広い画面を用意してください。
0-5. 制限版のプログラムになっています。
下記プログラムファイル comm.BAS では、こちらの排他ロックプログラム exLock.BAS を使用します。
この排他ロックプログラムには Windows 専用版の exLockWin.BAS もあります。
違いは、自動解除機能の有無です。
exLock.BAS は排他ロックという、ロックファイルを使って交通整理を司る副プログラムです。
予期しないシャットダウン等にロックファイルが残ることがあり、これを自動解除できる exLockWin.BAS もあります。
自動解除を十進 BASIC で実装するため、内部で Win32 API を呼び出しているので Windows 専用です。

exLockWin.BAS を使用する場合は comm.BAS の最終行を MERGE "exLockWin.BAS" と変更してください。
(exLockWin.BAS のファイル設置を忘れないように)

ロックファイルが残った場合でも手動でこれを削除すれば問題ないため、無理に自動解除を有効にしなくても構いません。
(exLockWin.BAS を Linux で実行するとエラーが表示され、実行できません)


1. 自分と会話

1-1. ユーザプログラムの作成
<テキスト19> ファイル名「user1.BAS」
DECLARE EXTERNAL SUB COMM.Init
DECLARE EXTERNAL SUB COMM.Quiet
DECLARE EXTERNAL SUB COMM.Talk

CALL COMM.Init("user1", "R:\")     ! 自分の識別名*2と伝言台*3を指定。
CALL COMM.Quiet

DO
    ! メッセージ送信の入力。
    ! 送りたいメッセージがなければ受信だけを行う。
    INPUT PROMPT "誰へ?": to$
    IF to$ = "." THEN       ! プログラムを終了するときは「.」を入力する。
        PRINT "終了します。"
        EXIT DO
    ELSEIF to$ <> "" THEN
        INPUT PROMPT "内容?": msg$
    END IF

    CALL COMM.Talk(msgFrom$, to$, msg$)
    SELECT CASE COMM.Result
        CASE -1     ! 発言なし。
            PRINT "問題なし。"
        CASE  0
            PRINT "発言失敗。"
        CASE  1
            PRINT "発言成功。"
        CASE  2     ! 発言の前にメッセージが到着していた。
            PRINT "メッセージ受信。From:" & msgFrom$ & " " & msg$
            IF to$ <> "" THEN
                PRINT "発言は失敗。"
            END IF
    END SELECT
LOOP

END


MERGE "comm.BAS"
*2自分の識別名はメッセージを送受信するときに使われます。プログラムを複数起動するときは、他と重ならない名前に変更してください。
*3伝言台にすべてのファイル (ロックファイル, from ファイル, to ファイル, メッセージファイル) を置きます。

<テキスト20> ファイル名「comm.BAS」(シー・オー・エム・エム・ドット・ビー・エー・エス)
· ファイル保存時の文字コードを Shift-JIS にすること。
·「user1.BAS」と同じフォルダーに置くこと。
(%LOCALAPPDATA%\VirtualStore\Program Files (x86)\Decimal Basic\BASICw32\ の中でも OK)
MODULE COMM
    DECLARE EXTERNAL FUNCTION ExLock_lock
    DECLARE EXTERNAL SUB ExLock_unlock
    DECLARE EXTERNAL FUNCTION SendMessage
    DECLARE EXTERNAL SUB ReceiveMessage
    DECLARE EXTERNAL SUB WipeMessage

    PUBLIC SUB Init
    PUBLIC SUB Quiet
    PUBLIC SUB Talk
    PUBLIC NUMERIC Result

    SHARE STRING myName$
    SHARE STRING TalkEnv$(4)
    SHARE NUMERIC TalkRetry


! 環境条件を保持する。 EXTERNAL SUB Init(mName$, baseDir$) LET myName$ = mName$ ! 自分の識別名 LET TalkRetry = 5 ! リトライ回数 ! 作業用ディレクトリを作成する。 WHEN EXCEPTION IN MAKE DIRECTORY baseDir$ USE END WHEN LET TalkEnv$(1) = baseDir$ & "\speak.LCK" ! 発言ロック用ディレクトリ LET TalkEnv$(2) = baseDir$ & "\from.txt" ! 発言者名用ファイル LET TalkEnv$(3) = baseDir$ & "\to.txt" ! 宛名用ファイル LET TalkEnv$(4) = baseDir$ & "\message.txt" ! 本文用ファイル LET Result = -1 END SUB
! メッセージ置き場をクリアにする。 EXTERNAL SUB Quiet CALL WipeMessage(TalkEnv$, TalkRetry) END SUB
! 可能ならメッセージを書き込む。 ! 自分宛てのメッセージがある場合は読み込む。 ! ! メッセージの送受信状況を返す。 ! -1: 自分宛てのメッセージが無かった / メッセージを取得できなかった ! 0: メッセージを発言できなかった ! 1: メッセージを発言できた ! 2: 自分宛てのメッセージが到着していた (自分の発言は取り消し) EXTERNAL SUB Talk(talkFrom$, talkTo$, msg$) LET Result = -1 ! 問いかけを取り出す。 CALL ReceiveMessage(TalkEnv$, from$, to$, message$, TalkRetry) IF to$ <> "" THEN ! 誰かの発言がある。 ! 自分宛てのメッセージなら受領する。 IF to$ = myName$ THEN ! メッセージを受領したら、メッセージ用ファイルを削除する。 CALL WipeMessage(TalkEnv$, TalkRetry) LET talkFrom$ = from$ LET msg$ = message$ LET Result = 2 ELSE ! 他人へのメッセージなので、自分の発言は控える。 IF talkTo$ <> "" THEN Result = 0 END IF END IF IF Result = -1 THEN ! 発言できそう。 IF talkTo$ <> "" THEN ! 発言したいことがある。 ! 発言する。 LET Result = 0 LET to$ = talkTo$ LET stat = SendMessage(TalkEnv$, myName$, to$, msg$, TalkRetry) IF stat = 1 THEN Result = 1 END IF END IF END SUB
! メッセージを送信し、その結果を返す。 ! 0: 送信失敗 ! 1: 送信成功 EXTERNAL FUNCTION SendMessage(TalkEnv$(), from$, to$, msg$, count) LET lockDir$ = TalkEnv$(1) LET FromFilePath$ = TalkEnv$(2) LET ToFilePath$ = TalkEnv$(3) LET MessagePath$ = TalkEnv$(4) FOR i = 0 TO count - 1 LET lock = ExLock_lock(lockDir$) IF lock <> 0 THEN EXIT FOR WAIT DELAY 0.2 ! 0.2 秒待つ。 NEXT i LET said = 0 IF lock <> 0 THEN ! 自分を名乗る。 OPEN #1: NAME FromFilePath$, ACCESS OUTPUT SET #1: CODING "UTF-8" ! 符号を世界標準に合わせる。 PRINT #1: from$ CLOSE #1 ! 相手を指名する。 OPEN #1: NAME ToFilePath$, ACCESS OUTPUT SET #1: CODING "UTF-8" PRINT #1: to$ CLOSE #1 ! 本文を設置する。 OPEN #1: NAME MessagePath$, ACCESS OUTPUT SET #1: CODING "UTF-8" PRINT #1: msg$ CLOSE #1 LET said = 1 END IF CALL ExLock_unlock(lockDir$, lock) LET SendMessage = said END FUNCTION
! メッセージを受信する。 EXTERNAL SUB ReceiveMessage(TalkEnv$(), from$, to$, message$, count) LET lockDir$ = TalkEnv$(1) LET FromFilePath$ = TalkEnv$(2) LET ToFilePath$ = TalkEnv$(3) LET MessagePath$ = TalkEnv$(4) LET from$ = "" LET to$ = "" LET message$ = "" FOR i = 0 TO count - 1 LET lock = ExLock_lock(lockDir$) IF lock <> 0 THEN EXIT FOR WAIT DELAY 0.2 ! 0.2 秒待つ。 NEXT i IF lock <> 0 THEN ! メッセージを読み込む。 WHEN EXCEPTION IN OPEN #1: NAME FromFilePath$, ACCESS INPUT SET #1: CODING "UTF-8" ! 符号を世界標準に合わせる。 INPUT #1: from$ CLOSE #1 OPEN #1: NAME ToFilePath$, ACCESS INPUT SET #1: CODING "UTF-8" INPUT #1: to$ CLOSE #1 OPEN #1: NAME MessagePath$, ACCESS INPUT SET #1: CODING "UTF-8" INPUT #1: message$ CLOSE #1 USE ! ファイルが無くてもエラーにしない。 CLOSE #1 END WHEN END IF CALL ExLock_unlock(lockDir$, lock) END SUB
! 名乗り用、宛名用、本文用ファイルをすべて削除する。 EXTERNAL SUB WipeMessage(TalkEnv$(), count) LET lockDir$ = TalkEnv$(1) LET FromFilePath$ = TalkEnv$(2) LET ToFilePath$ = TalkEnv$(3) LET MessagePath$ = TalkEnv$(4) FOR i = 0 TO count - 1 LET lock = ExLock_lock(lockDir$) IF lock <> 0 THEN EXIT FOR WAIT DELAY 0.2 ! 0.2 秒待つ。 NEXT i IF lock <> 0 THEN IF FILES(FromFilePath$) > 0 THEN FILE DELETE FromFilePath$ IF FILES(ToFilePath$) > 0 THEN FILE DELETE ToFilePath$ IF FILES(MessagePath$) > 0 THEN FILE DELETE MessagePath$ END IF CALL ExLock_unlock(lockDir$, lock) END SUB END MODULE
MERGE "exLock.BAS" ! 排他ロック副プログラム。交通整理にはこれが必須。*4
*4exLock.BASこちらと同じもの。comm.BAS と同じフォルダーに置くこと。
1-2. ユーザプログラムの実行
プログラム user1.BAS を起動して、自分に話しかけてください
親しい友人が見ていたら精神を心配されるかもしれませんが、動作確認のためなので仕方がありません。

自分の識別名は user1*2 です。

存在しない識別名へも発言出来てしまいます。
そうなると発言ファイルがクリアされないので、プログラムを起動しなおしてください。

誰へ?
user1
 (1) 発言 : 伝言台に伝言を置く。
内容?
お元気ですか?
誰へ?user1
内容?お元気ですか?
発言成功

誰へ?
 (2) 受信 : 伝言台に自分宛ての伝言があるかチェックする。
メッセージ受信。From:user1 お元気ですか?

誰へ?
user1
 (3) 発言
内容?
はい元気です
誰へ?user1
内容?はい元気です
発言成功

誰へ?
user1
 (4) 発言
内容?
あなたは元気ですか?
誰へ?user1
内容?あなたは元気ですか?
メッセージ受信。From:user1 はい元気です
発言は失敗。
 (5) 受信 : 自分宛ての伝言があった。

誰へ?
user1
 (6) 発言
内容?
あなたは元気ですか?
誰へ?user1
内容?あなたは元気ですか?
発言成功

誰へ?
 (7) 受信
メッセージ受信。From:user1 あなたは元気ですか?

誰へ?
user1
 (8) 発言
内容?
私も元気です。
誰へ?user1
内容?私も元気です。
発言成功

誰へ?
 (9) 受信
メッセージ受信。From:user1 私も元気です。

誰へ?
.
 (10) 会話を終了する。
終了します。
1-3. ユーザプログラムの説明
1-3-1. メッセージの衝突回避
冒頭で少し述べましたが、伝言台に伝言ファイルを置き、それを読むことで情報のやり取りを行います。

伝言ファイルは、送り主、宛先、メッセージの 3 ファイルです。
(分けずに 1 つのファイルでも実現できますが、プログラムを簡潔にするため分けています)

自分の都合で好き勝手に伝言ファイルを置くと、他の誰かがファイルを置くタイミングで自分も同じファイルを置いてしまうかもしれません。
交差点で起きる「出合い頭であいがしら事故」のような事象ですが、英単語で「コリジョン」と呼びます。

コリジョンを防止するための仕組みを「排他制御はいたせいぎょ」と呼び、本稿では exLock.BAS を使います。
現実の世界では「押しボタン式信号機」がそれに近い概念です。

以下を繰り返すことにより、データの衝突を避けます。
  1. ファイルを置く前にまず一本の旗を奪い合う。
  2. 旗を手に入れた人だけがファイルを伝言台に置く。
  3. ファイルを置いたら、旗を元に戻す。
  4. 旗を持っていない人は、ファイルを置くのを諦める。または旗が元に戻されるまで一定時間待つ。
  5. 1 へ戻る。
1-3-2. プログラムの階層化
プログラムの大きさを見てわかる通り comm.BAS が最も重要で、処理の多くを担っています。
プログラムの構成図 [1]
主プログラムuser1.BAS
comm.BAS抽象化ブロックInit, Quiet, Talk
実務ブロックSendMessage, RecvMessage, WipeMessage ↔ メッセージ伝言台
ライブラリexLock_lock, exLock_unlock ↔ 排他制御
comm.BAS のプロシージャは大きく分けて 2 つのブロックに分類できます。

Init, Quiet, Talk の抽象化ブロックと、
SendMessage, ReceiveMessage, WipeMessage の実務ブロックです。

主プログラムである user1.BAS から呼び出すのは抽象化ブロックのプロシージャです。
実務ブロックのプロシージャを主プログラムから直接呼び出すことはありません。
(呼び出しても構わないが、プログラムの構造が汚くなる)

実務ブロックのプロシージャは抽象化ブロックから呼び出され、ライブラリを呼び出します。

このように、プログラムにレベルを設け、種類別に保守できるようにすることを「階層化」と言います。

分かりやすい構造、分かりやすいコーディングは重要です。
あなたのプログラムを他人が理解できないのはアルゴリズムが高度なわけでなく、単純にコードが汚いからです。
「プログラムは動けば良い」という時代は 1999 年に終わりました。汚いコードはバグを引き寄せます。
自分が書いたプログラムを他人が理解できなかったら、優越感に浸るのではなく自分のプログラムを改良しなければなりません。

最初は難しいと思いますが、階層を考えてプログラムを設計できるように心がけてください。
階層化するとプログラムが格段に分かりやすくなります。


2. 他人と会話

2-1. 複数で会話するプログラムの作成
user1.BAS をコピーしてもうひとつ user2.BAS を作成し、その識別名を user2 へ変更します。
user1.BAS との違いは「自分の識別名」だけで、他はすべて同じです。

<テキスト21> ファイル名「user2.BAS」
DECLARE EXTERNAL SUB COMM.Init
DECLARE EXTERNAL SUB COMM.Quiet
DECLARE EXTERNAL SUB COMM.Talk

CALL COMM.Init("user2", "R:\")     ! 自分の識別名*5と伝言台*6を指定。
CALL COMM.Quiet

DO
    ! メッセージ送信の入力。
    ! 送りたいメッセージがなければ受信だけを行う。
    INPUT PROMPT "誰へ?": to$
    IF to$ = "." THEN       ! プログラムを終了するときは「.」を入力する。
        PRINT "終了します。"
        EXIT DO
    ELSEIF to$ <> "" THEN
        INPUT PROMPT "内容?": msg$
    END IF

    CALL COMM.Talk(msgFrom$, to$, msg$)
    SELECT CASE COMM.Result
        CASE -1     ! 発言なし。
            PRINT "問題なし。"
        CASE  0
            PRINT "発言失敗。"
        CASE  1
            PRINT "発言成功。"
        CASE  2     ! 発言の前にメッセージが到着していた。
            PRINT "メッセージ受信。From:" & msgFrom$ & " " & msg$
            IF to$ <> "" THEN
                PRINT "発言は失敗。"
            END IF
    END SELECT
LOOP

END


MERGE "comm.BAS"
*5自分の識別名はメッセージを送受信するときに使われます。user1 と異なる名前を指定します。
*6伝言台は user1 と同じ場所に合わせます。
comm.BAS と exLock.BAS も上記 1-1 と同様に user2.BAS と同じ場所へ置いてください。
2-2. 複数で会話するプログラムの実行
user1.BAS, user2.BAS を両方とも起動してから会話を開始します。
それぞれのプログラムから user1 と user2 で互いにメッセージを送り合います。
  • user1 ‐ メッセージ → user2
  • user1 ← メッセージ ‐ user2
例では user1 から会話を開始していますが、どちらが先でも構いません。
読みやすくするために行間を広げています。実際は広がっていません。

user1 を起動
誰へ?
user2
内容?
お元気ですか?
誰へ?user2
内容?お元気ですか?
発言成功


















誰へ?
user2
内容?
あなたは元気ですか?
誰へ?user2
内容?あなたは元気ですか?
メッセージ受信。From:user2 はい元気です
発言は失敗。












誰へ?
メッセージ受信。From:user2 あなたは元気ですか?

誰へ?
user2
内容?
私も元気です。
誰へ?user2
内容?私も元気です。
発言成功







誰へ?
.
終了します。
   user2 を起動











誰へ?
メッセージ受信。From:user1 お元気ですか?

誰へ?
user1
内容?
はい元気です
誰へ?user1
内容?はい元気です
発言成功












誰へ?
user1
内容?
あなたは元気ですか?
誰へ?user1
内容?あなたは元気ですか?
発言成功



















誰へ?
メッセージ受信。From:user1 私も元気です。

誰へ?
.
終了します。
2-3. 複数で会話するプログラムの説明
user1 と user2 のプログラムは同じで、識別名だけが異なっていますがうまく動作します。
「そうなるように作ってある」と言ったほうが分かりやすいかもしれません。

上記 1-3-2 のように、構成図にするとこうなります。
プログラムの構成図 [2]
主プログラムuser1.BASuser2.BAS
comm.BAS抽象化ブロックInit, Quiet, Talk
実務ブロックSendMessage, RecvMessage, WipeMessage ↔ メッセージ伝言台
ライブラリexLock_lock, exLock_unlock ↔ 排他制御
冒頭のルール「自分が発言するときは相手を指名する」は、以下の動作に要約可能で、
識別名が異なってもアルゴリズムは同一のため、同じプログラムを使うことができます。
  • 相手の識別名を指定して発言する。
  • 他人宛ての発言は無視する。
  • 自分宛ての発言を取り込む。
comm.BAS はこれを守ってプログラミングされています。


3. さらに会話を実験

3-1. 他の実装と会話する。
ルールさえ守られていれば、十進 BASIC ではない他のプログラムと会話することができます。
こちらを参照して、異なる言語で作られた同じプログラムとの会話を実験してください。
(固有の識別名にすることを忘れずに)
3-2. ネットワーク越しに会話する。
プログラム exLock.BAS は、排他制御にディレクトリを使用しているため
SMB (Windows で構成するファイル共有) や、NFS (Linux のファイル共有) でも動作できます。

他の PC やファイルサーバ (NAS とも呼ばれます) に共有フォルダーを設定し、
そこを伝言台にすることにより、自分以外の PC との会話を実験してください。
(共有フォルダーの設定は、PC の持ち主や LAN の管理者に相談してください)

伝言台にする PC やファイルサーバで必ずしもユーザプログラムが動作する必要はありません。
また、会話する PC の数は 2 台とは限りません。理論上は無制限に会話できます。
3-3. さらに多くのプログラムと会話する。
user1, user2 とプログラムを増やしたように、user3, user4, ··· と PC が許す限りいくつでもプログラムを増やすことができます。
好きなだけプログラムを増やして実験してください。
(固有の識別名にすることを忘れずに)

プログラムを増やしすぎると自分の手足が足りなくなるので、友人に手伝ってもらってください。
この章は、ここで終了です。