آشنایی با Transducers ها

اگر در حال یادگیری برنامه نویسی functional باشیم با map,filter,reduce آشنا میشیم و خیلی زود همه جا هم ازشون میخوایم استفاده کنیم و کلی باهاشون حال میکنیم و البته درسته این ۳ تا عالین وقتی داریم با یه آرایه کوچیک کار میکنیم ولی گاهی اوقات نیاز پیچیده تری داریم مثلا مجبوریم تعداد زیادی ازین ۳ رو با هم ترکیب کنیم یا مثلا چند تا آرایه داشته باشیم و آرایه ها هم سایزشون کوچیک نباشه اون موقع نه از کدمون راضی هستیم نه از سرعت اجرای برنامون. اینجاست که Transducer ها به کارمون میان و اگه بخوایم تعریف خیلی ساده ای ازشون بکنیم این هست:

«مبدل ها(Transducers) تابع هایی هستن که بدون ساختن آرایه و داده های واسطه داده مارو به چیزی که میخوایم تبدیل میکنن.»

زیاد واضح نبود نه پس فرض کنین آرایه ای با طول ۱۰۰ هزار داریم وقتی ما یه همچین کدی مینویسیم:

arr.map(func1).map(func2).map(func3).map(func4)

در واقع قبل از خروجی نهایی کد ما به ازای هر مپ یک آرایه جدید میسازه و منظور از آرایه واسطه همین هست. شکل گرافیکی همچین کاری بدین شکل میشه:


ولی ما همچین چیزی نمیخواستیم چون بهینه نیست اصلا بلکه میخواستیم داده نهایی بدون ساخت آرایه های واسطه داشته باشیم. یه همچین چیزی :

اگر بتونیم همچین کاری بکنیم خیلی خوب میشه نه ؟ قبل از رفتن سراغ این که چجوری این کارو بکنیم یه چیزی هم بگم که در این مورد بخصوص میشه از compose/pipe استفاده کرد ، pipe ها فانکشنی هستند که تعداد دلخواهی فانکشن رو میگیرن و یک فانکشن به عنوان خروجی به ما میدن که روند کارش بدین شکل هست که وقتی فانکشن رو call میکنیم شروع میکنه از چپ به راست خروجی فانکشن رو به فانکشن هایی بعدی پاس میده دقیقا مثل شکل بالا و کد بالا اینجوری میشه:

arr.map(pipe(func1,func2,func3,func4));
arr.map(compose(func4,func3,func2,func1));

راستی علاوه بر pipe ها ما compose رو داریم که دقیقا برعکس عمل میکنن یعنی به جای چپ به راست میان از راست به چپ ....


اگر فقط تعدادی map داشته باشیم استفاده از این ۲ کارمونو راه میندازه ولی همیشه که نمیخوایم map هارو chain کنیم گاهی نیازه اول یه مپ بعد یه فیلتر بعد دوباره مپ و درآخر reduce رو داشته باشیم. ولی شکل این ۳ تا با هم فرق میکنه پس نمیتونیم از compose/pipe استفاده کنیم. در نتیجه باید دنبال راه دیگه ای باشیم.

قدم اول

وقتی شکل این ۳ با هم فرق میکنه کاری نمیتونیم بکنیم پس باید کاری کنیم که همشکل بشن ازونجا که نمیشه فیلتر رو به شکل مپ یا برعکس نوشت یا reduce رو به شکل مپ یا فیلتر پس تنها راه ممکن اینه که map و filter رو به شکل reduce بنویسیم.

const mapReducer = f => (list,input) => [...list,f(input)];
const filterReducer = predicate => (list,input) => predicate(input) ? [...list,input] : list;

حالا فیلتر و مپ رو میتونیم بدین شکل بنویسیم:

arr.reduce(filterReducer(i=>i>15),[])
    .reduce(mapReducer(i=>i*2),[])

خب داریم نزدیک میشیم به چیزی که میخوایم پس میریم سراغ قدم دوم

قدم دوم

ما باید کاری کنیم که این چند reduce رو یکی کنیم اگر قرار بود reduce هامون هم با تعداد map,filter هامون یکی باشن که بیخودی خودمونو اذیت کردیم. پس باید کاری کنیم که کارو توی همون reduce اول تموم کنیم.


همونطور که دیدیم خروجی mapReducer و filterReducer یه reducing هست . reducing به فانکشنی گفته میشه که یه مقدار اولیه به همراه ورودی رو میگیره و یه خروجی میده همونکاری که + (جمع) انجام میده. وقتی مینویسیم ۴ + ۵ عدد پنج یه مقدار اولیست و ۴ هم ورودیمونه که روشون عملی انجام میدیم و خروجیمون میشه ۹. اینجا هم وقتی یه لیست و مقدار داریم و مقدار رو به انتهای لیست اضافه میکنیم و اونو برمیگردونیم همون reducing رو داریم انجام میدیم مگه نه؟ پس میتونیم مثل فاکتورگیری که تو ریاضی میکردیم اینجا هم بیایم reducing رو از mapReducer و filterReducer فاکتور بگیریم:

const mapping = f => reducing => (list,input) => reducing(list,f(input))
const filtering = predicate => reducing => (list,input) => predicate(input) ? reducing(list,input) : list

حالا علاوه بر فانکشن که میخوایم داده هارو تبدیل یا فیلتر کنیم باید reducing رو هم بهش بدیم در نتیجه حالا میتونیم filtering رو به mapping پاس بدیم چون filtering هم خروجیش یه reducing ئه پس همونطور که گفتیم کدی که بالا نوشتیم یه همچین چیزی میشه:

//Step 1
 arr.reduce(
    filtering(i=>i>15)((list,input) => [...list,input]),
    []
 ).reduce(
     mapping(i=>i*2)((list,input) => [...list,input]),
     []    
 )
 //Step 2
 arr.reduce(
    filtering(i=>i> 15)
       (mapping(i=> i*2 )
          ((list,input) => [...list,input])
        ),
    []
)

خب درست شد حالا با یه reduce و بدون ایجاد آرایه واسط تونستیم به هدفمون برسیم ولی مشکلی که هست اینه که نوشتن اینجوری خیلی سخته اینجوری نمیشه کار کرد پس میریم قدم بعدی.

قدم سوم

تا اینجا راه زیادی رو اومدیم ولی هنوز مشکلاتی داریم یکیش اینه که نوشتن اینجور کد سخته و وقتی تعداد reducer ها زیاد بشه واقعا آزاردهنده میشه و بعدی این که اگر قرار باشه این کار رو چند بار انجام بدیم باز بدردسر میافتیم و ما قرار بود کاری کنیم که توی دردسر نیفتیم دیگه درسته ؟

ما وقتی کد step 2 رو میبینیم که یه فانکشن ورودی به فانکشن بعدی پاس داده شده و به همین ترتیب... یاد compose/pipe ها میافتیم پس ازشون استفاده میکنیم و کد رو به شکل زیر تبدیل میکنیم:

const transducer = pipe(mapping(i=> i*2 ), filtering(i=>i> 15))
//const transducer = compose(filtering(i=>i> 15) , mapping(i=> i*2 ))
const listCombine = (list, input) => [...list,input];
const greaterAndDoubleReducer = transducer(listCombine);
arr.reduce(greaterAndDoubleReducer);

خیلی خوب شد نه ؟ تقریبا کارمون تمومه و میریم قدم بعدی و اخر تا دیگه نیاز نباشه هردفعه اینارو بنویسیم.

قدم آخر

حالا میایم این رو به صورت فانکشن می نویسیم.

 const transduce = ( transducer, reducing, initial, input) => input.reduce(transducer(reducing), initial);

و اینجوری ازش استفاده می کنیم :

 const transducer = pipe(mapping(i=> i*2 ), filtering(i=>i> 15))
 // const transducer = compose(filtering(i=>i> 15) , mapping(i=> i*2 ))  
 const listCombine = (list,input) => [...list,input];
 transduce(transducer ,listCombine,[],arr);

خب یه transducer ساده نوشتیم و فهمیدیم چجوری کار میکنن حال میتونیم از کتابخونه هایی نظر transducer.js استفاده کنیم.


در این پست از ۲ فانکشن استفاده کردیم که شاید باهاشون آشنایی نداشته باشین compose و pipe که کار مارو خیلی راحت کردن و این فانکشن ها رو هم میتونیم خودمون بنویسیم هم از فانکشن هایی که کتابخونه های functional مثل Ramda در اختیارمون میزارن استفاده کنیم.

برای پیاده سازیشون اگه کنجکاوین همونطور که اشاره شد کارشون اینه که تعداد فانکشن همشکل می گیرن و یه فانکشن خروجی میدن بهمون که با کال کردنش به ترتیب فانکشن ها رو اجرا و خروجیشون رو به عنوان ورودی به فانکشن بعدی میدن و در نهایت خروجی فانکشن اخر رو به ما میدن و فرقشون تنها این هست که pipe از چپ به راست و compose از راست به چپ هست. پس میایم با کمک reduce ها این ۲ رو پیاده سازی میکنیم.

const identity = x => x; 
const pipe = (...funcs) => funcs.reduce((acc,curr) => x => curr(acc(x)) , identity);
const compose = (...funcs) => funcs.reduce((acc,curr)=> x => acc(curr(x)),identity);