سؤال

لا يحتوي C++ على دعم أصلي للتقييم البطيء (كما يفعل هاسكل).

أتساءل عما إذا كان من الممكن تنفيذ التقييم البطيء في C++ بطريقة معقولة.إذا كانت الإجابة بنعم، كيف ستفعل ذلك؟

يحرر:تعجبني إجابة كونراد رودولف.

أتساءل عما إذا كان من الممكن تنفيذ ذلك بطريقة أكثر عمومية، على سبيل المثال باستخدام فئة ذات معلمات كسول تعمل بشكل أساسي مع T بالطريقة التي تعمل بها array_add مع المصفوفة.

أي عملية على T ستعود كسولة بدلاً من ذلك.المشكلة الوحيدة هي تخزين الوسائط ورمز العملية داخل نفسه.يمكن لأي شخص أن يرى كيفية تحسين هذا؟

هل كانت مفيدة؟

المحلول

أتساءل عما إذا كان من الممكن تنفيذ التقييم البطيء في C++ بطريقة معقولة.إذا كانت الإجابة بنعم، كيف ستفعل ذلك؟

نعم، هذا ممكن ويتم القيام به في كثير من الأحيان، على سبيل المثال.لحسابات المصفوفة.الآلية الرئيسية لتسهيل ذلك هي التحميل الزائد على المشغل.النظر في حالة إضافة المصفوفة.عادةً ما يبدو توقيع الوظيفة كما يلي:

matrix operator +(matrix const& a, matrix const& b);

الآن، لجعل هذه الوظيفة كسولة، يكفي إرجاع وكيل بدلاً من النتيجة الفعلية:

struct matrix_add;

matrix_add operator +(matrix const& a, matrix const& b) {
    return matrix_add(a, b);
}

الآن كل ما عليك فعله هو كتابة هذا الوكيل:

struct matrix_add {
    matrix_add(matrix const& a, matrix const& b) : a(a), b(b) { }

    operator matrix() const {
        matrix result;
        // Do the addition.
        return result;
    }
private:
    matrix const& a, b;
};

السحر يكمن في الطريقة operator matrix() وهو عامل تحويل ضمني من matrix_add إلى عادي matrix.بهذه الطريقة، يمكنك تسلسل عمليات متعددة (من خلال توفير الأحمال الزائدة المناسبة بالطبع).يتم التقييم فقط عندما يتم تعيين النتيجة النهائية إلى أ matrix مثال.

يحرر كان ينبغي أن أكون أكثر وضوحا.كما هو الحال، فإن الكود ليس له أي معنى لأنه على الرغم من أن التقييم يحدث بتكاسل، إلا أنه لا يزال يحدث بنفس التعبير.على وجه الخصوص، سيتم إضافة آخر تقييم هذا الرمز ما لم matrix_add يتم تغيير الهيكل للسماح بالإضافة بالسلاسل.يسهل C++0x ذلك إلى حد كبير من خلال السماح بالقوالب المتغيرة (أي.قوائم القوالب ذات الطول المتغير).

ومع ذلك، هناك حالة بسيطة للغاية حيث يكون لهذا الرمز فائدة حقيقية ومباشرة وهي ما يلي:

int value = (A + B)(2, 3);

وهنا يفترض ذلك A و B هي مصفوفات ثنائية الأبعاد ويتم إلغاء الإسناد في تدوين فورتران، أي.ما ورد أعلاه يحسب واحد عنصر من مجموع المصفوفة.من الإسراف بالطبع إضافة المصفوفات بأكملها. matrix_add إلى الإنقاذ:

struct matrix_add {
    // … yadda, yadda, yadda …

    int operator ()(unsigned int x, unsigned int y) {
        // Calculate *just one* element:
        return a(x, y) + b(x, y);
    }
};

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

نصائح أخرى

Boost.Lambda جميل جدًا، لكن Boost.Proto يكون بالضبط ما الذي تبحث عنه.لديها بالفعل حمولات زائدة الجميع عوامل تشغيل C++، والتي تؤدي وظيفتها المعتادة بشكل افتراضي عندما proto::eval() يسمى، ولكن يمكن تغييرها.

وماذا كونراد أوضح بالفعل يمكن وضعها أيضا على دعم الدعاء متداخلة من المشغلين، عن تنفيذ بتكاسل. في المثال كونراد، وقال انه لديه وجوه التعبير التي يمكن تخزين بالضبط حجتين، بالضبط اثنين من المعاملات من عملية واحدة. المشكلة هي أنه سيتم تنفيذ فقط <م> واحدة التعبير الجزئي بتكاسل، وهو ما يفسر لطيف هذا المفهوم في تقييم كسول وضع بعبارات بسيطة، ولكن لا تحسين الأداء بشكل ملحوظ. يظهر المثال الآخر أيضا جيدا كيف يمكن للمرء أن تطبيق operator() لإضافة بعض العناصر فقط باستخدام هذا الكائن التعبير. ولكن لتقييم التعبيرات المعقدة التعسفية، ونحن بحاجة الى بعض آلية التي يمكن أن <م> مخزن هيكل ذلك أيضا. لا نستطيع الحصول على جميع أنحاء قوالب للقيام بذلك. واسم لذلك هو expression templates. والفكرة هي أن أحد وجوه التعبير قالب يمكن تخزين بنية بعض التعسفي التعبير شبه متكرر، مثل شجرة، حيث العمليات هي العقد، والمعاملات هي-العقد التابعة. ل<م> جدا تفسير جيد وجدت اليوم فقط (بعد بضعة أيام كتبته رمز أدناه) يرى <وأ href = "https://web.archive.org/web/20090421155750/http:// ubiety.uwaterloo.ca/~tveldhui/papers/Expression-Templates/exprtmpl.html "يختلط =" noreferrer "> هنا .

template<typename Lhs, typename Rhs>
struct AddOp {
    Lhs const& lhs;
    Rhs const& rhs;

    AddOp(Lhs const& lhs, Rhs const& rhs):lhs(lhs), rhs(rhs) {
        // empty body
    }

    Lhs const& get_lhs() const { return lhs; }
    Rhs const& get_rhs() const { return rhs; }
};

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

struct Point { int x, y; };

// add expression template with point at the right
template<typename Lhs, typename Rhs> AddOp<AddOp<Lhs, Rhs>, Point> 
operator+(AddOp<Lhs, Rhs> const& lhs, Point const& p) {
    return AddOp<AddOp<Lhs, Rhs>, Point>(lhs, p);
} 

// add expression template with point at the left
template<typename Lhs, typename Rhs> AddOp< Point, AddOp<Lhs, Rhs> > 
operator+(Point const& p, AddOp<Lhs, Rhs> const& rhs) {
    return AddOp< Point, AddOp<Lhs, Rhs> >(p, rhs);
}

// add two points, yield a expression template    
AddOp< Point, Point > 
operator+(Point const& lhs, Point const& rhs) {
    return AddOp<Point, Point>(lhs, rhs);
}

والآن، إذا كان لديك

Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 };
p1 + (p2 + p3); // returns AddOp< Point, AddOp<Point, Point> >

أنت الآن بحاجة فقط إلى زيادة التحميل المشغل = وإضافة منشئ مناسبة لنوع نقطة، واستعرض AddOp. تغيير تعريف ل:

struct Point { 
    int x, y; 

    Point(int x = 0, int y = 0):x(x), y(y) { }

    template<typename Lhs, typename Rhs>
    Point(AddOp<Lhs, Rhs> const& op) {
        x = op.get_x();
        y = op.get_y();
    }

    template<typename Lhs, typename Rhs>
    Point& operator=(AddOp<Lhs, Rhs> const& op) {
        x = op.get_x();
        y = op.get_y();
        return *this;
    }

    int get_x() const { return x; }
    int get_y() const { return y; }
};

وإضافة get_x المناسب وget_y إلى AddOp وظائف الأعضاء:

int get_x() const {
    return lhs.get_x() + rhs.get_x();
}

int get_y() const {
    return lhs.get_y() + rhs.get_y();
}

لاحظ كيف أننا لم تقم بإنشاء أي المؤقتة المتقبلة من نوع نقطة. كان يمكن أن يكون مصفوفة كبيرة مع العديد من المجالات. ولكن في الوقت الذي يحتاج الى نتيجة، نحسب أنه بتكاسل .

وأنا أفكر في تنفيذ فئة قالب، يستخدم std::function. يجب على الطبقة، أكثر أو أقل، وتبدو هذه:

template <typename Value>
class Lazy
{
public:
    Lazy(std::function<Value()> function) : _function(function), _evaluated(false) {}

    Value &operator*()  { Evaluate(); return  _value; }
    Value *operator->() { Evaluate(); return &_value; }

private:
    void Evaluate()
    {
        if (!_evaluated)
        {
            _value = _function();
            _evaluated = true;
        }
    }

    std::function<Value()> _function;
    Value _value;
    bool _evaluated;
};

وعلى سبيل المثال استخدام:

class Noisy
{
public:
    Noisy(int i = 0) : _i(i)
    {
        std::cout << "Noisy(" << _i << ")"  << std::endl;
    }
    Noisy(const Noisy &that) : _i(that._i)
    {
        std::cout << "Noisy(const Noisy &)" << std::endl;
    }
    ~Noisy()
    {
        std::cout << "~Noisy(" << _i << ")" << std::endl;
    }

    void MakeNoise()
    {
        std::cout << "MakeNoise(" << _i << ")" << std::endl;
    }
private:
    int _i;
};  

int main()
{
    Lazy<Noisy> n = [] () { return Noisy(10); };

    std::cout << "about to make noise" << std::endl;

    n->MakeNoise();
    (*n).MakeNoise();
    auto &nn = *n;
    nn.MakeNoise();
}

وفوق رمز يجب أن ينتج الرسالة التالية على وحدة التحكم:

Noisy(0)
about to make noise
Noisy(10)
~Noisy(10)
MakeNoise(10)
MakeNoise(10)
MakeNoise(10)
~Noisy(10)

لاحظ أن Noisy(10) الطباعة منشئ لن يتم استدعاؤها حتى يتم الوصول إلى متغير.

وهذه الفئة هي أبعد ما تكون عن الكمال، وإن كان. إن أول شيء يكون ومنشئ الافتراضي Value يجب أن دعا التهيئة عضوا (طباعة Noisy(0) في هذه الحالة). يمكننا استخدام مؤشر ل_value بدلا من ذلك، ولكني لست متأكدا ما إذا كان يؤثر على الأداء.

ويوهانس "الجواب works.But عندما يتعلق الأمر المزيد من الأقواس، فإنه لا يعمل كما ترغب في ذلك. هنا مثال على ذلك.

Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 }, p4 = { 7, 8 };
(p1 + p2) + (p3+p4)// it works ,but not lazy enough

ولأن المشغل + ثلاثة زائد لا يشمل حالة

AddOp<Llhs,Lrhs>+AddOp<Rlhs,Rrhs>

وهكذا المترجم له لتحويل إما (P1 + P2) أو (P3 P4 +) إلى نقطة، هذا ليس enough.And كسول عندما يقرر المترجم الذي لتحويل، فإنه يشكو. لأن ليس هناك ما هو أفضل من الآخر. هنا يأتي بلدي التمديد: إضافة بعد مشغل آخر زائد +

    template <typename LLhs, typename LRhs, typename RLhs, typename RRhs>
AddOp<AddOp<LLhs, LRhs>, AddOp<RLhs, RRhs>> operator+(const AddOp<LLhs, LRhs> & leftOperandconst, const AddOp<RLhs, RRhs> & rightOperand)
{
    return  AddOp<AddOp<LLhs, LRhs>, AddOp<RLhs, RRhs>>(leftOperandconst, rightOperand);

}

والآن، المترجم يمكن التعامل مع الحالة أعلاه بشكل صحيح، وليس التحويل الضمني، volia!

وC ++ 0X لطيفة والإطلاق .... ولكن بالنسبة لأولئك منا الذين يعيشون في الوقت الحاضر لديك مكتبة امدا تعزيز وزيادة فينيكس. سواء بقصد جلب كميات كبيرة من البرمجة الوظيفية لC ++.

وكل شيء ممكن.

وهذا يعتمد على بالضبط ما تعنيه:

class X
{
     public: static X& getObjectA()
     {
          static X instanceA;

          return instanceA;
     }
};

وهنا لدينا تؤثر على المتغير العالمي الذي يتم تقييم بتكاسل عند نقطة الاستخدام لأول مرة.

وبناء على طلب حديثا في هذه المسألة.
وسرقة تصميم كونراد رودولف وتوسيع نطاقه.

والكائن كسول:

template<typename O,typename T1,typename T2>
struct Lazy
{
    Lazy(T1 const& l,T2 const& r)
        :lhs(l),rhs(r) {}

    typedef typename O::Result  Result;
    operator Result() const
    {
        O   op;
        return op(lhs,rhs);
    }
    private:
        T1 const&   lhs;
        T2 const&   rhs;
};

وكيفية استخدامه:

namespace M
{
    class Matrix
    {
    };
    struct MatrixAdd
    {
        typedef Matrix  Result;
        Result operator()(Matrix const& lhs,Matrix const& rhs) const
        {
            Result  r;
            return r;
        }
    };
    struct MatrixSub
    {
        typedef Matrix  Result;
        Result operator()(Matrix const& lhs,Matrix const& rhs) const
        {
            Result  r;
            return r;
        }
    };
    template<typename T1,typename T2>
    Lazy<MatrixAdd,T1,T2> operator+(T1 const& lhs,T2 const& rhs)
    {
        return Lazy<MatrixAdd,T1,T2>(lhs,rhs);
    }
    template<typename T1,typename T2>
    Lazy<MatrixSub,T1,T2> operator-(T1 const& lhs,T2 const& rhs)
    {
        return Lazy<MatrixSub,T1,T2>(lhs,rhs);
    }
}

في C ++ تقييم كسول 11 مماثل ليمكن تحقيق ذلك باستخدام الأمراض المنقولة جنسيا :: shared_future الجواب hiapay ل. لا يزال لديك لتغليف الحسابات في lambdas لكن يؤخذ التحفيظ رعاية:

std::shared_future<int> a = std::async(std::launch::deferred, [](){ return 1+1; });

وإليك مثال الكامل:

#include <iostream>
#include <future>

#define LAZY(EXPR, ...) std::async(std::launch::deferred, [__VA_ARGS__](){ std::cout << "evaluating "#EXPR << std::endl; return EXPR; })

int main() {
    std::shared_future<int> f1 = LAZY(8);
    std::shared_future<int> f2 = LAZY(2);
    std::shared_future<int> f3 = LAZY(f1.get() * f2.get(), f1, f2);

    std::cout << "f3 = " << f3.get() << std::endl;
    std::cout << "f2 = " << f2.get() << std::endl;
    std::cout << "f1 = " << f1.get() << std::endl;
    return 0;
}

لنأخذ هاسكل كمصدر إلهام لنا - فهو كسالى حتى النخاع.أيضًا، دعونا نضع في اعتبارنا كيف يستخدم Linq في C# أدوات التعداد بطريقة أحادية (urgh - هنا الكلمة - آسف).أخيرًا وليس آخرًا، دعونا نضع في اعتبارنا ما الذي من المفترض أن تقدمه coroutines للمبرمجين.وهي فصل الخطوات الحسابية (على سبيل المثال.المستهلك المنتج) من بعضها البعض.ودعنا نحاول التفكير في كيفية ارتباط الكوروتينات بالتقييم البطيء.

يبدو أن كل ما سبق مرتبط بطريقة أو بأخرى.

بعد ذلك، دعونا نحاول استخلاص تعريفنا الشخصي لما تعنيه كلمة "كسول".

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

دعونا نصبح ملموسين وندخل في بعض التعليمات البرمجية.ونحن بحاجة إلى مثال لذلك!هنا، اخترت "مشكلة" fizzbuzz كمثال، فقط لسبب وجود حل لطيف وكسول لها.

في هاسكل يبدو الأمر كما يلي:

module FizzBuzz
( fb
)
where
fb n =
    fmap merge fizzBuzzAndNumbers
    where
        fizz = cycle ["","","fizz"]
        buzz = cycle ["","","","","buzz"]
        fizzBuzz = zipWith (++) fizz buzz
        fizzBuzzAndNumbers = zip [1..n] fizzBuzz
        merge (x,s) = if length s == 0 then show x else s

دالة هاسكل cycle ينشئ قائمة لا نهائية (كسولة بالطبع!) من قائمة محدودة عن طريق تكرار القيم الموجودة في القائمة المحدودة إلى الأبد.بأسلوب برمجة متحمس، فإن كتابة شيء كهذا من شأنه أن يدق أجراس الإنذار (تجاوز الذاكرة، حلقات لا نهاية لها!).ولكن ليس كذلك في لغة كسولة.الحيلة هي أن القوائم البطيئة لا يتم حسابها على الفور.ربما أبدا.عادة فقط بقدر ما يتطلبه الكود اللاحق.

السطر الثالث في where الكتلة أعلاه تخلق كسولًا آخر !!القائمة، عن طريق الجمع بين القوائم اللانهائية fizz و buzz عن طريق وصفة عنصرين مفردين "قم بتسلسل عنصر سلسلة من أي من قائمتي الإدخال في سلسلة واحدة".مرة أخرى، إذا تم تقييم ذلك على الفور، فسيتعين علينا الانتظار حتى نفاد موارد جهاز الكمبيوتر الخاص بنا.

في السطر الرابع، نقوم بإنشاء مجموعات من أعضاء القائمة الكسولة المحدودة [1..n] مع قائمتنا الكسولة التي لا نهاية لها fizzbuzz.والنتيجة لا تزال كسولة.

حتى في الجسم الرئيسي لدينا fb الوظيفة، ليست هناك حاجة للحصول على حريصة.تقوم الوظيفة بأكملها بإرجاع قائمة بالحل، وهو في حد ذاته كسول مرة أخرى.يمكنك أيضًا التفكير في نتيجة fb 50 كحساب يمكنك (جزئيًا) تقييمه لاحقًا.أو دمجها مع أشياء أخرى، مما يؤدي إلى تقييم أكبر (كسول).

لذا، من أجل البدء في إصدار C++ الخاص بنا من "fizzbuzz"، نحتاج إلى التفكير في طرق كيفية دمج الخطوات الجزئية لحساباتنا في أجزاء أكبر من الحسابات، حيث تسحب كل منها البيانات من الخطوات السابقة كما هو مطلوب.

يمكنك رؤية القصة كاملة في جوهر الألغام.

هنا الأفكار الأساسية وراء الكود:

بالاقتراض من C# وLinq، نحن "نخترع" نوعًا عامًا ذا حالة Enumerator, ، الذي يحمل
- القيمة الحالية للحساب الجزئي
- حالة الحساب الجزئي (حتى نتمكن من إنتاج قيم لاحقة)
- دالة العامل، التي تنتج الحالة التالية، والقيمة التالية، والمنطق الذي يوضح ما إذا كان هناك المزيد من البيانات أو ما إذا كان التعداد قد انتهى.

لكي يتمكن من التأليف Enumerator<T,S> على سبيل المثال عن طريق قوة . (dot)، تحتوي هذه الفئة أيضًا على وظائف مستعارة من فئات نوع Haskell مثل Functor و Applicative.

تكون دالة العامل للعداد دائمًا بالشكل: S -> std::tuple<bool,S,T أين S هو متغير النوع العام الذي يمثل الحالة و T هو متغير النوع العام الذي يمثل قيمة - نتيجة خطوة حسابية.

كل هذا واضح بالفعل في السطور الأولى من الكتاب Enumerator تعريف الطبقة.

template <class T, class S>
class Enumerator
{
public:
    typedef typename S State_t;
    typedef typename T Value_t;
    typedef std::function<
        std::tuple<bool, State_t, Value_t>
        (const State_t&
            )
    > Worker_t;

    Enumerator(Worker_t worker, State_t s0)
        : m_worker(worker)
        , m_state(s0)
        , m_value{}
    {
    }
    // ...
};

لذلك، كل ما نحتاجه لإنشاء مثيل عداد محدد، نحتاج إلى إنشاء دالة عاملة، والحصول على الحالة الأولية وإنشاء مثيل لـ Enumerator مع هاتين الحجتين.

هنا مثال - وظيفة range(first,last) يخلق مجموعة محدودة من القيم.وهذا يتوافق مع قائمة كسولة في عالم هاسكل.

template <class T>
Enumerator<T, T> range(const T& first, const T& last)
{
    auto finiteRange =
        [first, last](const T& state)
    {
        T v = state;
        T s1 = (state < last) ? (state + 1) : state;
        bool active = state != s1;
        return std::make_tuple(active, s1, v);
    };
    return Enumerator<T,T>(finiteRange, first);
}

ويمكننا الاستفادة من هذه الوظيفة، على سبيل المثال مثل هذا: auto r1 = range(size_t{1},10); - لقد أنشأنا لأنفسنا قائمة كسولة تحتوي على 10 عناصر!

الآن، كل ما نفتقده في تجربتنا "المذهلة"، هو أن نرى كيف يمكننا تكوين العدادين.العودة إلى هاسكل cycle وظيفة، وهو نوع من بارد.كيف سيبدو الأمر في عالمنا C++؟ها هو:

template <class T, class S>
auto
cycle
( Enumerator<T, S> values
) -> Enumerator<T, S>
{
    auto eternally =
        [values](const S& state) -> std::tuple<bool, S, T>
    {
        auto[active, s1, v] = values.step(state);
        if (active)
        {
            return std::make_tuple(active, s1, v);
        }
        else
        {
            return std::make_tuple(true, values.state(), v);
        }
    };
    return Enumerator<T, S>(eternally, values.state());
}

يأخذ العداد كمدخل ويعيد العداد.وظيفة محلية (لامدا). eternally ما عليك سوى إعادة تعيين تعداد الإدخال إلى قيمة البداية الخاصة به كلما نفدت القيم وها هو - لدينا نسخة لا نهائية ومتكررة باستمرار من القائمة التي قدمناها كوسيطة:: auto foo = cycle(range(size_t{1},3)); ويمكننا بالفعل دون خجل أن نؤلف "حساباتنا" الكسولة.

zip يعد هذا مثالًا جيدًا، حيث يوضح أنه يمكننا أيضًا إنشاء عداد جديد من عدادي إدخال.ينتج العداد الناتج عددًا من القيم أصغر من أي من عدادي الإدخال (صفوف تحتوي على عنصرين، واحد لكل عداد إدخال).لقد نفذت zip داخل class Enumerator بحد ذاتها.هنا كيف يبدو:

// member function of class Enumerator<S,T> 
template <class T1, class S1>
auto
zip
( Enumerator<T1, S1> other
) -> Enumerator<std::tuple<T, T1>, std::tuple<S, S1> >
{
    auto worker0 = this->m_worker;
    auto worker1 = other.worker();
    auto combine =
        [worker0,worker1](std::tuple<S, S1> state) ->
        std::tuple<bool, std::tuple<S, S1>, std::tuple<T, T1> >
    {
        auto[s0, s1] = state;
        auto[active0, newS0, v0] = worker0(s0);
        auto[active1, newS1, v1] = worker1(s1);
        return std::make_tuple
            ( active0 && active1
            , std::make_tuple(newS0, newS1)
            , std::make_tuple(v0, v1)
            );
    };
    return Enumerator<std::tuple<T, T1>, std::tuple<S, S1> >
        ( combine
        , std::make_tuple(m_state, other.state())
        );
}

يرجى ملاحظة كيف يؤدي "الدمج" أيضًا إلى الجمع بين حالة كلا المصدرين وقيم كلا المصدرين.

لأن هذا المنشور بالفعل TL;DR;بالنسبة للكثيرين، هنا ...

ملخص

نعم، يمكن تنفيذ التقييم البطيء في C++.لقد فعلت ذلك هنا عن طريق استعارة أسماء الوظائف من haskell والنموذج من عدادات C# وLinq.قد تكون هناك أوجه تشابه مع pythons itertools، راجع للشغل.وأعتقد أنهم اتبعوا نهجا مماثلا.

التنفيذ الخاص بي (راجع رابط Gist أعلاه) هو مجرد نموذج أولي - وليس رمز إنتاج، راجع للشغل.لذلك لا توجد ضمانات على الإطلاق من جانبي.إنه يعمل بشكل جيد كرمز تجريبي لتوصيل الفكرة العامة.

وماذا ستكون هذه الإجابة بدون إصدار C++ النهائي من fizzbuz، أليس كذلك؟ها هو:

std::string fizzbuzz(size_t n)
{
    typedef std::vector<std::string> SVec;
    // merge (x,s) = if length s == 0 then show x else s
    auto merge =
        [](const std::tuple<size_t, std::string> & value)
        -> std::string
    {
        auto[x, s] = value;
        if (s.length() > 0) return s; 
        else return std::to_string(x);
    };

    SVec fizzes{ "","","fizz" };
    SVec buzzes{ "","","","","buzz" };

    return
    range(size_t{ 1 }, n)
    .zip
        ( cycle(iterRange(fizzes.cbegin(), fizzes.cend()))
          .zipWith
            ( std::function(concatStrings)
            , cycle(iterRange(buzzes.cbegin(), buzzes.cend()))
            )
        )
    .map<std::string>(merge)
    .statefulFold<std::ostringstream&>
    (
        [](std::ostringstream& oss, const std::string& s) 
        {
            if (0 == oss.tellp())
            {
                oss << s;
            }
            else
            {
                oss << "," << s;
            }
        }
        , std::ostringstream()
    )
    .str();
}

و...لتوضيح هذه النقطة إلى أبعد من ذلك - هنا شكل مختلف من fizzbuzz الذي يُرجع "قائمة لا نهائية" إلى المتصل:

typedef std::vector<std::string> SVec;
static const SVec fizzes{ "","","fizz" };
static const SVec buzzes{ "","","","","buzz" };

auto fizzbuzzInfinite() -> decltype(auto)
{
    // merge (x,s) = if length s == 0 then show x else s
    auto merge =
        [](const std::tuple<size_t, std::string> & value)
        -> std::string
    {
        auto[x, s] = value;
        if (s.length() > 0) return s;
        else return std::to_string(x);
    };

    auto result =
        range(size_t{ 1 })
        .zip
        (cycle(iterRange(fizzes.cbegin(), fizzes.cend()))
            .zipWith
            (std::function(concatStrings)
                , cycle(iterRange(buzzes.cbegin(), buzzes.cend()))
            )
        )
        .map<std::string>(merge)
        ;
    return result;
}

من الجدير أن نوضح ذلك، حيث يمكنك أن تتعلم منه كيفية تجنب السؤال عن نوع الإرجاع الدقيق لهذه الوظيفة (لأنه يعتمد على تنفيذ الوظيفة وحدها، أي كيف يجمع الكود بين العدادين).

كما يوضح أنه كان علينا تحريك المتجهات fizzes و buzzes خارج نطاق الوظيفة بحيث تظل موجودة عندما تكون في الخارج في النهاية، تنتج الآلية البطيئة قيمًا.ولو لم نفعل ذلك، ل iterRange(..) كان من الممكن أن يقوم الكود بتخزين التكرارات على المتجهات التي اختفت منذ فترة طويلة.

وباستخدام تعريف بسيط جدا التقييم كسول، وهو ما لا يتم تقييم قيمة لحين الحاجة إليها، وأود أن أقول أن واحدا يمكن تنفيذ ذلك من خلال استخدام مؤشر وحدات الماكرو (للسكر النحو).

#include <stdatomic.h>

#define lazy(var_type) lazy_ ## var_type

#define def_lazy_type( var_type ) \
    typedef _Atomic var_type _atomic_ ## var_type; \
    typedef _atomic_ ## var_type * lazy(var_type);  //pointer to atomic type

#define def_lazy_variable(var_type, var_name ) \
    _atomic_ ## var_type _ ## var_name; \
    lazy_ ## var_type var_name = & _ ## var_name;

#define assign_lazy( var_name, val ) atomic_store( & _ ## var_name, val )
#define eval_lazy(var_name) atomic_load( &(*var_name) )

#include <stdio.h>

def_lazy_type(int)

void print_power2 ( lazy(int) i )
{
      printf( "%d\n", eval_lazy(i) * eval_lazy(i) );
}

typedef struct {
    int a;
} simple;

def_lazy_type(simple)

void print_simple ( lazy(simple) s )
{
    simple temp = eval_lazy(s);
    printf("%d\n", temp.a );
}


#define def_lazy_array1( var_type, nElements, var_name ) \
    _atomic_ ## var_type  _ ## var_name [ nElements ]; \
    lazy(var_type) var_name = _ ## var_name; 

int main ( )
{
    //declarations
    def_lazy_variable( int, X )
    def_lazy_variable( simple, Y)
    def_lazy_array1(int,10,Z)
    simple new_simple;

    //first the lazy int
    assign_lazy(X,111);
    print_power2(X);

    //second the lazy struct
    new_simple.a = 555;
    assign_lazy(Y,new_simple);
    print_simple ( Y );

    //third the array of lazy ints
    for(int i=0; i < 10; i++)
    {
        assign_lazy( Z[i], i );
    }

    for(int i=0; i < 10; i++)
    {
        int r = eval_lazy( &Z[i] ); //must pass with &
        printf("%d\n", r );
    }

    return 0;
}

وستلاحظ في print_power2 وظيفة هناك ماكرو تسمى eval_lazy الذي لا يفعل شيئا أكثر من dereference مؤشر للحصول على قيمة قبيل عندما يكون هناك حاجة فعلا. يتم الوصول إلى نوع كسول بالذرة، لذلك فمن الخيط آمنة تماما.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top