Featured image of post 組み合わせ生成機を作る - Mooでループをオブジェクトにする

組み合わせ生成機を作る - Mooでループをオブジェクトにする

Mooを使って「次のパスワード候補」を生成するクラスを作成。複雑なループ構造をオブジェクトの中に隠蔽し、スッキリとしたコードを実現します。

前回は、可変長のパスワード探索において「再帰呼び出し」が必要になり、コードが複雑化する問題に直面しました。

今回は、その複雑さを「オブジェクト」の中に閉じ込め、使う側からは非常にシンプルに見えるようにリファクタリングします。ここで登場するのが「Iterator(反復子)」の概念です。

目標:nextメソッドを持つオブジェクト

目指すのは、以下のように使えるクラス BruteForceIterator です。

1
2
3
4
5
my $iterator = BruteForceIterator->new(length => 4);

while (my $password = $iterator->next) {
    # ここで $password を使う(0000, 0001, ... 9999)
}

この $iterator->next を呼ぶたびに、新しいパスワード候補が1つ返ってきます。最後まで出し尽くしたら undef を返してループが終了します。

これなら、使う側は「どうやって組み合わせを作っているか(for文なのか再帰なのか)」を知る必要がありません。

Client(使う側)とIteratorの関係は以下のようになります。Clientはひたすら next を呼び続け、Iteratorは内部で計算した値を1つずつ返します。

	sequenceDiagram
    participant Client
    participant Iterator
    participant Lock

    Note over Client, Iterator: 準備フェーズ
    Client->>Iterator: new(length => 4)
    Iterator-->>Client: インスタンス生成

    Note over Client, Iterator: 反復フェーズ
    loop undefが返るまで
        Client->>Iterator: next()
        Iterator->>Iterator: 次の値を計算 (例: "0000")
        Iterator-->>Client: "0000"

        Client->>Lock: unlock("0000")

        Client->>Iterator: next()
        Iterator->>Iterator: 次の値を計算 (例: "0001")
        Iterator-->>Client: "0001"

        Client->>Lock: unlock("0001")
    end

    Client->>Iterator: next()
    Iterator-->>Client: undef (終了)

実装:MooでIteratorを作る

では、BruteForceIterator.pm を実装してみましょう。 内部的には、現在の数値を覚えておき、next が呼ばれるたびに1つ増やして返す、という単純な仕組みで実現できます(数字のパスワードならではの簡略化です)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package BruteForceIterator;
use Moo;
use experimental qw(signatures);

# パスワードの桁数
has length => (
    is       => 'ro',
    required => 1,
);

# 現在の状態(カウンター)
has _current => (
    is      => 'rw',
    default => 0,
);

# 最大値(上限)
has _max => (
    is      => 'lazy',
    builder => sub ($self) {
        return 10 ** $self->length; # 3桁なら1000
    },
);

# 次の値を返すメソッド
sub next ($self) {
    my $val = $self->_current;

    # 上限に達していたら undef を返して終了
    if ($val >= $self->_max) {
        return undef;
    }

    # カウンターを進める
    $self->_current($val + 1);

    # 指定された桁数になるように0埋めする (例: 1 -> "0001")
    return sprintf("%0*d", $self->length, $val);
}

1;

見てください。再帰も多重ループも使っていません。単に「今の数字」を覚えておいて、呼ばれるたびにインクリメントしているだけです。

使う側のコード(メインスクリプト)

これを使う cracker_iterator.pl は以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use v5.36;
use PasswordLock;
use BruteForceIterator;

my $target_length = 4;
my $lock = PasswordLock->new;

# Iteratorを生成
my $iterator = BruteForceIterator->new(length => $target_length);

say "クラッキングを開始します...";

# nextメソッドを使ってループ
while (defined(my $attempt = $iterator->next)) {
    if ($lock->unlock($attempt)) {
        say "解除成功! パスワードは [ $attempt ] です!";
        exit;
    }
}

say "見つかりませんでした。";

非常にスッキリしました! 再帰ロジックの複雑さは消え去り、直感的な while ループになりました。

Iteratorの利点:遅延評価(Lazy Evaluation)

このアプローチの最大の利点は、「全ての組み合わせをメモリに展開しなくて良い」 という点です。

もし、8桁のパスワード(1億通り)を全て配列に入れてから処理しようとすると、巨大なメモリを消費します。しかし、このIterator方式なら、メモリ上には「現在のカウント(1つの整数)」しか保持していません。

next が呼ばれた瞬間に、必要な値を1つだけ計算して(作り出して)返しています。これを遅延評価と呼びます。無限に近いリストでも、これなら扱うことができます。

次回予告

今回は数字だけのパスワードだったので、単純なカウンターで実装できました。しかし、もし「英字」が含まれていたら? 「辞書にある単語」を使いたかったら?

単純なインクリメントでは対応できません。

次回は、この next メソッドという共通インターフェースを守りながら、内部の実装を差し替えることで、「辞書攻撃」 にも対応できる柔軟なツールへと進化させます。ポリモーフィズム(多態性) の威力を体験しましょう。

" >前回記事: 4桁、5桁…ループが止まらない - 多重ループの絶望

comments powered by Disqus