المولدات في PHP

من موسوعة حسوب
< PHP
(بالتحويل من PHP/Generators)

تقدّم المولِّدات (Generators) طريقة سهلة لاستخدام المكرِّرات (Iterators) البسيطة دون الدخول في تعقيدات استخدام صنف يطبّق الواجهة Iterator.

تتيح المولِّدات كتابة شيفرة تستخدم foreach لمرور على مجموعة من البيانات دون الحاجة إلى بناء مصفوفة في الذاكرة مما قد يؤدي إلى تجاوز حدود الذاكرة، أو يحتاج إلى وقت معالجة طويل لإنشائه. بدلًا من ذلك، يمكن كتابة دالة مولِّدة وهي دالة مشابهة للدوال الاعتيادية باستثناء أنّها لا تعيد النتيجة مرة واحدة بل تعيد العدد المطلوب من النتائج لغرض تقديم القيم التي يتم المرور عليها.

ومن أبسط الأمثلة على المولِّدات هو إعادة استخدام الدالة range()‎ كمولِّد. تنشئ الدالة range()‎ الاعتيادية مصفوفة تتضمن جميع القيم وتعيدها وقد ينتج عن ذلك مصفوفة كبيرة جدًّا، فعلى سبيل المثال استدعاء الدالة range(0, 1000000)‎ يستهلك ما يقارب 100 MB من الذاكرة.

يمكن استبدال الدالة الاعتيادية بالدالة المولدة xrange()‎‎، والتي تحتاج إلى كمية مقبولة من الذاكرة لإنشاء كائن Iterator وتتبع حالة المولّد الحالية داخليًا، وكلّ ذلك يستهل أقل من 1 كيلوبايت.

المثال 1: استخدام الدالة range()‎ كدالة مولّدة

لاحظ أنّ كلتا الدالتين range()‎ و xrange()‎ تعطيان المخرجات ذاتها.

<?php
function xrange($start, $limit, $step = 1) {
    if ($start < $limit) {
        if ($step <= 0) {
            throw new LogicException('Step must be +ve');
        }

        for ($i = $start; $i <= $limit; $i += $step) {
            yield $i;
        }
    } else {
        if ($step >= 0) {
            throw new LogicException('Step must be -ve');
        }

        for ($i = $start; $i >= $limit; $i += $step) {
            yield $i;
        }
    }
}

echo 'Single digit odd numbers from range():  ';
foreach (range(1, 9, 2) as $number) {
    echo "$number ";
}
echo "\n";

echo 'Single digit odd numbers from xrange(): ';
foreach (xrange(1, 9, 2) as $number) {
    echo "$number ";
}
?>

يعطي المثال السابق المخرجات التالية:

Single digit odd numbers from range():  1 3 5 7 9 
Single digit odd numbers from xrange(): 1 3 5 7 9

كائنات Generator

عند استدعاء الدالة المولِّدة للمرة الأولى تعيد الدالة كائنًا من الصنف الداخلي Generator. يطبّق هذا الكائن واجهة Iterator بنفس الطريقة التي يتبعها كائن iterator الموجّه للإمام فقط، ويقدّم التوابع التي يمكن استدعاءها لمعالجة حالة المولِّد، ومنها إرسال القيم إلى المولِّد واستقبالها منه.

صيغة المولدات

الدالة المولدة مشابهة تمامًا للدالة الاعتيادية، باستثناء أنّ الدالة المولدة تنتج (yield) قيمًا حسب الحاجة بدلًا من إعادة قيمة واحدة.

تعيد الدالة المولّدة عند استدعائها كائنًا يمكن المرور على عناصره، وعند القيام بذلك (باستخدام حلقة foreach مثلًا) فإنّ PHP ستستدعي الدالة المولدة في كل مرة تحتاج فيها إلى قيمة، ثم تحفظ حالة الدالة المولدة عندما تنتج قيمة وهكذا يمكن الرجوع إلى تلك الحالة عند الحاجة إلى القيمة اللاحقة.

وعند انتهاء القيمة المنتجة من الدالة المولدة فإنّها تتوقف عن العمل وتستمر الشيفرة في عملها كما لو أنّ مصفوفة قد وصلت إلى نهاية القيم المتوفرة فيها.

ملاحظة: لا يمكن للمولدات أن تُعيد قيمة في PHP 5، والقيام بذلك يؤدي إلى إطلاق خطأ في التصريف. كانت عبارة return الفارغة تعدّ صيغة صحيحة في المولدات وبإمكانها إيقاف عمل الدالة المولّدة، ولكن في الإصدار 7.0 من PHP أصبح بإمكان المولِّدات إعادة القيم، ويمكن الحصول عليها باستخدام Generator::getReturn()‎‎.

الكلمة المفتاحية yield

الكلمة المفتاحية yield هي أساس عمل الدوال المولِّدة، وفي صيغتها البسيطة تشبه عبارة yield عبارة return كثيرًا، باستثناء أنّ عبارة return توقف تنفيذ الشيفرة في الدالة وتعيد قيمة معينة، أما عبارة yield فتقدّم قيمة إلى الشيفرة التي تدور حول المولّد وتوقف تنفيذ شيفرة المولِّد بصورة مؤقتة.

المثال 2: مثال بسيط عن القيم المنتجة

لاحظ كيف أنّ قيمة المتغيّر ‎‎$i تبقى محفوظة بعد تنفيذ عبارة yield

<?php
function gen_one_to_three() {
    for ($i = 1; $i <= 3; $i++) {
        yield $i;
    }
}

$generator = gen_one_to_three();
foreach ($generator as $value) {
    echo "$value\n";
}
?>

تعطي الشيفرة السابقة المخرجات التالية:

1
2
3

ملاحظة: تجمع اللغة مفاتيح الأعداد الصحيحة المتتالية على هيئة مزدوجات مع القيم المنتجة، كما هو الحال مع المصفوفات غير الترابطية. تنبيه: في حال استخدام الكلمة المفتاحية yield ضمن تعبير معين (على سبيل المثال، على يمين عامل الإسناد) يجب إحاطة العبارة بالأقواس في الإصدار الخامس من PHP. فعلى سبيل المثال هذه الصيغة صحيحة:

$data = (yield $value);

ولكن هذه الصيغة غير صحيحة، وستؤدي إلى حدوث خطأ من نوع parse في الإصدار الخامس من اللغة:

$data = yield $value;

لا حاجة لاستخدام الأقواس في الإصدار السابع من اللغة.

يمكن استخدام هذه الصيغة إلى جانب التابع Generator::send()‎.

إنتاج القيم مع المفاتيح

تدعم PHP المصفوفات الترابطية، ولا تختلف المولّدات في ذلك عن المصفوفات، فبالإضافة إلى إنتاج القيم البسيطة كما وضحنا أعلاه، يمكن أيضاً إنتاج مفتاح في الوقت نفسه.

صيغة إنتاج زوج من المفتاح والقيمة مشابهة جدًّا للصيغة المستخدمة في تعريف المصفوفة الترابطية، كما هو موضح أدناه.

المثال 3: إنتاج زوج مفتاح/قيمة

المدخلات في المثال التالي هي حقول مفصولة بالفاصلة المنقوطة، والحقل الأول هو معرّف لاستخدام المفتاح.

<?php


$input = <<<'EOF'
1;PHP;Likes dollar signs
2;Python;Likes whitespace
3;Ruby;Likes blocks
EOF;

function input_parser($input) {
    foreach (explode("\n", $input) as $line) {
        $fields = explode(';', $line);
        $id = array_shift($fields);

        yield $id => $fields;
    }
}

foreach (input_parser($input) as $id => $fields) {
    echo "$id:\n";
    echo "    $fields[0]\n";
    echo "    $fields[1]\n";
}
?>

تعطي الشيفرة السابقة المخرجات التالية:

1:
    PHP
    Likes dollar signs
2:
    Python
    Likes whitespace
3:
    Ruby
    Likes blocks

تنبيه: كما هو الحال مع إنتاج القيم البسيطة، يجب أن تحاط عبارة إنتاج الزوج مفتاح/قيمة بالأقواس في حال استخدامها ضمن سياق معين.

$data = (yield $key => $value);

إنتاج قيم null

يمكن استدعاء yield دون تقديم أي معاملات لإنتاج القيمة NULL مع مفتاح تلقائي.

المثال 4: إنتاج القيمة NULL

<?php
function gen_three_nulls() {
    foreach (range(1, 3) as $i) {
        yield;
    }
}

var_dump(iterator_to_array(gen_three_nulls()));
?>

تعطي الشيفرة السابقة المخرجات التالية:

array(3) {
  [0]=>
  NULL
  [1]=>
  NULL
  [2]=>
  NULL
}

إنتاج القيم بالمرجعية

يمكن للدوال المولِّدة أن تنتج قيمًا بالمرجعية، ويمكن القيام بذلك بنفس الطريقة المتّبعة لإعادة المراجع من الدوال وذلك بإضافة العلامة & إلى بداية اسم الدالة.

المثال 5: إنتاج القيم بالمرجعية

لاحظ أنّ بالإمكان تغيير قيمة المتغيّر ‎‎$number من داخل الحلقة، ولما كانت الدالة المولّدة تنتج قيمًا بالمرجعية، فإنّ قيمة المتغير ‎‎$value ضمن الدالة gen_reference()‎ ستتغيّر.

<?php
function &gen_reference() {
    $value = 3;

    while ($value > 0) {
        yield $value;
    }
}

foreach (gen_reference() as &$number) {
    echo (--$number).'... ';
}
?>

يعطي المثال السابق المخرجات التالية:

2... 1... 0...

تفويض المولِّدات بواسطة yield from

يتيح تفويض المولِّد (generator delegation) في الإصدار السابع من اللغة إنتاج قيم من مولِّد آخر أو كائن يمكن المرور على عناصره أو مصفوفة، وذلك باستخدام الكلمة المفتاحية yield from، حيث سينتج المولّد الخارجي جميع القيم من المولّد أو الكائن أو المصفوفة الداخلية، إلى أن يصل إلى نهاية القيم ليستمر بعدها تنفيذ الشيفرة في المولّد الخارجي.

في حال استخدام مولّد مع الكلمة المفتاحية yield from فإنّ هذه العبارة ستعيد أيضًا جميع القيم المعادة من قبل المولِّد الداخلي.

التخزين في مصفوفة (مثال: باستخدام iterator_to_array()‎)

لا تعيد الكلمة المفتاحية yield from تعيين المفاتيح، بل تحفظ المفاتيح المعادة من المصفوفات أو الكائنات التي يمكن المرور على عناصرها؛ لهذا يمكن أن تمتلك بعض القيم مفاتيح مشتركة مع عبارة yield أو yield from أخرى، والتي ستحلّ محل القيم السابقة في حال تخزين القيم في مصفوفة.

من الحالات الشائعة أن تعيد الدالة iterator_to_array()‎ مصفوفة ذات مفاتيح بصورة افتراضية، مما يؤدّي إلى الحصول على نتائج غير متوقعة. تمتلك الدالة iterator_to_array()‎ معاملًا ثانيًا هو use_keys والذي يمكن إعطاؤه القيمة FALSE لجمع كلّ القيم مع تجاهل المفاتيح المعادة بواسطة الصنف Generator.

المثال 6: استخدام yield from مع الدالة iterator_to_array()‎

<?php
function from() {
    yield 1; // key 0
    yield 2; // key 1
    yield 3; // key 2
}
function gen() {
    yield 0; // key 0
    yield from from(); // keys 0-2
    yield 4; // key 1
}
// pass false as second parameter to get an array [0, 1, 2, 3, 4]
var_dump(iterator_to_array(gen()));
?>

يعطي المثال السابق المخرجات التالية:

array(3) {
  [0]=>
  int(1)
  [1]=>
  int(4)
  [2]=>
  int(3)
}

المثال 7: مثال بسيط عن استخدام الكلمة المفتاحية yield from

<?php
function count_to_ten() {
    yield 1;
    yield 2;
    yield from [3, 4];
    yield from new ArrayIterator([5, 6]);
    yield from seven_eight();
    yield 9;
    yield 10;
}

function seven_eight() {
    yield 7;
    yield from eight();
}

function eight() {
    yield 8;
}

foreach (count_to_ten() as $num) {
    echo "$num ";
}
?>

يعطي المثال السابق النتيجة التالية:

1 2 3 4 5 6 7 8 9 10

المثال 8: الكلمة المفتاحية yield from والقيم المعادة

<?php
function count_to_ten() {
    yield 1;
    yield 2;
    yield from [3, 4];
    yield from new ArrayIterator([5, 6]);
    yield from seven_eight();
    return yield from nine_ten();
}

function seven_eight() {
    yield 7;
    yield from eight();
}

function eight() {
    yield 8;
}

function nine_ten() {
    yield 9;
    return 10;
}

$gen = count_to_ten();
foreach ($gen as $num) {
    echo "$num ";
}
echo $gen->getReturn();
?>

يعطي المثال السابق المخرجات التالية:

1 2 3 4 5 6 7 8 9 10

مقارنة المولّدات بكائنات Iterator

إن الميزة الأساسية التي تتمتّع بها المولّدات هي بساطتها، إذ أنّ الشيفرة المطلوبة أقل بكثير مقارنة باستخدام صنف Iterator، وتكون ذات مقروئية أعلى، فعلى سبيل المثال الدالة والصنف في المثال التالي متكافئان:

<?php
function getLinesFromFile($fileName) {
    if (!$fileHandle = fopen($fileName, 'r')) {
        return;
    }
 
    while (false !== $line = fgets($fileHandle)) {
        yield $line;
    }
 
    fclose($fileHandle);
}

// مقابل...

class LineIterator implements Iterator {
    protected $fileHandle;
 
    protected $line;
    protected $i;
 
    public function __construct($fileName) {
        if (!$this->fileHandle = fopen($fileName, 'r')) {
            throw new RuntimeException('Couldn\'t open file "' . $fileName . '"');
        }
    }
 
    public function rewind() {
        fseek($this->fileHandle, 0);
        $this->line = fgets($this->fileHandle);
        $this->i = 0;
    }
 
    public function valid() {
        return false !== $this->line;
    }
 
    public function current() {
        return $this->line;
    }
 
    public function key() {
        return $this->i;
    }
 
    public function next() {
        if (false !== $this->line) {
            $this->line = fgets($this->fileHandle);
            $this->i++;
        }
    }
 
    public function __destruct() {
        fclose($this->fileHandle);
    }
}
?>

ولكن هناك ثمن مقابل هذه المرونة، فالمولّدات هي مكرّرات أمامية التوجيه فقط (forward-only iterators) ولا يمكن العودة إلى بدايتها بعد أن تبدأ العمل. هذا يعني أيضًا عدم إمكانية المرور على المولّد نفسه أكثر من مرة، ويجب إعادة بناء المولّد باستدعاء الدالة المولّدة مرة أخرى.

مصادر