🥚

CASE文によるステートマシン

2024/11/15に公開

はじめに

本記事は、PLC(Programmable Logic Controller)のソフトウェア開発に従事、または関心のある方で、Structured Text(ST)による開発に興味がある方向けです。OMRON社のSysmac Studio環境を想定していますが、他環境へも応用可能です。

STでソフトウェアを開発したことはあるでしょうか。STは数値処理を差し込むためだけのツールではありません。制御全体を記述することができます。しかし、言語を扱えるだけでは不十分です。STによるソフトウェア開発では、CASE文による処理に習熟しているかが、開発の成否を分けるほど重要です。会得すべきは、サイクル毎にCASE文のある節だけを実行し、その末尾で次のサイクルに実行する節を決定する構造です。高尚にCASE文によるステートマシン(CASEステートマシン)と呼ぶことにしますが、実用的な手段です。以下のような構造です。

CASE State OF
	0:
		// Do something.
		// ...
		Inc(State);
	1:
		// Do something.
		// ...
		IF A THEN
			Inc(State);
		ELSE
			Dec(State);
		END_IF;
	2: 
		// Do something.
		// ...
		IF B THEN
			State := 0;
		END_IF;
	...
END_CASE;

この構造を応用することで、多くのメリットを得ることができます。もちろん、万能ではありません。不適当といえる場合もあれば、メリットを引き出せるかは開発者次第という不確かさもあります。一般的な言語からすればアンチパターンにも見えますが、これは、苦肉の策として見出された[1]構造です。規格(IEC 61131-3第3版)としては、OOPの要素を補強していますが、展開状況は芳しくありません。また、OOPの取り込みもどことなく危うさを感じます。実際のプラクティスや方法論についての情報の乏しさがその理由です。

課題

CASE文ステートマシンを用いない場合の課題は、ラダー(LD)で記述したソフトウェアをそのままSTに置き換える場合を考えることで得られます。PLCの種類や開発環境を問わず2つの課題があります。多数のIF文深いネスト構造です。

多数のIF文

LDは、接点とコイルが主たる構成要素です。連続した接点は、論理積(AND)であり、並列して同一の接点またはコイルに接続する接点は論理和(OR)です。それをそのままSTに置き換えれば、多数のIF文が生じます。接点の中には、LD記述上簡素にするために設けた接点も存在します。それらが装置状態として曖昧、あるいは、名無し(一時変数など)とも言える接点であれば、条件式の意図を読み取ることも苦労します。

多数のIF文が上から下へ並んだST(あなたの慣れ親しんだ言語でも)で構成したソフトウェアを想像してください。全てのIF文の条件と処理は互いに独立しているでしょうか。あるIF文の条件式は、別のIF文の条件式に他の条件を追加したものになっていませんか。あるいは、あるIF文の処理の結果が、別のIF文の条件式に含まれていませんか。このような構造を見るとIF文の条件式を整理し、あるIF文の処理内に別のIF文を移動したくなるものです。

深いネスト構造

LDでもFOR命令を使用していれば、ネストです。あるコイルが接点として存在していれば、それもネストです。ネストは問題ではありません。深いネストが問題です。深いネストは、視認性の悪さも相まって把握に苦労します。そのコードに問題があることも示しています。何を成すべきかを考えることなく追加されたのかもしれません。あるいは、四苦八苦して何とか動かしたという状況の現れであるかもしれません。そうであれば、そのコードは妥当に動作しているか疑わしくなります。

なぜ、深いネストが生じるのでしょうか。深いネストを良しとする開発者はいない(と信じています)とすれば、ソフトウェアの発展の結果として、生じていることになります。また、繰り返し制御(FORやWHILE)による深いネストは、早々にサイクルタイムを圧迫するため残らないでしょう。IF文が深いネストを構成します。

なぜ課題が生じるのか

課題の導出に疑問を覚えなかったでしょうか。幸か不幸か、本格的にSTを使用しないという選択によって現実的な課題として溢れかえることにはなっていません。どれだけのベンダーが、自社PLCのマニュアルでST言語仕様、数値処理を目的としたインラインST以上の解説を行っているでしょうか。現状では、LDに親しんだ開発者は、LDで考えるように考え、STで記述することになります。

例えば、ある行が、多数の接点を前提とし、コイルをONにするとしましょう。そのうち、いくつかの接点は他のコイルをONとする行と同じとします。さらに、別のいくつかの接点は相互にインターロックしているとします。

これらをSTに置換することを考えます。全ての接点の論理積で構成した別個のIF文とするのがよいでしょうか。他の行の共通接点でくくってネストしたIF文にするのがよいでしょうか。CASE文で条件選択を行えるような構造に作りかえるのがよいでしょうか。いずれにしても同等のものを記述しようとしたとき、突如としてそこに大きく異なる選択肢が出現します。接点とコイルで展開される選択肢よりも複雑で、判断し難い印象を与える選択肢です。あるいは、全く選択肢が考え付かず、大きなIF文を強制されます。

LDはどこに点(接点、コイル)を置くかで課題を解決していきます。比較FUN、タイマーFUN/FB、モーション制御FBはいずれも点です。比較FUNは比較という処理こそ行いますが、次を評価するか否かの点です。タイマーFUN/FBは、時間の影響こそ受けますが、やはり次を評価するか否かの点です。そもそも、いずれも出力を一度コイルで受け、他で接点として使用できるのですから点であるのは当然のことです。モーション制御FBは、コイルとしての役割が殆どです。ブレンドして動作させる場合は、接点にもなります。

STはどうでしょうか。何かに集約することができません。ロジックを記述するといっても、どのような課題でどのような構造なのかによって異なることでしょう。あるいは、直接にロジックを記述するのではなく、課題を解決するロジックが生じる環境を作るようなこともあります。影を描いて対象を浮かび上がらせるようにです。

LDは手段が限定的であるため、すぐにHowに取り組んでもハズレが少ないのですが、STはWhatによってHowが全く異なるため、すぐにHowに取り組むとハズレを引く可能性が高いのです。そのため、LDで考えるようにSTを考えると、不格好で使い勝手が悪かったり、上手くいかなかったりというハズレを引きやすくなります。また、STのHowは、制御構造に限ったものではなく多岐に渡ります。しかし、紹介されるのは、制御構造に限られることが殆どです。そして、その制御構造によって処理を記述すると解説され、印象付けられるので、それに従って記述することになります。

STの制御構造以外のHowとは、結局、デザインパターンやアーキテクチャといったトピックのことです。しかしながら、主要な言語と一般的な実行環境で有益な手法や考え方が、必ずしも有益、あるいは同様に展開可能ではありません。これは、STの言語仕様と実行環境の制約、ソフトウェアに課す役割の差異によります。あるいは、有益とされる手法や考え方が、特定のプログラミングパラダイムを強く要請しているように見えるためです。一般的に述べられる手法や考え方を具現する場合、程度の差はありますが、特殊化を伴います。それが特定の言語、環境でのプラクティスになります。STは、それが大きく欠けています。その欠乏の表象として先の課題が現れるのです。

詳しく

CASEステートマシンは、サイクル毎にCASE文のある節だけを実行し、その末尾で次サイクルに実行する節を決定する構造です。この構造は、一定時間内に処理を完了し、かつ、状態(ソフトウェア内外いずれも)によって処理を変える必要があるという要求に対して、明確にロジックを記述できると同時に、サイクル進行に対してロジック構造を展開できるようになります。正確に言えば、明確であることを強く要求し、そうでなければ、破綻します。何となく動くものや、何となく動きそうなものを試すという考えは、その記述コストの高さに圧迫されます。

IF文を駆使すれば、同機能のコードを記述できます。割り込みや、より一般的な言語が使用可能であれば、異なる選択肢があります。しかしながら、IF文は、先の課題回避に悩み、より一般的な言語の選択は、選択可能なハードウェアの制約と開発チームに要求する技術スタックの違いに悩むことになります。LDで十分でしょうか。CASEステートマシンの効果をLDで容易に実現できているならその通りです。

効果

CASEステートマシンの効果は3つあります。関心の分離処理のサイクル分離非同期ファンクションブロック(FB)の使用です。

関心の分離

関心の分離は最も有用な効果です。CASE文の節を関心(責務・為すべきこと)に対応させることで、実現することができます。もちろん、関心の分離は手段や方法についてのトピックではないため、違和感があります。短絡的に述べているわけではありません。CASEステートマシンをどのように構成するかということに起因しています。CASEステートマシンの構成は、関心の分離に従って検討します。そしてその最も素朴、かつ、最も出現する構成が、各CASE節が各関心に対応するという構成です。関心の分離は、CASEステートマシンを構築する作業の結果として得られるのではなく、関心の分離に従ってCASEステートマシンを検討するが為に得るのです。機械的に得られるわけではありません。少し強引に因果を逆転すると、CASE文の節に関心を対応させるということになります。後はこれがどれだけ有益(または害悪)であるかです。

重要な点は、関心という視点を持つことです。もし、LDでそのような視点が重要視されているのであれば、ある行は何らかの関心に対して凝集し、あるセクションはそれを含むより機能的な凝集を実現するように構成されているでしょう。さて、FAの世界はそのようなLDで溢れかえっているでしょうか。恐らく、そのような環境は稀なのではないでしょうか。そうであれば、半ば強引でも明確な指針がある方が良心的です。習熟すれば、関心の分離がそれほど単純でないことも、何かに機械的に従えばよいわけでないことも体験し、無意識に近い状態で判断しながら必要なことを成せるようになるのではないでしょうか。

素朴なサーボ制御を考えてみましょう。原点位置にあるワークを指定の目標位置まで移動するというものです。ここでは、サーボの状態を関心とします。

  • 原点位置で停止中に為すべきこと
  • 加速中に為すべきこと
  • 定速動作中に為すべきこと
  • 目標位置手前で減速中に為すべきこと
  • 目標位置で停止中に為すべきこと

これらは全て同じでしょうか。関与する外部信号、定常状態を考慮しただけでも異なります。操作仕様や動作に対する細かな要求を考慮すれば、さらに差異が出てくるでしょう。あるいは、仕様変更で変則的な動作が必要になったとします。上記のいずれとも異なるとし、その状態を追加(CASE節として分ける)しておけば、ざっくり"こう動かしてくれ"と要求されても、ある状態からその状態に移行するのか、その場合はどのようにしたいのかを抜けなく確認することになります。コードにはその意図が明確に残ります。

処理のサイクル分離

PLCによる制御は、処理を、決めたサイクル時間内、あるいは、タクトタイム内に終えることが必要です。簡単な数値処理でも二重のFORループでサイクルタイムオーバーは発生します。単一処理の繰り返しであれば、サイクル当たりの実行回数を制御すれば十分です。しかし、異なる高負荷の処理を連続的に行う必要があるのであれば、CASEステートマシンとすることにメリットがあります。しかし、その目的は関心の分離にあります。処理のサイクル分離は副次的です。その処理は単一のアルゴリズムではなく、一種のパイプライン処理であるかもしれません。各処理の内容を動的に切り替える可能性や例外処理が必要となるかもしれません。

サイクルタイムの制約に対する手立ては、それなりにあるため、その解消を目的としてCASEステートマシンを選択することは推奨しません。高負荷な処理がサイクルタイムを圧迫している場合、タスク設計に問題があることを示しています。高負荷な処理を別タスクに移行し、タスクセーフ(スレッドセーフ同様)なメッセージキューでやり取りすることもできます。イベントタスクの制約が問題にならないのであれば、それもよいでしょう。

非同期ファンクションブロック(FB)の使用

非同期FBは、SDカード操作、ネットワーク処理やサーボ制御が必要な場合、必ず使用します。また、非同期FBに限らず、いつ返ってくるか分からない応答に対応するというのは、珍しいことではありません。処理サイクルに同期してIOリフレッシュが可能なフィールドバスを採用していてもです。外部装置との信号授受は、主フィールドバスに接続したリモートIOを介しても非同期です。まして、主フィールドバス以外であれば一定の周期性を担保するか否かに関わらず非同期です。

STには非同期処理を同期的に記述する機能や糖衣構文が存在しません。一般には投げっぱなしで非同期FBを実行するか、インターロックで不足の事態をガードすることが多いようです。CASEステートマシンを使用することとで、明示的な状態管理と例外処理を行いやすくなります。

CASEステートマシンの構成

CASEステートマシンはどのように構成すればよいのでしょうか。関心の分離に従って検討することが重要です。ここでは、"関心"という用語を都合よく使用します。明らかに他に適切な用語がある場合もあります。用語と分量の増大、曖昧さと不確かさの増大のトレードオフで、読者を信頼して後者を選択します。できる限り日常的に考えるという目的もあります。

LDで記述した、一連のセンサとアクチュエータ制御をCASEステートマシンに置換することを考えてみましょう。センサ入力とアクチュエータのフィードバックを外部信号とし、アクチュエータの動作手続きにエラーモード動作と停止動作を加えた構成になるでしょう。主役はアクチュエータです。アクチュエータに目的とする動作をさせ、非安全な状態を極力回避することが為すべきことです。なぜ、為すべきことなのでしょうか。付加価値を生むからです。生産設備のソフトウェアの目的は、汎用ソフトウェアに比べ明確です。目的とする動作は、一連の動作手順に分解されるでしょう。非安全な状態は外部信号とどの動作手順にあるかで異なりますが、概ねアクチュエータが動作するが為に引き起こされるのですから、アクチュエータが主因です。そうであれば、アクチュエータの動作手順を関心として検討することが妥当でしょう。

制御対象が為すべきことを洗い出すと、実装するために必要となる補助的機構も関心にあがり始めます。例えば、一連の動作手順が複数あり、大部分が重複しているとき、重複部分を共通化して実行する機構についての関心があるでしょう。フィールドバス異常といったシステム自体の不具合も関心になります。

ここで、LDのソフトウェアがどのようなものか検討してみましょう。LDで記述したソフトウェアは、制御対象となる電気設備の回路を厳格に反映していれば、並列な機能群と関連した動作可否機構、補助的な表示機構、機能に独立で優先する安全機構から成っていると見なせます。過度に複雑なソフトウェアは稀です。特定の機能性を実現する回路もありますが、それらは手段です。設備回路とロジックの符合が崩れ、ソフトウェアの性質が強まると規模と複雑さが増しますが、基本的な構造は同じです。過度な複雑さが見られても、動作ロジックのスパゲッティ化、動作可否信号のアドホックな追加、外部通知機能の追加、タイマーによる短絡的調整によってもたらされたもので、構造的な変質はまずありません。サーボ制御があるとインターロックと動作手順の増加でサーボ制御外の規模が増しますが、やはり構造的には同じです。タッチパネルやその他入力機器は大きく見積もっても、並列な機能群の一つです。もし、手元にLDのソフトウェアがあるようであれば、観察してみましょう。

一連のセンサとアクチュエータ制御の置換は、機械的に適用しても使用できる状態まで実装できたならば、大きな弊害はまず無いでしょう。また、適用できる事例は少なくないので、検討のための事例を探し回ることも無いはずです。関心については説明していません。それが何であるかを考え頭が痛くなっていれば問題ありません。私にできることは、それを行うのに近い状態を作ることです。確かなことは、関心の分離について理解が深まったとしても、常に考えることが求められるということです。理解をすれば楽になるというものではありません。

よくできたCASEステートマシンは、コードを読むにしても確認するにしても一つの節にだけ注目すれば、必要なことが満たせるものです。そのような状態にあるとき、関心の分離は十分になされています。

CASEステートマシンの構造

CASEステートマシンの骨子は、CASE文です。CASE文は、条件分岐の制御構造です。CASEステートマシンはCASE文の制御構造を使用しているにすぎません。よって言語による特別なサポートがあるわけではないので、意識的に構造を作り上げることが必要です。CASEステートマシンで展開するロジック構造は開発者次第です。素朴なCASEステートマシンは、手順をステップに分けた手続き記述と見なせます。これが、最終ステップから最初のステップに戻ることを繰り返せば繰り返し構造です。何らかのイベント(外部信号の変化)を待ち、イベントが生じたら特定のステップを実行するのであれば、オブザーバの一種です。CASEステートマシンのロジック構造は、サイクル進行によって明確になりますが、静的にも十分読み取れます。サイクルはCPUのクロック、CASE節のコードは、複雑な命令と考えれば不思議なことではありません。アセンブリ言語、あるいは、インストラクション・リスト(IL)を考えてみてください。

CASE文とCASEステートマシンの比較

まずは、CASE文を再確認します。

CASEOF
	選択値: 文
	選択値:...
ELSE:END_CASE;

"式"の評価値に一致する"選択値"の"文"を実行し、評価値がいずれの"選択値"にも一致せず、デフォルト節(ELSE)が存在すれば、その文が実行されます。選択値と文の組み合わせを便宜的に節と呼びます。

次に、CASEステートマシンを確認します。

CASE 状態変数 OF
	状態値: 文
	状態値:...
END_CASE;

"式"ではなく"状態変数"として変数を評価します。"状態式"となる可能性も無いわけではありませんが、重厚なミドルウェアや別言語の処理系を実装するのでもない限り、不必要な機能性を持ち込むことになります。選択肢を減らし、ハズレを引く可能性を小さくします。不足があるとなった時に初めて、本来の機能性を選択肢に含めます。デフォルト節は使用しません。存在しない状態値を拾って復旧する理由はありません。デフォルト節に制御が移るのは致命的な欠陥です。

状態値

状態値は、プログラムのアドレス、あるいは、ラベルと見なせます。エントリポイントとなる状態値は、定数としてPOU内に定義します。Sysmac Studioでは適切な名称を付けた整数型変数を、初期値を状態値、コンスタント属性を有効にして定義します。各節では、状態値変数にその定数を代入することで、遷移先の状態を指定します。定数を定義しておくことでリファクタリングで状態値を変更しても、変更箇所が1か所となります。

状態値は、適当な間隔を持たせます。CASE文の選択値は整数であるため、隣接する節間の状態値の差が1であると、変更のコストが増します。隣接した節間に別の節を差し込む場合、変更が不要な周囲の状態値を変更することになるためです。非同期FBの定型的な処理や、シミュレータで十分なテストが行える状態であれば、状態値の差が1でも大きな支障はありません。

状態値の差は、凝集度の大きさ(状態値の差が小さければ凝集度が高い)と考えると値を決める目安になります。また、節の数が10を超える一連の処理は、何らかの定型処理を含んでいないか確認します。多数の節で構成する一連の処理は、ルーチン節(後述)、FUNやFBに分離して凝集度を高めることで可読性や再利用性の向上、変更の負担軽減を実現することができます。FUNやFBへの分離は、強い隠蔽をもたらし、追加の構造体やデータ変換処理を必要とするようになると、構成要素が増えることになります。

状態値にコード設計を施す場合、緩やかであれば可読性の向上に寄与します。コード設計を駆使して大規模なコード構築を実現することは避けましょう。そもそもCASEステートマシンは消去法による選択です。できる限り単純な、必要最低限のコードで目的を達成することを目指します。

列挙型の状態値

列挙型の状態値は、POU外に状態を通知する必要がある場合に使用します。しかし、外部に対してはできる限りの隠蔽を目指すほうが良い結果につながります。なぜ外部に公開する必要があるのかについて、それは何を目的にしているのか、使用せずに目的を達成する方法が全くないのかを確認します。

POU外に状態を通知するとした場合、POU内はリファクタリングを想定した構造とします。不要と分かった時点で、リファクタリングを行えるようにするためです。CASEステートマシンで列挙型の状態値を直接使用することは避け、CASEステートマシンの実行後に整数型の状態値から列挙型の状態値を生成する処理を追加します。Sysmac Studioであれば、状態値の除算とNumToEmun命令で十分です。凝った仕組みは避け、列挙型の定義値と状態値の設計調整で実現します。

CASE State OF
	10: // A function.
	...
	19: // Done A.
	
	20: // B function.
	...
	29: // Done B.
	...
END_CASE;

NumToEnum(In:=(State / 10), InOut:=OutState);
節の順序

CASE文の節は選択値の大小とは無関係な順序で配置することができますが、昇順で並べましょう。多くのシステムは重要度の高低について、値が小さいほど優先度が高くなります。前後が入れ替わるような配置も避けます。例えば、状態値にコード設計を施した結果として、何らかの処理の分岐ルートを100、実処理を10010-10019にして、隣接させたとします。コード設計を見直しましょう。構造的な肥大化を生じさせる余地のある設計は初期に見直します。後になるほどリファクタリングのコストが上がります。節の順序に注意を向ける必要がある設計は、何らかの不都合がある可能性を示しています。

ルーチン節

ルーチン節は、CASEステートマシン内で複数回出現する同一の一連の節を分離し、使用節は、その処理が必要になったら制御を移し、処理後に指定の節へと戻す構造です。CASEステートマシンに何度も現れる一連の節があり、機能的に分離しても意味が通る(名前を付けられる)場合、ルーチン節としての分離を検討します。

CASE State OF
	10: // A function.
	    ...
		Inc(State);
	11: // Do something
	12: ...
	13: ...
	14: // A only.
	    ...
	19: // Done A.
	
	20: // B function.
	    ...
		Inc(State);
	21: // Do something
	22: ...
	23: ...
	24: // B only.
	    ...
	29: // Done B.
	...
END_CASE;

"Do something"(11-13, 21-23)をルーチン節にすると以下のようになります。

CASE State OF
	10: // A function.
	    ...
	    ReturnState := State + 1;
		State := 100
	11: // A only.
	    ...
	16: // Done A.
	
	20: // B function.
	    ...
	    ReturnState := State + 1;
		State := 100
	21: // B only.
	    ...
	26: // Done B.
	
	100: // Do something routine.
	101: ...
	102: ...
		State := ReturnState;
END_CASE;

スタックマシンを構築したくなるかもしれませんが、それはやりすぎです。一連の多段遷移のためにスタックを使用することはあります。

ルーチンCASEステートマシン

ルーチンCASEステートマシンは、POU内にある主CASEステートマシンと並列に記述したCASEステートマシンです。ルーチンCASEステートマシンは、それ自体が完結したステートマシンで、主CASEステートマシンとは独立して動作します。主CASEステートマシンの処理に優先して強制介入する必要がある機能、主CASEステートマシンの状態遷移監視、履歴的で多段の状態遷移を必要とする外部信号モニタ、並列IO処理等の高度な機能を実現するために使用します。多くの場合、FBとして分離、隠蔽し、主CASEステートマシンはFBの実行状態管理と出力に関心を払います。

CASE State OF
	1:
	...
	10:
		RoutineAState := 1;
		...
	15:
		IF RoutineAState = 5 THEN
			...
			Inc(State);
		END_IF;
END_CASE;

CASE RoutineAState OF
	1:
		RoutineA.Execute := False;
		Inc(RoutineAState);
	2:
		RoutineA.Execute := True;
		Inc(RoutineAState);
		...
	5: // Done the RoutineA.
END_CASE;
RoutineA();
内部CASEステートマシン

内部CASEステートマシンは、POUの主CASEステートマシンのある節に記述したCASEステートマシンです。見かけ上、入れ子のCASE文です。何らかのパーサーを構築する場合に、構造的にできることがあります。制御用途(例えば、内部状態を明示して制御を記述)で使用することは避けましょう。可読性の維持と変数の誤用防止を考えるとFUNやFBに分離して隠蔽することが必要で、よほどの規模でなければ、開発者に負荷をかけるだけです。内部状態は状態値での表現を検討します。

CASE State OF
	1:
	...
	10:
		CASE ch OF
			16#0A: ...
			16#0D: ...
		END_CASE;
		...
	11:
		...
END_CASE;
デフォルト節

デフォルト節を設けることはしません。よって状態遷移に不測があれば、HALTします。そもそもそのような状態になる可能性のあるCASEステートマシンをリリースしてはいけません。動的に状態値を生成する場合、生成しうる全ての値についてテストを実施します。不測の値は、実行時エラーではなく、欠陥です。また、状態値がランダム値や実行環境のシステム値(時刻など)に依存する場合、できる限り外部から与える作りにします。これらも十分なテストを必要とします。

状態変数の初期値

状態変数の初期値は必ず設定します。状態値がゼロの節があるとき、初期値が未定(不定)だとコントローラの動作開始(初回サイクルから)と同時に意図せず動作します。特定の条件が成立した時点から動作させる場合、初期値はそのCASEステートマシンのどの状態値とも異なる値にします。初回サイクルから動作することを意図している場合でも、明示的にゼロを初期値として設定します。

まとめ

STのHowとしてCASEステートマシンがあるということは示せたと考えています。次は、CASEステートマシンを使用した実装例を示す必要があります。STは言語、実行環境の制約が多いため、一次的なHowの選択肢が多くありません。しかしながら、一次的なHowとしてCASEステートマシンを使用することで、二次的に豊かなHowを展開する場ができます。それを含め、シミュレータだけで実行でき、LDが不得手とするものにしましょう。LDで十分なものをSTに置換しても訴求するものがありません。LDが不得手とするものであれば、STが訴求するものもあるでしょう。

脚注
  1. 誰が最初にSTへ持ち込んだかの確認はできていません。たまに見かけますよね、ぐらいです。 ↩︎

Discussion