この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
フリップフロップと聞いて、いきなりRubyやPerlの話を始める人はいないと思います が、今回の記事はRubyのフリップフロップのお話です。 論理回路のフリップフロップもRubyのフリップフロップも一度に確認できちゃうお得な記事にしたいと思います。
はじめに
みなさん初めまして、新卒エンジニアのくろすです。
今回の話、決してFlip Flopやフリップフラッパーズ、RWBYの話ではございませんのでご注意を。
個人的には3Dモデリングとかやってる人にRWBYの話とかして欲しかったりしますが。
そもそも フリップフロップ とは
flipflop とは (洗濯物・旗・サンダルなど)パタパタなる音、とんぼ返り、宙返り、(意見などの)急変、豹変(ひょうへん)、フリップフラップ [出典:Weblio]
なんのこっちゃわかりませんね。
とりあえず今回は普通の人(?)ならまず思い浮かべるであろうフリップフロップ回路の話から始めたいと思います。
要するにフリップフロップを使うと電源がある間は情報の保持が可能であるって話。
いきなり回路の話ですので、興味ない方は読み飛ばしておいていただけると。
メモリ って?
まずはエンジニアでない方でも聞いたことがあるであろうメモリの話から始めたいと思います。
メモリとはなんぞ?って質問にはとりあえずパソコンに入ってる4GBとか8GBとかの方のやつ。
くらいに思っておいてもらえればいいかと思います。
ざっくり言うとメモリには2通りの種類があり揮発性メモリと不揮発性メモリに分けることが可能です。
- 不揮発性メモリ 電源の供給がなくても情報の保持が可能であるメモリ
- 揮発性メモリ 電源の供給がないと情報の保持ができないメモリ
この揮発性メモリにはまた種類がありその中にはDRAMとSRAMと呼ばれる種類のものがあります。
DRAMは今回の話にあまり関係ないのでざっくりとしか紹介しませんが、
DRAMとはDynamic Random Access Memoryの略でコンデンサに電荷を蓄え、その電荷の有無で1bitの情報を記憶する半導体メモリです。この電荷をリフレッシュと言う作業で蓄え続けることによって情報を維持しています。
よくパソコンに刺すメモリなんかはこちらが多いかと思います。
そしてSRAMですがDRAMと比較して、回路が複雑でコストが高いがリフレッシュ作業がない分高速で消費電力が小さいと言う特徴を持っています。
SRAMとはStatic Random Access Memoryの略で、リフレッシュ作業がないことからStaticとの名前がついています。
フリップフロップ
さてこのSRAMの記憶部にはフリップフロップ回路というものが使われています。
ようやく出ましたね、フリップフロップくん。
最も基本的なフリップフロップはRSFFと呼ばれるもので以下の回路図で表されます。
Sがセット、Rがリセットで各入力に対する結果は以下のようになります。
S | R | Q | ¬Q |
---|---|---|---|
0 | 0 | 前の状態を保持 | 前の状態を保持 |
0 | 1 | 0 | 1 |
1 | 0 | 1 | 0 |
1 | 1 | 禁止 | 禁止 |
上の結果を見てもらえればわかりますが、セットSが立ち上がると無条件で出力は1に、リセットRが立ち上がると無条件で出力は0になります。出力の状態により(0, 0)を入力した際の状態が変わりますが、出力は直前の出力を保持します。
これがフリップフロップ回路が1bitの情報を記憶するメカニズムです。
一瞬の電気信号を回路自身がデータとして保持し続けるということなのですね。
※ ちなみにSRFFで(1, 1)入力は禁止です。これは入力を(1, 1)から(0, 0)に変化させた場合に、出力が(1, 0)で安定するか(0, 1)で安定するかが定まってないため禁止となっています。
Rubyのフリップフロップ
文字数稼ぎのはずが筆が乗りました。
ようやく本題です、むしろ本題の方がしょっぱい気さえします。
とりあえず、以下のコードを見てください。
string = "これからもよかったらかくりよの門をよろしくお願いします"
string.each_char do |char|
if (char == 'よ')..(char == 'よ')
print char
end
end
このコード実行するとこんな結果が帰ってきます。
よよよ
まぁ、見た感じそうなるでしょうって結果ですね。
次に上のコードそっくりだけどドットの数が違うだけの以下のコードを実行してみます。
string = "これからもよかったらかくりよの門をよろしくお願いします"
string.each_char do |char|
if (char == 'よ')...(char == 'よ')
print char
end
end
すると結果はこうなります
よかったらかくりよよろしくお願いします
ややっ、これは由々しき事態です。
どうしてこうなるのか、というのはだいたいリファレンスに書いてありますが説明していきます。
範囲式とは
リファレンスを読む限りではRangeクラスのインスタンスを生成する式のことを範囲式と言っているように思えます。
(a) 式1 .. 式2
(b) 式1 ... 式2
条件式の外側で使うと式1から式2までの範囲を表すオブジェクトを返してくれます。
また、(b)の書き方をすると範囲オブジェクトは式2の値を含みません。
Rangeクラスのリファレンスを見てもらえるとわかりますが、上の2つはRangeクラスのインスタンスとして宣言することも可能です。
(a) Range.new(式1, 式2, false) # default値がfalseなので実際はfalseいらない
(b) Range.new(式1, 式2, true )
ここで覚えておいて欲しいのは(b)式の場合は範囲オブジェクトが終端を含まないという点です。
条件式として範囲式を使う
さて、今回問題としているのは条件式として範囲式を使った場合です。
先に説明した、フリップフロップ回路の特性である「回路自身が状態を記憶する」ことと、範囲オブジェクトの終端の話を念頭に置くと理解しやすいかと思います。
少なくとも私は理解しやすかったです。
先ほども登場した範囲式についてのリファレンスには以下のように書いてあります。
「..」の場合:
1. 初期状態では式1だけを評価し、式1が真を返すまでは false を返します。
2. 式1が真を返すと true を返します。式2が真なら初期状態に戻ります。
3. この後は式2だけを評価し、式2が真を返すまで true を返します。
4. 式2が真を返すと true を返したあと、初期状態に戻ります。「…」の場合:
1. 初期状態では式1だけを評価し、式1が真を返すまでは false を返します。
2. 式1が真を返すと true を返します。
3. この後は式2だけを評価し、式2が真を返すまで true を返します。
4. 式2が真を返すと true を返したあと、初期状態に戻ります。
つまりこの2つの違いは式1が真を返した時
に式2をその場で評価するか
にあります。
(b)式の場合
は終端である式2をその場で評価しないのですね。
そして、式2がtrueを返すまでの間は範囲式自身がtrueであるという状態を記憶してtrueを返します。
RSFFでいうなら、式1が入力S、式2が入力Rと考えながら見ると入力が(1, 0)
Rubyだとnilとfalse以外は全てtrue
なのでこれはマズイですね。
入力が(true, *)
の時に出力はtrue
となり範囲式としてはtrueを維持
するようになります。
次からは式2を使い値を評価した結果、入力が(*, true)
ならリセットが立ち上がり、
その場では範囲式の出力としてtrueを返した後
に、範囲式の出力をfalseとして保持する
。
と言った具合です。
まとめ
(a) 式1 .. 式2
(b) 式1 ... 式2
今回の話は結局RSFFで禁止されていた(1, 1)入力、つまり(true, true)に対する出力とその時保持する値をどう定義したか?
っていうのに繋がる話なんですよね。
- (a)式の場合 trueを返しfasleを保持している だから次に評価されるのはセットである式1
- (b)式の場合 trueを返しtrueを保持している だから次に評価されるのはリセットである式2
以上のように定義されているのがrubyのフリップフロップというわけです。
ここまで読んでいただけたら、フリップフロップからRubyを連想できるようになり、かつ「式」という漢字がゲシュタルト崩壊を起こしていることでしょう。