RAS Syndrome

冗長。

プログラミングにおける"フック"とは

フックの意味

フックという言葉には色々な意味がある。

  • 鉤。物を引っかける器具。
    • 電話機で受話器を置く部分にあるボタン。フックスイッチを参照。
    • 衣服の留めかぎ。ホック。こはぜ。
    • 釣り針。
    • 義手の手先金具、またはそれのついた義手。装飾義手(ハンドタイプ)ではない方の実用義手(フックタイプ)をいう。両腕義手の場合、複数形hooksとなる。
    • BDSMで、使うフック。鼻フックが有名だが、他にも肛門や陰部に引っ掛けるプレイもある。
  • フック (打撃) - ボクシングなどの攻撃の一種。引っ掛けるように横から打つ。
  • ゴルフで右打者の打球が左に、また左打者の打球が右に大きく曲がること。
  • フック (プログラミング) - コンピュータの処理を割り込ませる(引っかける)こと。
  • つかみ - 芸能で、客を引きつけるためのしかけ。
  • フック (音楽) - 覚え易い音楽の一節。
  • フック - 航空機の機動の種類で、旋回中に急激に機首を旋回円の中心へ向けたまま機体の進行方向を変更しない機動。
  • フックつき文字 - 文字の一部分を伸ばし曲げるようにして付け足した部分。
  • フック符号 - 「?」の下の点を取ったような形のダイアクリティカルマークベトナム語アルファベット(クオック・グー)で母音字の上に付ける。

フック - Wikipedia

共通するのは、「引っかける」という動作に関連したものということだ。

プログラミングにおけるフックの意味

プログラミングに関する部分だけ、もう一度引用する。

  • フック (プログラミング) - コンピュータの処理を割り込ませる(引っかける)こと。

フック - Wikipedia

では、この意味のフックについて掘り下げてみよう。

フック(Hook)は、プログラム中の特定の箇所に、利用者が独自の処理を追加できるようにする仕組みである。また、フックを利用して独自の処理を追加することを「フックする」という。

フック (プログラミング) - Wikipedia

つまり、利用者が独自の処理を"引っかける"こと、または"引っかける"ことができる仕組みのことをフックというわけだ。

念の為、英語版の Wikipedia も確認してみよう。

In computer programming, the term hooking covers a range of techniques used to alter or augment the behaviour of an operating system, of applications, or of other software components by intercepting function calls or messages or events passed between software components.

Hooking - Wikipedia

関数呼出、メッセージング、イベントなどを"引っかける"ことを hooking (動詞:フックする)という。

Code that handles such intercepted function calls, events or messages is called a hook.

Hooking - Wikipedia

"引っかけられる"部分を hook (名詞:フック)というわけだ。
これは誤読だった。よく読むとこの文章は"引っかける"方を hook と呼んでいる。気がする。(どっち側も handle しているとは言えそうなので分かりづらい。しかし、後述の「イベントハンドラ」といった用語を考慮すると、"引っかける"方を指していると考えるのが自然な気がする。)
また、この記事の続きを読んでも "our hook function" 等の表現が見られ、どうも"引っかける"のに用いる処理の方を hook と呼んでいるように感じる。
しかし、他のいろいろな情報をあたってみると、"引っかけられる"方を hook と呼んでいる方が多い感じがする(例えば What is meant by the term "hook" in programming? - Stack Overflow とか)。
なのでこの記事では以降、独自の処理を"引っかけられる"方を hook とする解釈で進めていく。

f:id:ikngtty:20210307170840p:plain

フックのコード例

Wikipedia の例はシステムレベルの話が多い。もっと平凡なコードでフックを使ってみよう。
以下のような動きをするプログラムを Ruby で書く。

f:id:ikngtty:20200606114626p:plain

題材は、インタビューを取り扱うプログラムにしてみた。
プログラムが質問して、ユーザーが答える。それだけのやつ。
なるべく単純なコンソールアプリにしたいと思ったらこうなった。

そんなわけでコードがこれ。

# frozen_string_literal: true

InterviewInfo = Struct.new(:question, :answer, keyword_init: true)

class Interviewer
  def initialize
    @questions = %w[
      調子どうだ?カラダ?
      あなたは赤い部屋が好きですか?
      おまえは今まで食ったパンの枚数をおぼえているのか?
      てかLINEやってる?
      おいィ?お前らは今の言葉聞こえたか?
      何いきなり話かけて来てるわけ?
      質問文に対し質問文で答えるとテスト0点なの知ってたか?
      あなた…『覚悟して来てる人』……ですよね?
      小便は済ませたか?神様にお祈りは?部屋の隅でガタガタふるえて命乞いする心の準備はOK?
      これはミラーシェード=サンのケジメ案件では?
    ]
    @hooked_funcs = []
  end

  # フックに処理を引っかける
  def add_to_hook(func)
    @hooked_funcs << func
  end

  def interview
    @questions.shuffle.take(5).each do |question|
      puts question

      print '> '
      answer = gets
      puts

      info = InterviewInfo.new(question: question, answer: answer)
      call_hooked_funcs(info)
    end
  end

  private

  # フックに引っ掛けられた処理を全部呼ぶ
  def call_hooked_funcs(info)
    @hooked_funcs.each { |f| f.call(info) }
  end
end

# フックに引っかける処理1
# 概要:インタビュー内容を逐次ログファイルに出力。
realtime_report = lambda do |info|
  File.open('realtime.log', 'a') do |file|
    file.puts "Q: #{info.question}"
    file.puts "A: #{info.answer}"
  end
end

# フックに引っかける処理2(store メソッドを引っかける)
# 概要:インタビュー内容を store メソッドで蓄え、report メソッドで一気に出力。
class StoreReporter
  def initialize
    @interview_infoes = []
  end

  def store(info)
    @interview_infoes << info
  end

  def report
    File.open('summary.log', 'a') do |file|
      file.puts '質問まとめ'
      @interview_infoes.each_with_index do |info, i|
        file.puts "#{i + 1}: #{info.question}"
      end

      file.puts '回答まとめ'
      @interview_infoes.each_with_index do |info, i|
        file.puts "#{i + 1}: #{info.answer}"
      end
    end
  end
end
store_reporter = StoreReporter.new

# 準備:2つの処理をフックしておく
interviewer = Interviewer.new
interviewer.add_to_hook(realtime_report)
interviewer.add_to_hook(store_reporter.method(:store))

# インタビューの実行
interviewer.interview

# 処理2によって蓄えたインタビュー内容も出力
store_reporter.report

質問を行うオブジェクトは関数呼出をフックできるようになっており、実際に処理を 2 つフックしている。
ユーザーが 1 回質問に答える度に、フックした処理が 1 回ずつ呼ばれる形だ。

f:id:ikngtty:20200606114351p:plain

f:id:ikngtty:20200606114409p:plain

実行時のコンソールはこんな感じ。

【console】

小便は済ませたか?神様にお祈りは?部屋の隅でガタガタふるえて命乞いする心の準備はOK?
> ミレニ……アム

おいィ?お前らは今の言葉聞こえたか?
> 俺のログには何もないな

てかLINEやってる?
> 笑    

調子どうだ?カラダ?
> 俺が楽天斎

質問文に対し質問文で答えるとテスト0点なの知ってたか?
> マヌケ

出力結果はそれぞれ以下。

【realtime.log】

Q: 小便は済ませたか?神様にお祈りは?部屋の隅でガタガタふるえて命乞いする心の準備はOK?
A: ミレニ……アム
Q: おいィ?お前らは今の言葉聞こえたか?
A: 俺のログには何もないな
Q: てかLINEやってる?
A: 笑
Q: 調子どうだ?カラダ?
A: 俺が楽天斎
Q: 質問文に対し質問文で答えるとテスト0点なの知ってたか?
A: マヌケ
【summary.log】

質問まとめ
1: 小便は済ませたか?神様にお祈りは?部屋の隅でガタガタふるえて命乞いする心の準備はOK?
2: おいィ?お前らは今の言葉聞こえたか?
3: てかLINEやってる?
4: 調子どうだ?カラダ?
5: 質問文に対し質問文で答えるとテスト0点なの知ってたか?
回答まとめ
1: ミレニ……アム
2: 俺のログには何もないな
3: 笑
4: 俺が楽天斎
5: マヌケ

無事、フックした2つの処理が呼ばれていることを確認できた。


話は少し脱線して、Ruby 固有のポイントに少し触れておく。

Ruby では 関数っぽいもの (例えばメソッドや、lambda 構文で生成されるやつ等)を変数に入れることができる。
そして、変数 f関数っぽいもの を入れた時、その 関数っぽいもの の呼び方は f(arg) ではなく、f.call(arg) となる。
なぜかというと、f に入っている 関数っぽいもの は、実際には 関数 ではなく、関数っぽい オブジェクト だからだ。
オブジェクト である以上は、なんらかのメソッドを呼ばない限り使うことはできない、というわけ。

全てがオブジェクトでできているというのは、 Ruby の大きな特徴の一つと言える。

フックとイベント駆動型プログラミングは似ている

コード例

上で書いたプログラムは、「イベント駆動型プログラミング」(日本版 wikipedia)の発想で書かれていると捉えることもできる。

試しに C# の event 構文を使ってプログラムを書き換えてみよう。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Interview
{
    // ビルトインに足りない関数を追加している
    static class Extensions
    {
        private static readonly Random random = new Random();

        public static IOrderedEnumerable<T> Shuffle<T>(this IEnumerable<T> list)
        {
            return list.OrderBy(item => random.Next());
        }

        public static IEnumerable<(T, int)> WithIndex<T>(this IEnumerable<T> list)
        {
            return list.Select((item, index) => (item, index));
        }
    }

    class InterviewFinishedEventArgs : EventArgs
    {
        public string Question { get; set; }
        public string Answer { get; set; }

        public InterviewFinishedEventArgs(string question, string answer)
        {
            Question = question;
            Answer = answer;
        }
    }

    delegate void InterviewFinishedEventHandler(InterviewFinishedEventArgs e);

    class Interviewer
    {
        private readonly string[] questions;
        public event InterviewFinishedEventHandler OnInterviewFinished;

        public Interviewer()
        {
            questions = new string[] {
                "調子どうだ?カラダ?",
                "あなたは赤い部屋が好きですか?",
                "おまえは今まで食ったパンの枚数をおぼえているのか?",
                "てかLINEやってる?",
                "おいィ?お前らは今の言葉聞こえたか?",
                "何いきなり話かけて来てるわけ?",
                "質問文に対し質問文で答えるとテスト0点なの知ってたか?",
                "あなた…『覚悟して来てる人』……ですよね?",
                "小便は済ませたか?神様にお祈りは?部屋の隅でガタガタふるえて命乞いする心の準備はOK?",
                "これはミラーシェード=サンのケジメ案件では?"
            };
        }

        public void Interview()
        {
            foreach (var question in questions.Shuffle().Take(5))
            {
                Console.WriteLine(question);

                Console.Write("> ");
                var answer = Console.ReadLine();
                Console.WriteLine();

                var info = new InterviewFinishedEventArgs(question, answer);
                OnInterviewFinished(info);
            }
        }
    }

    class StoreReporter
    {
        private readonly List<InterviewFinishedEventArgs> interviewInfoes = new List<InterviewFinishedEventArgs>();

        public void StoreInterviewInfo(InterviewFinishedEventArgs info)
        {
            interviewInfoes.Add(info);
        }

        public void Report()
        {
            using (var w = new StreamWriter("summary.log", append: true))
            {
                w.WriteLine("質問まとめ");
                foreach (var (info, index) in interviewInfoes.WithIndex())
                {
                    w.WriteLine($"{index + 1}: {info.Question}");
                }

                w.WriteLine("回答まとめ");
                foreach (var (info, index) in interviewInfoes.WithIndex())
                {
                    w.WriteLine($"{index + 1}: {info.Answer}");
                }
            }
        }
    }

    class MainClass
    {
        public static void Main(string[] args)
        {
            InterviewFinishedEventHandler realtimeReport = info =>
            {
                using (var w = new StreamWriter("realtime.log", append: true))
                {
                    w.WriteLine($"Q: {info.Question}");
                    w.WriteLine($"A: {info.Answer}");
                }
            };
            var storeReporter = new StoreReporter();

            var interviewer = new Interviewer();
            interviewer.OnInterviewFinished += realtimeReport;
            interviewer.OnInterviewFinished += storeReporter.StoreInterviewInfo;

            interviewer.Interview();

            storeReporter.Report();
        }
    }
}

実行結果は変わらないので省略。

C# 版のプログラムから event 構文に関係する部分を抜粋し、Ruby 版のプログラムから対応する部分を抜粋して並べてみる。

    class InterviewFinishedEventArgs : EventArgs
    {
        public string Question { get; set; }
        public string Answer { get; set; }

        public InterviewFinishedEventArgs(string question, string answer)
        {
            Question = question;
            Answer = answer;
        }
    }

    delegate void InterviewFinishedEventHandler(InterviewFinishedEventArgs e);

    class Interviewer
    {
        public event InterviewFinishedEventHandler OnInterviewFinished;

        public void Interview()
        {
                OnInterviewFinished(info);
        }
    }

    class MainClass
    {
        public static void Main(string[] args)
        {
            interviewer.OnInterviewFinished += realtimeReport;
            interviewer.OnInterviewFinished += storeReporter.StoreInterviewInfo;
        }
    }
InterviewInfo = Struct.new(:question, :answer, keyword_init: true)

class Interviewer
  def add_to_hook(func)
    @hooked_funcs << func
  end

  def interview
      call_hooked_funcs(info)
  end

  private

  def call_hooked_funcs(info)
    @hooked_funcs.each { |f| f.call(info) }
  end
end

interviewer.add_to_hook(realtime_report)
interviewer.add_to_hook(store_reporter.method(:store))

こうしてみると、使っている言葉こそイベント駆動型プログラミング独特のものになっているが、やっていることはほとんど変わらないことが分かると思う。

詳しく見てみよう。

C# 版のプログラムで event キーワードを使って宣言している OnInterviewFinished。これがフックに相当する。
「InterviewFinished」というイベントが発生した時に呼び出すフック、というイメージでこういった命名をした。
event キーワードを使用したことで、フックへの処理の登録は interviewer.OnInterviewFinished += realtimeReport; という形で行えるようになっているし、登録された処理は OnInterviewFinished(info); の形で呼び出すことができるようになっている。
Ruby 版で書いた add_to_hook(func) のような処理や call_fooked_funcs(info) のような処理は必要ない。全部 event キーワードがよしなにやってくれている。

InterviewFinishedEventHandler というものを定義している行があるが、これはフックに登録できる関数の型みたいなものだ。
フックに登録できる関数は InterviewFinishedEventArgs 型の変数 e を受け取る、ということがここで読み取れる。
詳しく理解するためには、C# 特有の delegate 構文について知る必要があるだろう。
ここでは説明しないので ggrks。

InterviewFinishedEventArgs。これは Ruby 版で言うところの InterviewInfo に相当するクラスとなっている。
「InterviewFinished イベント」に関する情報を集めた変数(arguments)、というイメージの命名だ。
このクラスは EventArgs クラスを継承している。event キーワードを使う際にはこうしないといけない決まりになっている。

まあとにかく、C#event キーワードを使ったこのプログラムは、Ruby で書いたフックプログラムと同じことをやっているということだ。

語彙の整理

上のプログラムでは EventHandler という言葉を使った。
これはイベント駆動型プログラミングの語彙だ。

イベントが発生した時に呼び出される処理のことを、「イベントハンドラ」(Event Handler)とよく言う。
フックから渡されるイベント情報を処理(ハンドリング)するイメージだ。
「イベントリスナ」(Event Listener)、「イベントレシーバ」(Event Reciever)等と言うこともある。

イベントハンドラをフックに引っ掛けること/フックから解除することについては、以下のような語彙を使う。
Ruby 版のプログラムでは add_to_hook と書いた部分だ。)

  • register/unregister
  • add/remove
  • subscribe/unsubscribe

イベントを発火させること(すなわち、フックされた関数にイベント情報を渡して呼び出すこと)については、以下のように語彙がたくさんある。
Ruby 版のプログラムでは call_hooked_funcs と書いた部分。)

  • raise
  • trigger
  • emit
  • send
  • invoke
  • fire
  • publish

語彙を置換してみる

フックを使った処理の説明を、イベント関係の言葉で置き換えてみよう。

「任意の処理を
フックに登録しておけば、
フックを持つ側のプログラムが
ある時点で
その処理を呼び出してくれる。」

「任意のイベントハンドラ
イベントを購読させておけば、
イベントを持つ側のプログラムが
イベント発生時に
そのイベントハンドラを呼び出してくれる。」

というわけで、ふわっとした帰納的な説明ではあったが、フックとイベントは近い概念だということがなんとなく伝わったんじゃないかと思う。

"Web フック"とは

ここからが本番のつもりだったが、長くなっちゃったのでここで一旦区切ることにする。

続く。