جنریتور ها در PHP

سرعت در دنیای وب حرف اول رو میزنه، هر چه قدر که وب سایت شما سریع تر باشه کاربر پسند تره.

بیشتر مواقع ما برای سریع تر کردن لود وب سایتمون به سراغ راه حلهایی مثل مینیمایز کردن  فایلهای css و js  ،  استفاده از سیستم ها و کتابخانه های Cache، کم حجم کردن تصاویر و ... می رویم. ولی یادمون می رود که به نحوه کدنویسی خومون هم توجه داشته باشیم تا شاید بهینه تر بشه کد زد. نکته های خیلی ریزی در نحوه نوشتن کدهای PHP است که رعایت کردن آنها در نهایت باعث افزایش کارایی می شوند. یکی از این موارد استفاده از generator ها است. که امروز با هم دیگه بررسی میکنیم.

جتریتور (Generator) چیست ؟

جنریتورها (Generators) که از نسخه 5.5 به PHP اضافه شدند، جنریتورها یک نوع فانکشن هستند که امکان پیاده سازیiterator  ها یا همان تکرار کننده های (مثلfor, foreach, while) بهینه رو به ما می دهند بدون اینکه حافظه زیادی رو اشغال کنند و یا باعث  پیجیده تر شدن کد شوند. 

چرا به Generator ها نیاز داریم ؟

خب اجازه بدهید این را با یک مثال ملموس بیان کنیم. موافقید ؟ در این مثال یک رنجی(محدوده ای) از رشته ها تولید کنیم

<?php

function getRange ($max = 10) {
    $array = [];

    for ($i = 1; $i < $max; $i++) {
        $array[] = $i;
    }

    return $array;
}

foreach (getRange(15) as $range) {
    echo "Dataset {$range} <br>";
}

خب اگر اون فایل رو در لوکال هاست اجرا کنیم، خواهیم دید که خروجی شبیه زیر خواهد داشت.

Dataset 1
Dataset 2
Dataset 3
Dataset 4
Dataset 5
Dataset 6
Dataset 7
Dataset 8
Dataset 9
Dataset 10

خب خروجی فوق خیلی ساده است و نیاز به توضیح خاصی نداره و کار خاص و سنگینی هم انجام نمیده ولی حالا بیایید برگردیم و یک تغییرکوچک روی کد خودمون داشته باشیم

<?php

foreach (getRange(PHP_INT_MAX) as $range) {
    echo "Dataset {$range} <br>";
}

حالا بالاترین رنج تولید شده برابر با PHP_INT_MAX  است که بزرگترین عددی است که نسخه PHP شما می تواند به آن برسد. بعد از اعمال این تغییر به صفحه مرورگر خود برگشته و صفحه مثال رو refresh کنید.
این بار متواجه خواهید شد که با خطای fatal error زیر مواجه می شوید

 خب زیاد جالب نیست نه!؟ PHP حافظه کم آورد! آسانترین و راحت ترین روشی که به فکر بعضی عزیزان میرسه افزایش مقدار memory_limit در فایل php.ini است. ولی واقعا این کار غیر حرفه ای است، با این کار اجازه می دهیم که یک اسکریپت کوچک تمام حافظه ما رو اشغال کند!

راه حل: استفاده از Generator ها 

بیاید تابع بالا رو مجدد با مقدار PHP_INT_MAX تعریف کنیم با این تفاوت که  این بار یک generator ایجاد خواهیم کرد.

<?php

function getRange ($max = 10) {
    for ($i = 1; $i < $max; $i++) {
        yield $i;
    }
}

foreach (getRange(PHP_INT_MAX) as $range) {
    echo "Dataset {$range} <br>";
}

خب حالا اگر این دفعه صفحه خودتون رو رفرش کنید خواهید دید که بدون هیچ مشکلی در سرعت و کمبود حافظه کدهاتون اجرا می شوند.

یک مثال ساده دیگه از جنریتورها میتونه دوباره نوشتن تابع Rang خود PHP باشه. Range در پی اچ پی دو پارامتر میگیره و باید یک آرایه از مقدار های بین آن دو رو تولید کنه و برگردونه که میتونه در نهایت منجر به ایجاد آرایه های بزرگ بشه. برای مثال صدا زدن (0,1000000)range منجر به استفاده بالای 100 مگابایت از حافظه در حال استفاده خواهد شد.

به عنوان یک جایگزین بهینه تر ما تابع xrange رو پیاده سازی می کنیم. که فقط حافظه ای برای تکرار روی آبجکت و track کردن حالت جاری خواهد داشت که به نظر میرسه کمتر از 1 کیلوبایت است!

<?php
function xrange($start, $limit, $step = 1) {
    if ($start < $limit) {

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

جنریتور ها چطور کار میکنند ؟

جنریتورها از کلید واژه yield بجای return استفاده می کنند. Yield همانند  return است و یک مقدار رو به تابع صدا زننده  بر میگردونه ولی به جای حذف کردن فانکشن از stack ، وضعیت یا state آن را حفظ می کند.  که این به فانکشن اجازه می دهد تا از جایی که دوباره صدا زده شده ادامه پیدا کنه.

جنریتور ها برای اجرای تکرارها روی مجموعه داده های بزرگ هم از لحاظ سرعت و هم  از لحاظ میزان مصرف حافظه بسیار مناسب می باشند و از آنجا که نیازی ندارید تا  کلاس Iterator رو extend کنید. برای پیاده سازی هم بسیار سریع تر می باشند.

زمانهایی وجود داره که ما می خواهیم حجم بزرگی از داده رو parse کنیم مثل فایلهای log یا باید محاسبات سنگین روی نتایج بزرگ دیتابیس ما ایجاد شوند. در این صورت ما نمی خواهیم که کل حافظه رو اشغال کنیم و فضایی برای کار دیگه نداشته باشیم! دیتا حتما نباید بزرگ باشه که generator ها تاثیر خودشون رو نشون بدهند! پیشنهاد می کنم همیشه از آنها استفاده کنید. فراموش نکنید هدف ما افزایش سرعت و استفاده کم از حافظه می باشد.

ء Return کردن کلیدها

زمانهایی وحود داره که به مقادیر بازگشتی ما باید به صورت key-value باشند در واقع ما به key های خودمون نیاز داریم. ما می تونیم key-value ها رو بدین صورت yield کنیم.

<?php

function getRange ($max = 10) {
    for ($i = 1; $i < $max; $i++) {
        $value = $i * mt_rand();

        yield $i => $value;
    }
}

و می تونیم از آنها بدین صورت در تابع خودمون استفاده کنیم 

<?php

foreach (getRange(PHP_INT_MAX) as $range => $value) {
    echo "Dataset {$range} has {$value} value<br>";
}

ارسال مقدار به generator  ها

جنریتور ها مقدار هم می گیرند بدین معنی که ما میتوانیم به آنها یک مقداری را inject کنیم تا بدینوسیله به طور مثال به generator خودمون بگوییم که متوقف بشه یا خروجی رو تغییر بدهد، به طور مثال ما میخواهیم که تابع getRange  بعد از دریافت  مقدار   ‘stop’ متوقف شود.

<?php

function getRange ($max = 10) {
    for ($i = 1; $i < $max; $i++) {
        $injected = yield $i;

        if ($injected === 'stop') return;
    }
}

 مقدار را بدین صورت به آن ارسال می کنیم:

<?php

$generator = getRange(PHP_INT_MAX);

foreach ($generator as $range) {
    if ($range === 10000) {
        $generator->send('stop');
    }

    echo "Dataset {$range} <br>";
}

حالا که با generatorها آشنا شدید امیدوارم بیشتر از آنها استفاده کنید و بهینه تر کد بنویسید.