کاتلین از ابتدا : inline functions

مقدمه

قسمتی که امروز میخوایم در موردش صحبت کنیم در مورد inline functions هست، که کمی مشکل هست و نیازمند توجه و دقت بیشتر :) .

در حالت معمول هروقت که یک لامبدا رو میسازید به یک anonymous class کامپایل میشه. درنتیجه این به این معنیه که هر وقت از lambda expression استفاده میکنید، یک کلاس اضافه خواهیم داشت. و توی هر فراخوانی ( invoke کردن ) یک آبجکت جدید ساخته میشه. که این مساله باعث ایجاد runtime overhead میشه. به همین دلیل استفاده از یک تابع معمولی که حجم کد یکسانی نسبت به لامبدا داره، ( در شرایطی که هر دو کار یکسانی انجام میدن ) انتخاب بهتری خواهد بود.

عایا ممکنه که بتونیم کدی رو کامپایل کنیم که اون کده در عین حالی که مثل جاوا کارآمد هست، بشه اون منطق و لاجیکی که تکراری هم هست رو، توی تابع مقصد پیاده‌سازی کنیم ( جلوتر بریم منظور من از این سوال رو بیشتر متوجه خواهید شد ) ؟

جواب این سوال مثبته، در حقیقت کامپایلر کاتلین میتونه این کارو برای شما انجام بده، برای این هدف، شما باید از واژه‌ی inline قبل از متد مورد نظرتون استفاده کنید، با این کار کامپایلر به جای این که هر بار که میخواد از تابع استفاده کنه، به جای اینکه بیاد و یک تابع رو تولید ( یا به قول معروف generate ) کنه، موقع فراخوانی میاد تابع مبدأ و نوع پیاده‌سازی اون رو فراخوانی میکنه و مورد استفاده قرار میده. حالا در ادامه با جزییات و مثال‌های بیشتر نحوه‌ی کار این نوع تابع رو توضیح میدیم.

اصل مطلب

بحث رو با inline شروع می‌کنیم، کلمه‌ی کلیدی inline فوق العاده انعطاف‌پذیر هست و نکات ظریف و ریزی هم در موردش وجود داره که نمی‌خوام همه‌ی اونا رو توی این قسمت پوشش بدم، مثلا اینکه inline نوع متغییر رو بررسی میکنه...، خب بدون اتلاف وقت وارد اصل موضوع میشیم، خب کلمه‌ی کلیدی inline یعنی چی؟ inline یک کلمه‌ی کلیدی هست که شما در کاتلین میتونید اون رو به ابتدای توابع اضافه کنید، وقتی که شما این کلمه‌ی کلیدی رو به ابتدای تابع اضافه می‌کنید کامپایلر همه‌ی کد رو از سر جای اصلیش کپی کرده و اون رو به جایی که از اونجا فراخوانی شروع میشه منتقل میکنه، اصولا هدف از inline کردن، حذف یک نوع runtime overhead ( سربار زمان اجرا ) هست. اینجوری براتون بگم که اصولا وقتی که یک higher order function یا لامبدا رو به یک تابع میفرستیم، یک anonymous class طرف مقصد ساخته میشه،‌ و همین عمل ( مخصوصا وقتی که توی یک حلقه چندین بار تکرار بشه ) میتونه سربار زیادی رو ایجاد کنه،‌ به خاطر همین ما با inline کردن از ایجاد کلاس‌های اضافی برای لامبداها جلوگیری می‌کنیم. به بیان بهتر در یک تابع inline ،‌ در هنگام کامپایل شدن، بدنه‌ی تابع به طور کامل در سمت مقصد عینا کپی میشه. inline کردن قوانین خودش رو داره و نکات ظریف و مهم نسبتا زیادی رو هم با خودش داره که در ادامه در موردشون بیشتر صحبت می‌کنیم.

البته یک سوال میشه که چرا نباید همیشه از inline ، برای توابع معمولی استفاده کنیم؟، inline کردن وقتی بهترین عملکرد رو برای ما داره که اون رو همراه با توابعی قرار بدیم که higher order function هستن و همچنین لامبدا رو دریافت میکنن. در غیر این صورت inline کردن یک تابع معمولی هیچ فایده‌ای برای ما نخواهد داشت. و مثلا وقتی که داخل Java Intellij یک تابع معمولی رو inline میکنیم،‌ با هشدار پایینی مواجه میشیم :

Warning:(123, 4) Kotlin: Expected performance impact of inlining '...' can be insignificant. Inlining works best for functions with lambda parameters

برای درک بهتر مکانیسم inline functions یه نگاهی به کد پایینی ( که inline نشده ) بندازید :

https://gist.github.com/sajjadyousefnia/79e075ec833d72c7485577a24cc0d930

استفاده از تابع استاتیک nonInlinedFilter یک راه برای فیلتر کردن لیستی از متغییرهای integer هست، در حقیقت اگه بخوایم بالایی رو دیکامپایل کنیم، به صورت زیر در میاد :

https://gist.github.com/sajjadyousefnia/4aab7221b12c846632c97582766ce1dd

همونطور که مشاهده میکنید، پارامتر لامبدایی تبدیل به یک instance از اینترفیس Function1 شده. پس این معنیش اینه که با دریافت یک لامبدا یک کلاس و یک instance جنریت یا تولید شده.

خب، حالا نگاه میکنیم تا ببینیم اگه همون کد رو inline کنیم، چه اتفاقی می‌افته؟

https://gist.github.com/sajjadyousefnia/857584ce28ce012a045b9c519d2d97c6

ورژن دیکامپایل شده‌ی اون به صورت زیر هست :


https://gist.github.com/sajjadyousefnia/bf5d427caeb3c5b2edb0929e666c25ee

اگه دقت کنید میبینید که کد inlinedFilter تغییری نکرده و مثل همون قبلیه هست، به همین خاطره که میگیم استفاده از inline کردن در کدهای معمولی، باعث ایجاد تغییری نخواهد شد. یک نکته‌ی دیگه‌ای هم که وجود داره اینه که اگه دقت کرده‌باشین، بدنه‌ی تابع inlinedFilter کپی میشه و میره به جایی که قراره تابع از اونجا صدا زده بشه، اصل کاری که توی inline کردن انجام میشه، همینه. به خاطر اینکه مبدأ و مقصد جاشون رو با هم عوض میکنن، دیگه نیازی به ساخت instance برای کلاس anonymous در مقصد نداریم.

پس تا اینجای قصه موضوعات زیر رو می‌تونیم نتیجه بگیریم :

1- توی یک inlined function بدنه‌ی تابع در مقصد قرار داده میشه ( و اصطلاحا inline میشه )

2- چون توی یک inlined function بدنه‌ی تابع در مقصد قرار داده میشه، دیگه در مقصد نیازی به ساخت instance از این تابعمون نداریم دیگه.

3- درحقیقت، چون مقدار کدمون دو برابر میشه، حجم کد بیشتری خواهیم داشت.

4- تابع inline شده، از نظر رفتاری تفاوتی با تابع معمولی در جاوا نداره.

خب حالا به یک مثال دیگه‌ای نگاه میندازیم، ایندفعه به جای استفاده از لامبدا، از یک لامبدا از یک function refrence استفاده می‌کنیم.

https://gist.github.com/sajjadyousefnia/e5ccf8a088ce85d25783444dd53d7bf8

ورژن دیکامپایل شده‌ی کد بالایی به صورت زیر هست :

https://gist.github.com/sajjadyousefnia/b1b059ca59ba49e280252d3884192b99

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

خب حالا به یک مثال دیگه‌ای نگاه میندازیم :

https://gist.github.com/sajjadyousefnia/dc921bb20656d4c673c24fdd6ce399a2

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

https://gist.github.com/sajjadyousefnia/b6baabf3cba5cb095e3130b87d4e3306

همه‌ی نتیجه‌گیری‌ها و خلاصه نکاتی که گفتیم تا اینجا همشون درست در اومدن، ولی باید یک نکته‌ي دیگه‌ای رو هم به این نکات اضافه کرد. البته دقت کنید که پارامتر لامبدایی توی مقصد یعنی ( call-site ) دیگه inline نمیشه. دلیل این اینه که قبل از اینکه inlined function رو بخوایم فراخوانی کنیم، ازش یک instance درست شده قبلا. کامپایلر هم اطلاعات کافی‌ای در مورد اینکه بدنه‌ی لامبدا چجوری هست نداره و تنها کاری هم که از دستش برمیاد اینه که عمل invoke رو انجام بده. بنابراین نتیجه گیری‌ها و نکات رو تا اینجای کار به صورت زیر آپدیت می‌کنیم.

1- توی یک inlined function بدنه‌ی تابع در مقصد قرار داده میشه ( و اصطلاحا inline میشه )

2- چون توی یک inlined function بدنه‌ی تابع در مقصد قرار داده میشه، دیگه در مقصد نیازی به ساخت instance از این تابعمون نداریم دیگه.

3- درحقیقت، چون مقدار کدمون دو برابر میشه، حجم کد بیشتری خواهیم داشت.

4- تابع inline شده، از نظر رفتار تفاوتی با تابع معمولی در جاوا نداره.

5- وقتی که توی call-site ( یعنی جای مقصد ) بدنه‌ی تابعمون ( یعنی منظورم اینه که تابع چه نوع پارامتری میگیره و چه نوع آرگومانی رو بر می‌گردونه ) رو تعریف نکرده باشیم، این تابعه inline نخواهد شد.

حالا فکر می‌کنید اگه تابع lambdaInstance رو inline کنیم، چه اتفاقی میفته؟ وقتشه یه نگاهی بهش بندازیم ببینیم چطوری خواهد بود؟

https://gist.github.com/sajjadyousefnia/edf137f1f07adcbb253465104df4eab0

همونطور که انتظار می‌رفت اتفاق افتاد، کد inlinedFilter توی lambdaInstance ه inline شد، و بدنه‌ی کدمون از inlinedFilter به lambdaInstanceTest منتقل شد. پس تا اینجا میتونیم ششمین نتیجه رو هم بگیریم.

1- توی یک inlined function بدنه‌ی تابع در مقصد قرار داده میشه ( و اصطلاحا inline میشه )

2- چون توی یک inlined function بدنه‌ی تابع در مقصد قرار داده میشه، دیگه در مقصد نیازی به ساخت instance از این تابعمون نداریم دیگه.

3- درحقیقت، چون مقدار کدمون دو برابر میشه، حجم کد بیشتری خواهیم داشت.

4- تابع inline شده، از نظر رفتار تفاوتی با تابع معمولی در جاوا نداره.

5- وقتی که توی call-site ( یعنی جای مقصد ) بدنه‌ی تابعمون ( یعنی منظورم اینه که تابع چه نوع پارامتری میگیره و چه نوع آرگومانی رو بر می‌گردونه ) رو تعریف نکرده باشیم، این تابعه inline نخواهد شد.

6- میتونیم یک پارامتر inline شده رو به یک تابع inline دیگه بفرستیم.

خب حالا یک نگاه نزدیک‌تری به پارامترهای inline شده میندازیم. فهمیدیم که میتونیم اونا رو داخل یک inline function ه invokeشون کنیم. پس همین نتیجه رو هم به لیست اضافه می‌کنیم.

7- میتونیم پارامترهای inline شده رو داخل یک inline function دیگه فراخوانی کنیم.

خب حالا اگه اونا رو داخل یک متغییر ذخیره کنیم تا بعدا ازشون استفاده کنیم، چی میشه؟ برای همین کد زیر رو امتحان می‌کنیم و نگاه می‌کنیم ببینیم چی میشه؟

https://gist.github.com/sajjadyousefnia/ea1a922ea18314c8045b6e87653c44d5

خب کامپایلر به ما خطا یا همون ارور زیر رو میده :

Error:(12, 34) Kotlin: Illegal usage of inline-parameter 'predicate' in 'public inline fun inlinedFilter(list: List<Int>, predicate: (Int) -> Boolean): List<Int> defined in functiontypes in file Inline.kt'. Add 'noinline' modifier to the parameter declaration

این اتفاق به این دلیل رخ میده که عمل inline کردن رو فقط میشه رو بدنه‌ی لامبداهایی که به صورت سنکرون ( همزمان ) فراخوانده شدن انجام داد ولاغیر. برای ذخیره کردن یک instance از یک لامبدا به یک آبجکت نیاز داریم که این در تناقض با هدف inline کردن هست ( به خاطر اینکه ما inline می‌کنیم تا کمتر instance بسازیم، حالا اگه بخوایم این کارو انجام بدیم که راهو برعکس رفتیم :| ). پس حالا هشتمین نتیجمون رو میگیریم و همه‌ی نتایج رو خدمت شما می‌نویسم.

1- توی یک inlined function بدنه‌ی تابع در مقصد قرار داده میشه ( و اصطلاحا inline میشه )

2- چون توی یک inlined function بدنه‌ی تابع در مقصد قرار داده میشه، دیگه در مقصد نیازی به ساخت instance از این تابعمون نداریم دیگه.

3- درحقیقت، چون مقدار کدمون دو برابر میشه، حجم کد بیشتری خواهیم داشت.

4- تابع inline شده، از نظر رفتار تفاوتی با تابع معمولی در جاوا نداره.

5- وقتی که توی call-site ( یعنی جای مقصد ) بدنه‌ی تابعمون ( یعنی منظورم اینه که تابع چه نوع پارامتری میگیره و چه نوع آرگومانی رو بر می‌گردونه ) رو تعریف نکرده باشیم، این تابعه inline نخواهد شد.

6- میتونیم یک پارامتر inline شده رو به یک تابع inline دیگه بفرستیم.

7- میتونیم پارامترهای inline شده رو داخل یک inline function دیگه فراخوانی کنیم.

8- ما نمی‌تونیم که از پارامترهای inline شده instance بسازیم و به قولی یک آبجکت از نوع اونا رو ایجاد کنیم.

خب امیدوارم با قضیه‌ی inline کردن آشنا شده‌باشید، اگه در مورد همین موضوع سوالی داشتید همین پایین بپرسید، اگه بلد بودم که جواب میدم اگر که نه میرم دنبالش تا جوابشو پیدا کنم :) .