Podcastの音源を編集するときにやっていること

2021年10月からPodcast てくてくラジオ • A podcast on Anchor をやっています。

自分はエピソード番号が奇数の回の編集をやっていて、エピソード17 から音源を編集し始めたので、どんな感じでやっているかを記録しておきます。(2週間に一度しか作業しないから、作業方法を忘れてしまう。自分のためにかきます)

前提

  • Podcastのパーソナリティは二人です(二人分の音源を編集します)
  • 収録はZoomの録画機能を使っています
  • 編集はGarageBandを使っています

事前準備

Zoomの設定

Zoomの「設定 > レコーディング > 参加者ごとに個別のオーディオファイルにレコーディング」をONにします。

f:id:tanaken0515:20220313143518p:plain

編集の際に二人の声の大きさを調整したいので、この設定をしています。

BGMを探す

フリー音源をダウンロードできるWebサイトからBGMを探しておきます。

直近ではこのBGMを使っています。

収録

Zoomミーティングの録画機能でおしゃべりする様子を録画します。

ミーティングを終了すると、このようなファイルができます。

f:id:tanaken0515:20220313151558p:plain:w312

編集

GarageBandでの作業です。

二人の音源と1曲目BGMを貼り付ける

音声ファイルをドラッグ&ドロップで貼り付けます。

f:id:tanaken0515:20220313151826p:plain:w312

音量をざっくり調整する

聴きやすくなるように調整します。

てくてくラジオでは、相対的に自分の声が聞き取りづらい印象がある(声が低いから?それとも小さい?)ので、雑にボリュームを +3.0dB しています。

f:id:tanaken0515:20220313152501p:plain:w312

また、BGMは大きすぎるのでいったん -35.0dB に設定します。

会話音声の冒頭をカットし、BGMに合わせる

録画ボタンを押してから話を始めるまでに余計な音声があるので、まずはそれをカットします。

shift キーを押しながら二人の会話音源をアクティブにして、

f:id:tanaken0515:20220313153451p:plain:w312

カットしたい位置に再生ヘッドを移動した状態で command + t をし、

f:id:tanaken0515:20220313153455p:plain:w312

不要になった部分を消します。

f:id:tanaken0515:20220313153500p:plain:w312

その後、BGMの良きタイミングに合わせて会話が始まるように、会話音源の開始位置を左右に調整します。

f:id:tanaken0515:20220313154135p:plain:w312

BGMの音量を冒頭だけ大きくする

BGMの冒頭だけ大きくして、会話が始まる頃には小さくなるように設定します。

GarageBandの「オートメーション」機能をONにします。

f:id:tanaken0515:20220313154858p:plain:w312

これをすると、音声トラックに黄色い横線が表示されます。

f:id:tanaken0515:20220313155130p:plain

この黄色い線上の任意の箇所をクリックすると黄色い丸ぽちができ、それを上下に動かすことでボリュームを調整することができます。

f:id:tanaken0515:20220313160033p:plain

最初は -10.0dB で、会話が始まる直前にぐっと下げて -35.0dB になるように設定しています。

BGMの繰り返しを設定する

BGMのトラックを右クリックして、ループをONに設定します。

f:id:tanaken0515:20220313162143p:plain

話題の転換のタイミングで、2曲目のBGMに切り替える

この作業はこれまでやってきた作業を組み合わせて実現します。

  1. 話題の転換点で会話音声をカットする
  2. 1曲目のBGMのボリュームを0にする
  3. 2曲目のBGMのトラックを追加する
  4. BGMの冒頭が大きく、会話の開始時には小さくなるように調整する
  5. 会話の開始位置を調整する
  6. BGMの繰り返しを設定する

f:id:tanaken0515:20220313164657p:plain

会話音声の最後をカットし、BGMをフェードアウトする

f:id:tanaken0515:20220313171629p:plain

おわりに

だいたいこんな感じで作業しています。

作業時間は「会話音声の時間 + 10~15分」くらいです。

GarageBandに詳しい方がいたら「もっとこうやると良いよ〜とか作業がラクになるよ〜」などのアドバイスをいただけると嬉しいです。 (特に「オートメーション」機能を使っているのに手動でポチポチと音量を調整しており、「"オートメーション"という機能名からして自動で音量をいい感じ調整してくれる機能なのでは...?使い方が違うのか...?」と疑っています。)

ではまた。

GoogleスプレッドシートでFILTERした範囲を結合する

またGoogleスプレッドシートの話です。

このような2つのテーブルがあるとします。 f:id:tanaken0515:20220228224029p:plain

これらのテーブルをもとに

  • num が奇数の行だけを抜き出して縦方向に結合したテーブルA
  • num が偶数の行だけを抜き出して縦方向に結合したテーブルB

を作りたいとしましょう。

f:id:tanaken0515:20220228224059p:plain

結論ファースト

これでいけます。

テーブルA

=FILTER(
  {
    IFNA(FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=1),{"",""});
    IFNA(FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=1),{"",""})
  },
  {
    IFNA(FILTER($A$3:$A$5,MOD($A$3:$A$5,2)=1),{""});
    IFNA(FILTER($D$3:$D$5,MOD($D$3:$D$5,2)=1),{""})
  }<>""
)

テーブルB

=FILTER(
  {
    IFNA(FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=0),{"",""});
    IFNA(FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=0),{"",""})
  },
  {
    IFNA(FILTER($A$3:$A$5,MOD($A$3:$A$5,2)=0),{""});
    IFNA(FILTER($D$3:$D$5,MOD($D$3:$D$5,2)=0),{""})
  }<>""
)

解説

テーブルA

テーブルAを作る手順をやっていきましょう。

まずはテーブル1から奇数の行だけを抜き出してみます。FILTERを使えば簡単です。

=FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=1)

f:id:tanaken0515:20220228225242p:plain

同じ要領でテーブル2からも奇数の行だけを抜き出してみましょう。

=FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=1)

f:id:tanaken0515:20220228225508p:plain

この2つを縦方向に結合しましょう!

縦方向の結合は ={範囲;範囲} という式で書くことができます。

={
  FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=1);
  FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=1)
}

f:id:tanaken0515:20220228225815p:plain

お〜、できました!(ほんとか?)

テーブルB

この勢いで、テーブルBも作っていきましょう。 テーブルBは偶数の行を抜き出したいので、↑の式を MOD(ほげ,2) = 0 で書き換えれば良いはずですね。

={
  FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=0);
  FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=0)
}

f:id:tanaken0515:20220228230243p:plain

あら〜?エラーになってしまいました。

それもそのはず、FILTER関数は条件に一致する行が見つからないとき #N/A を返します。 ref: FILTER - ドキュメント エディタ ヘルプ

今回はテーブル1に偶数の行がないので、

={
  FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=0);  <- ここで #N/A エラーになる
  FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=0)
}

という状況です。

配列の #N/A エラーを回避する

#N/A エラーを回避する方法としてIFNA関数があります。

https://support.google.com/docs/answer/9365944?hl=ja IFNA 関数 - ドキュメント エディタ ヘルプ

IFNA関数の第一引数には「#N/A エラーになる可能性がある値」を、第二引数には「第一引数が #N/A だった場合に代わりに表示する値」を指定します。

つまり、今回の場合では

={
  IFNA(FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=0),ほげ);
  IFNA(FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=0),ふが)
}

という感じにしてあげれば、テーブル1またはテーブル2に偶数の行がなくても #N/A エラーは発生しなくなる、ということですね!

さて、次に問題となるのは、↑の ほげふが になにを入れればいいの?ということです。

いまIFNA関数の第一引数に渡している FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=0)FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=0) は、どちらもN行2列の配列です(それぞれ条件を満たす行があれば)。

このN行2列の配列を縦方向に結合しようとしているわけです。

複数の配列を縦方向に結合するとき、列の数が一致している必要があります。

つまり、 #N/A エラーになった時も常に同じ列数の配列を返すようにしてあげれば良い、ということですね。

今回は1行2列の空文字列の配列 {"",""} を返すことにしましょう。

={
  IFNA(FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=0),{"",""});
  IFNA(FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=0),{"",""})
}

f:id:tanaken0515:20220228233450p:plain

お〜、ここまできました。

無駄な空行を無くす

↑の画像は、テーブル1に偶数の行がないために1行2列の空文字列の配列 {"",""} が表示されていて、不要な空行が残ってしまっています。

最後にまたFILTER関数を使って

=FILTER(
  該当の範囲, 
  該当の範囲の1列目<>""
)

という式を作れば完了です。

これを書き換えると、こうなります。

=FILTER(
  {
    IFNA(FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=0),{"",""});
    IFNA(FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=0),{"",""})
  },
  {
    IFNA(FILTER($A$3:$A$5,MOD($A$3:$A$5,2)=0),{""});
    IFNA(FILTER($D$3:$D$5,MOD($D$3:$D$5,2)=0),{""})
  }<>""
)

はい、これが「結論ファースト」で示したテーブルBの式ですね!

改めてテーブルA

さて、テーブルAはテーブルBの奇数版なのでMODのところだけ書き換えて

=FILTER(
  {
    IFNA(FILTER($A$3:$B$5,MOD($A$3:$A$5,2)=1),{"",""});
    IFNA(FILTER($D$3:$E$5,MOD($D$3:$D$5,2)=1),{"",""})
  },
  {
    IFNA(FILTER($A$3:$A$5,MOD($A$3:$A$5,2)=1),{""});
    IFNA(FILTER($D$3:$D$5,MOD($D$3:$D$5,2)=1),{""})
  }<>""
)

ですね!

おわりに

GoogleスプレッドシートでFILTERした範囲を結合する方法をまとめました。

「配列の #N/A エラーを回避する」というのがミソですね。

ではまた!

Googleスプレッドシートで FULL OUTER JOIN っぽい挙動を実現する

Google スプレッドシートで2つの表を突合したいシーンがありました。

具体的には

  • 特定の列をkeyとして、一方に存在してもう一方に存在しない行はどれか
  • keyが同じ行は両方の表に存在するが、別の列の値が異なっている行はどれか

を知りたい、というシーンです。

FULL OUTER JOIN (完全外部結合) のようなことができればそれらを簡単に調べることができるのですが、Google スプレッドシートにその機能はないのでそれっぽい挙動を実現しようと試みました。

結論

以下の手順で実現できました。

  1. 結合したい各表のkeyとなる列の範囲を指定する
  2. 縦方向の範囲結合 {範囲1;範囲2} でkeyとなる列を縦に並べる
  3. UNIQUE を使ってkeyの重複を排除する
  4. keyの各行に対して、結合したい各表から VLOOKUP で必要な値を取得する

FULL OUTER JOIN (完全外部結合) とは

いつもお仕事でお世話になっている PostgreSQL のドキュメントに基づいて説明します。

https://www.postgresql.jp/document/13/html/queries-table-expressions.html

「限定的な結合」というセクションにある通り、テーブル結合の構文は次の通りです。

T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 ON boolean_expression

T1 , T2 は結合対象のテーブルのことですね。

これを FULL OUTER JOIN に絞って書き換えておくとこうです。

T1 FULL [OUTER] JOIN T2 ON boolean_expression

PostgreSQL の構文ではこの OUTER は省略可能です。

FULL OUTER JOIN については次のように説明されています。

まず、内部結合が行われます。 その後、T2のどの行の結合条件も満たさないT1の各行については、T2の列をNULL値として結合行が追加されます。 さらに、T1のどの行でも結合条件を満たさないT2の各行に対して、T1の列をNULL値として結合行が追加されます。

具体例を見てみましょう。ドキュメントの具体例をそのまま使います。

テーブル t1

num | name
-----+------
   1 | a
   2 | b
   3 | c

テーブル t2

num | value
-----+-------
   1 | xxx
   3 | yyy
   5 | zzz

を想定して、完全外部結合をすると

SELECT * FROM t1 FULL JOIN t2 ON t1.num = t2.num;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   2 | b    |     |
   3 | c    |   3 | yyy
     |      |   5 | zzz
(4 rows)

このような結果を得られます。

Google スプレッドシートで実現する

テーブル t1, t2 を用意しました。 f:id:tanaken0515:20220130164634p:plain

次の4ステップで完全外部結合を実現します。

  1. 結合したい各表のkeyとなる列の範囲を指定する
  2. 縦方向の範囲結合 {範囲1;範囲2} でkeyとなる列を縦に並べる
  3. UNIQUE を使ってkeyの重複を排除する
  4. keyの各行に対して、結合したい各表から VLOOKUP で必要な値を取得する

1. 結合したい各表のkeyとなる列の範囲を指定する

各テーブルの num 列の値をkeyとして完全外部結合をしたいわけですね。

それぞれの範囲は $A$3:$A$5, $D$3:$D$5 です。

2. 縦方向の範囲結合 {範囲1;範囲2} でkeyとなる列を縦に並べる

まずはすべての num を洗い出すために {範囲1;範囲2} という記法を使って num を縦方向に並べます。 f:id:tanaken0515:20220130170304p:plain

ちなみに{範囲1;範囲2} という記法については Google スプレッドシートで配列を使用する - ドキュメント エディタ ヘルプ に記載があります。

複数の範囲を単一の連続する範囲に結合できます。たとえば、A1〜A10 の値を D1〜D10 の値と統合するには、={A1:A10; D1:D10} の数式を使って連続した列に範囲を作成します。

3. UNIQUE を使ってkeyの重複を排除する

num の値に重複があるので UNIQUE 関数を使って重複を排除します。

f:id:tanaken0515:20220130180342p:plain

ref: UNIQUE 関数 - ドキュメント エディタ ヘルプ

4. keyの各行に対して、結合したい各表から VLOOKUP で必要な値を取得する

あとは各行のそれぞれの列の値を次のような式で取得するだけです。

  • t1.num: VLOOKUP($G3,$A$3:$B$5,1,FALSE)
  • t1.value: VLOOKUP($G3,$A$3:$B$5,2,FALSE)
  • t2.num: VLOOKUP($G3,$D$3:$E$5,1,FALSE)
  • t2.value: VLOOKUP($G3,$D$3:$E$5,2,FALSE)

結果はこちら

f:id:tanaken0515:20220130180956p:plain

まとめ

Google スプレッドシートで FULL OUTER JOIN っぽい挙動を実現する方法について書きました。

Google Apps Scriptで実装する方法もありますが、関数の組み合わせで実現する手段を知っておくと幅広いひとに活用してもらえそうだと思ったので記事にまとめることにしました。

この方法を模索する過程で範囲の結合をする記法があることを知れて、これは他にも色々と活用できそうなのでお得でした。

最近スプレッドシートをいじることが多いので、また何か見つけたら記事書こうと思います。ではまた。

「ステンレスフードスタンド」1周年記念日 #minneで買ってよかったもの

これは minne #買ってよかったもの Advent Calendar 2021 - Adventar の8日目の記事です。

こんにちはtanakenです。普段はminneを運営しているGMOペパボでSUZURIというサービスを開発しています。

minneの運営スタッフではないのですが、minneで買ってよかったものがあるのでAdvent Calendarの一枠をいただいて記事を書きます🖋

僕のminneで買ってよかったものはこれです。

minne.com

ちょうど1年前の2020年12月8日に使い始めました。今日は1周年記念日です🎂

Neko no OTAKUさんの作品で、さまざまなサイズ・用途のフードスタンドやステンレス製の作品が販売されています。

我が家の食事シーンを支えるフードスタンド
我が家の食事シーンを支えるフードスタンド

我が家のやんちゃな愛犬がガツガツと食事をしても、このフードスタンドは倒れたりすることなくしっかりと安定して支えてくれます。

以前はフードボウルを床に置いて食事をあげていましたが、身体が大きくなってきた愛犬にとっては位置が低すぎて食べづらいようでした。 それを見兼ねた妻が、この作品を見つけてくれました。

使ってみたところ、高さがちょうど良くて食べづらさは解消され、今では毎日の朝晩の食事と昼間の水分補給をこのフードスタンドが支えてくれていて、我が家の生活には欠かせないものとなっています。

これはフードスタンドを使って水を飲んでいる様子を撮影した動画です。顔が写っていてかわいいです。

1年間使い続けても劣化は見られず、まだまだ長く利用できそうです。

Neko no OTAKUさん、素敵な作品を作ってくださってありがとうございます。

来年もminneで良いものを買って生活を豊かにしていくぞ〜。

おしまい。

ペパボのCREとCSのイベントを開催しました

このイベントを開催しました。

pepabo.connpass.com

去年からやりたいなと思っていて、今年の8月末に改めて「やりたい!」と声を上げて準備を進めていきました。

元々はCREだけのイベントにするつもりだったのですが、最近の弊社CS室はテックな取り組みをたくさんやっていて面白いし、CTOのあんちぽ(@kentaro)さんがCS室の管掌役員になったことで、さらにテックに&面白くなっていくだろうと感じたので、CS室のメンバーと合同のイベントにすることにしました。

勢いだけで「やりたい!」の宣言をすると、人事のあちゃ(@achamixx)さんが様々な段取りを進めてくれて、めちゃくちゃありがたかったです。本当にありがとうございます。(3年前にも、あちゃさんの "のっていき" にめちゃくちゃ助けられたことを思い出しました。本当にありがたい...)

また、発表してくださったみなさん(@_ave_hさん、@3or9cakeさん、@__m5iさん、@Fendo181さん、@ot0m1さん)も、粛々と準備を進めてくださり、すてきな発表をしてくださいました。ありがとうございます。

特に@3or9cakeさんは、こういったイベントでの発表が初めてだったそうですが、そんなことを感じさせない、参加者の興味をそそる発表でした。 ご自身でふりかえりの記事を書かれていましたので下に貼っておきます。

自分は最初にSUZURIのCREチームの話、最後にペパボ全体のCRE/CSについての話、の計2回の発表をしました。

自分自身の最近のお仕事は、サービスの会計周りのことがほとんどで、ユーザに直接的に「安心して気持ちよく使えるSUZURI」を感じてもらうための取り組みはできていません。

「会計周りを整えることで組織としての信頼性を高め、間接的にユーザとの信頼性を高めている」と捉えることはできますが、やっぱり「この仕事の価値は...?」と思ってしまう瞬間は時々あります。

そんな中でも、こういったイベントを通じて自分のやってきたことややろうとしていることを再認識して、少しずつであっても活動を継続していくことで、自分自身や自分の周りの誰かに対して何かを残すことができたら良いなと思っています。

イベントの開催に携わったみなさま、そして、ご参加いただいたみなさま、本当にありがとうございました。

日記を書きはじめて1年経った

日記を書きはじめて1年経ったのでふりかえりをします。

最近はじめたPodcastでもこの話をしたので聞いてくれたら嬉しいです。

書き始めた理由

2020年10月11日に日記をはじめました。

2020年の7~10月くらいは公私共にとても忙しくしていて、自分自身のことに向き合う時間が取れていないように感じていました。

日記を書くことで、自分の考えを文章にして自分自身と向き合うきっかけづくりをしたかったのだと思います。

そんな思いはこのページに書き出してありました。

最近考えてること2020-10 - tanaken0515

書くにあたって工夫したこと

日記を書くにあたって最も優先したことは「書き続けること」です。

それを実現するためにいくつか工夫をしました。

1. タイトルをつけない

日記のページのタイトルは日付だけにしました。 タイトルを考えようとすると、それだけでも少しだけハードルが上がってしまうと考えたからです。

2. 毎朝、その日の日記のリンクをSlackに通知する

Rubyのスクリプトを書いて、GitHub Actionsを使って毎日Slackに通知を送るようにしました。

Scrapboxには /ページ名?body=hogehoge という形式でリンクを開くと、 body に渡した値を本文としてページを作成する機能があります。(詳しくはページを作る - Scrapbox ヘルプを見てください)

この機能を活用して、ページタイトルとしての日付とタグなどの本文を入れたリンクを作って通知しています。

f:id:tanaken0515:20211031203421p:plain

3. リンクを開きたくなるような工夫をする

これは細かい話ですが、

日付系のタグをつける(祝日と祝日名のタグもつける)

f:id:tanaken0515:20211031205359p:plain

Yahoo!きっず今日は何の日 のリンクをつける

f:id:tanaken0515:20211031205807p:plain

などをやっています。

日付のリンクがつながっていくのは楽しいですし、今日は何の日か知るのも楽しいのでリンクを開くきっかけになっています。

4. 書かない日があっても気にしない

最後に、心構え的な話なんですが、書かない日があっても気にしないようにしてます。

「書き続けること」を優先してますが、それは「絶対毎日欠かさずに書くこと」ではないです。

1年とか2年とか、そういうスケールで見たときに「だいたいほぼほぼ毎日書いてる」という状態を目指しています。

なので、書かない日があっても気にしません。実際にこの1年で書いていない日(ページすらつくっていない日)もあると思います。調べてないけれど。

自分に向き合う時間が作れていればいいので、毎日書くこと自体はどっちでもいい、というスタンスです。

1年間継続しての感想

やったこと、思ったこと、などの記録が残っていて楽しいです。

何かをはじめたりやめたりした日が明確に分かるのも面白いですね。

今後

工夫のところに「1. タイトルをつけない」を挙げたんですが、2年目を迎えたし、タイトルをつける良さもあるので、つけようかな?どうしようかな?と揺れています。

あとはGitHub Actionsでの通知の仕組みを雑に作ってある(毎回bundle installしてて優しくない、など)ので、もうちょい良い感じにするなどしたいです。

Enumerable#all? と Enumerable#any? の実装を読んだ

約3年前にEnumerable#all? が罠っぽかった - tanaken’s blogという記事を書いたのですが、Rubyの実装には踏み込みませんでした。最近気になってRubyの実装を読んだので、その結果を書いておきます。

Enumerable#all? / Enumerable#any? とは

Enumerable#all? (Ruby 3.0.0 リファレンスマニュアル)

すべての要素が真である場合に true を返します。偽である要素があれば、ただちに false を返します。

Enumerable#any? (Ruby 3.0.0 リファレンスマニュアル)

すべての要素が偽である場合に false を返します。真である要素があれば、ただちに true を返します。

Enumerableなオブジェクトに対して、すべての要素が真であるかどうかを調べたい時は all? を、どれか一つでも真の要素があるかを調べたい場合は any? を使うことができます。 個人的な経験では業務コードでも何度かお世話になっているメソッドたちです。

[3, 6, 9].all? {|v| v % 3 == 0 } # => true (すべての要素が3の倍数)

[1, 2, 3].all? {|v| v % 3 == 0 } # => false (3の倍数は3だけ)

[1, 2, 3].any? {|v| v % 3 == 0 } # => true

便利なメソッドですが、要素が空のオブジェクトに対しての挙動に注意が必要です。

[].all? {|v| v % 3 == 0 } # => true

[].any? {|v| v % 3 == 0 } # => false

これを日本語で表現すると

  • 「空配列の要素はすべて3の倍数ですか?」「はい
  • 「空配列の要素のうちどれか1つは3の倍数ですか?」「いいえ

というように読めてしまいます。

個人的な感覚では、一つ目の問いに対して「いいえ」と答えるのが正しいように感じます。

自身の感覚と実際の挙動とのギャップがあったので "罠っぽかった" と題して書いたのが冒頭に紹介した記事です。

今回の記事について

Enumerable#all?Enumerable#any? の挙動がどのような実装によって実現されているのかを知ることがゴールです。

次の4項目について書いていきます。

  • 読むべきコードにたどり着くまでの道のり
  • 雰囲気コードリーディング
  • Enumerable#all? の実装について
  • Enumerable#any? の実装について

読むべきコードにたどり着くまでの道のり

さて、どういう実装になっているか知りたい、という状況なので、https://github.com/ruby/rubyのコードを読めばいいわけです。

が、さっそく迷子になりますよね。普段からruby/rubyのコードを読んでいるわけでもないし、C言語に精通しているわけでもないのでさっぱりです。

そんな時にRubyリファレンスマニュアルが便利です。

Enumerable#all? (Ruby 3.0.0 リファレンスマニュアル)の右上に [rdoc] というリンクがあるのでそれを開きます。

f:id:tanaken0515:20210926174234p:plain

このページ https://docs.ruby-lang.org/en/3.0.0/Enumerable.html#method-i-all-3F が開くはずです。 この画面でメソッド名にマウスカーソルを持っていくと、次のキャプチャのように click to toggle source という表示が出てきます。

f:id:tanaken0515:20210926174523p:plain

クリックすると実装コードをみることができます。

static VALUE
enum_all(int argc, VALUE *argv, VALUE obj)
{
    struct MEMO *memo = MEMO_ENUM_NEW(Qtrue);
    WARN_UNUSED_BLOCK(argc);
    rb_block_call(obj, id_each, 0, 0, ENUMFUNC(all), (VALUE)memo);
    return memo->v1;
}

これだけでもざっくり実装内容が分かります。

さらに、https://github.com/ruby/rubyでどのファイルを読めばよさそうか知りたければ、この実装を元にキーワード検索するとだいたいわかるんじゃないかと思います。

今回は enum_all で検索してみます。

https://github.com/ruby/ruby/search?q=enum_all

f:id:tanaken0515:20210926175158p:plain

enum.c で実装されていることが分かりました。

こんな感じで読むべきコードにたどり着くことができました。

雰囲気コードリーディング

C言語を雰囲気で読んでいきます。(筆者のC言語への理解度は、10年以上前に大学の講義でC言語やったことあるような無いような気がする程度です)

まずは click to toggle source で出てきた箇所のコードを読んでいきましょう。

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1530-L1537

static VALUE
enum_all(int argc, VALUE *argv, VALUE obj)
{
    struct MEMO *memo = MEMO_ENUM_NEW(Qtrue);
    WARN_UNUSED_BLOCK(argc);
    rb_block_call(obj, id_each, 0, 0, ENUMFUNC(all), (VALUE)memo);
    return memo->v1;
}

4行目の struct MEMO *memo = MEMO_ENUM_NEW(Qtrue); からは「true的な何かをメモしておくぜ」というような気持ちが窺えますね。

6行目の rb_block_call(obj, id_each, 0, 0, ENUMFUNC(all), (VALUE)memo); からは「いまからrubyのblockを呼び出すぜ?メモの値とENUMFUNC(all)を使ってな!」的な勢いを感じますね。

ENUMFUNC(all) ってなんですか?

enum.c のコードを読んでみると、それっぽい子がいました。

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1462

#define ENUMFUNC(name) argc ? name##_eqq : rb_block_given_p() ? name##_iter_i : name##_i

これの name = all ってことなので、なんとなく

  • argc が真なら all_eqq を使いなさい。そうでないなら次へ。
  • rb_block_given_p() が真なら all_iter_i を使いなさい。そうでないなら all_i を使いなさい。

のような感じでしょうか。

all_eqq / all_iter_i / all_i ってなんですか?

これら3つの実装を雰囲気で読んでみると、

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1466-L1489

#define DEFINE_ENUMFUNCS(name) \
static VALUE enum_##name##_func(VALUE result, struct MEMO *memo); \
\
static VALUE \
name##_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo)) \
{ \
    return enum_##name##_func(rb_enum_values_pack(argc, argv), MEMO_CAST(memo)); \
} \
\
static VALUE \
name##_iter_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo)) \
{ \
    return enum_##name##_func(rb_yield_values2(argc, argv), MEMO_CAST(memo));    \
} \
\
static VALUE \
name##_eqq(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo)) \
{ \
    ENUM_WANT_SVALUE(); \
    return enum_##name##_func(rb_funcallv(MEMO_CAST(memo)->v2, id_eqq, 1, &i), MEMO_CAST(memo)); \
} \
\
static VALUE \
enum_##name##_func(VALUE result, struct MEMO *memo)

とありました。DEFINE_ENUMFUNCS(name)name = all を渡すとall_eqq / all_iter_i / all_i が生まれそうな感じがしてきます。

どこかに DEFINE_ENUMFUNCS(all) を呼んでいる場所があるのでしょうか。

DEFINE_ENUMFUNCS(all)

すぐ下にありました。

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1497-L1504

DEFINE_ENUMFUNCS(all)
{
    if (!RTEST(result)) {
        MEMO_V1_SET(memo, Qfalse);
        rb_iter_break();
    }
    return Qnil;
}

おおよそ読むべきコードの見当をつけることができましたね。

Enumerable#all? の実装について

改めて enum_all のコードを見てみます。

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1530-L1537

static VALUE
enum_all(int argc, VALUE *argv, VALUE obj)
{
    struct MEMO *memo = MEMO_ENUM_NEW(Qtrue);
    WARN_UNUSED_BLOCK(argc);
    rb_block_call(obj, id_each, 0, 0, ENUMFUNC(all), (VALUE)memo);
    return memo->v1;
}

4行目:「true的な何かをメモしておくぜ

6行目:「いまからrubyのblockを呼び出すぜ?メモの値とENUMFUNC(all)を使ってな!

で、ENUMFUNC(all)の実体(?)はDEFINE_ENUMFUNCS(all)で生み出されています。

DEFINE_ENUMFUNCS(all)のコードを読むと、

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1497-L1504

DEFINE_ENUMFUNCS(all)
{
    if (!RTEST(result)) {
        MEMO_V1_SET(memo, Qfalse);
        rb_iter_break();
    }
    return Qnil;
}

もしもRTEST(result)が偽であれば、メモにfalse的な何かをセットしてイテレータをbreakするぜ!」と読めそうです。

これらの内容をまとめると:

  • Enumerable#all?の初期値(?)はtrueである
  • 要素を評価し、結果が偽であるものを発見したらfalseを返す

ということになりますね。

この理解に基づいて、要素が空のケースを考えてみると「要素をひとつも評価していないので、初期値(true)が返ってくる」と解釈することができます。

Enumerable#any? の実装について

読み方は all? の場合と同様です。

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1572-L1579

static VALUE
enum_any(int argc, VALUE *argv, VALUE obj)
{
    struct MEMO *memo = MEMO_ENUM_NEW(Qfalse);
    WARN_UNUSED_BLOCK(argc);
    rb_block_call(obj, id_each, 0, 0, ENUMFUNC(any), (VALUE)memo);
    return memo->v1;
}

4行目:「false的な何かをメモしておくぜ

6行目:「いまからrubyのblockを呼び出すぜ?メモの値とENUMFUNC(any)を使ってな!

そしてENUMFUNC(any)

https://github.com/ruby/ruby/blob/046f1bf492d707465c0fe90ea8bac34746c9455a/enum.c#L1539-L1546

DEFINE_ENUMFUNCS(any)
{
    if (RTEST(result)) {
        MEMO_V1_SET(memo, Qtrue);
        rb_iter_break();
    }
    return Qnil;
}

もしもRTEST(result)が真であれば、メモにtrue的な何かをセットしてイテレータをbreakするぜ!」と読めそうです。

これらの内容をまとめると:

  • Enumerable#any?の初期値(?)はfalseである
  • 要素を評価し、結果が真であるものを発見したらtrueを返す

ということになりますね。

この理解に基づいて、要素が空のケースを考えてみると「要素をひとつも評価していないので、初期値(false)が返ってくる」と解釈することができます。

まとめ

Enumerable#all?Enumerable#any? の実装を雰囲気でコードリーディングしてみました。

C言語の知識がなくてもなんとなく読むことができ、理解が深まった感じがします。

冒頭に書いた空配列に対する呼び出しについて、改めて日本語で表現すると

[].all? {|v| v % 3 == 0 } # => true

[].any? {|v| v % 3 == 0 } # => false
  • 「空配列の要素のうち、3の倍数ではない要素は存在しませんでしたか?」「はい(存在しませんでした(だって要素が1つもないからさ))
  • 「空配列の要素のうちどれか1つは3の倍数ですか?」「いいえ

ということになりますね。なるほどな〜。

おしまい。