توابع محدوده‌بندی (Scope Functions) در کاتلین - apply, with, let, also, run

برنامه‌نویسی عملکردی (functional) به خوبی توسط سینتکس و کتابخانه استاندارد کاتلین پشتیبانی می‌شود. در این پست به تشریح پنج تابع مرتبه بالا (higher-order) در زبان کاتلین، یعنی apply ،with ،let ،also و run، می‌پردازیم.هنگام یادگیری این پنج تابع باید دو چیز را به خاطر بسپارید: زمان و چگونگی استفاده از آن‌ها. به این دلیل که با توجه به ماهیت مشابه این پنج تابع، در ابتدا ممکن است وجود هر پنج تای آن‌ها غیر ضروری به نظر برسد.در این پست ما ابتدا مشترکات این پنج تابع را بررسی می‌کنیم. سپس با تفاوت‌های آن‌ها آشنا می‌شویم و در آخر موارد استفاده‌ی آن‌ها را یاد خواهیم گرفت.آن‌ها چکار می‌کنند؟همه‌ی این پنج تابع اساسا یک کار انجام می‌دهند. آنها توابع محدوده‌بندی هستند که یک آرگومان گیرنده و یک قطعه کد می‌گیرند و بعد قطعه کد ارائه شده را بر روی گیرنده اجرا می‌کنند.بیایید با نحوه کار یکی از این توابع آشنا شویم. تابع with به صورت زیر تعریف شده است:inline fun <T, R> with(receiver: T, block: T.() -> R): R { return receiver.block() }با استفاده از این تابع می‌توان کد را مختصرتر کرد. ابتدا بیایید مقداری کد معمولی بدون استفاده از توابع محدوده‌بندی ببینیم:class Person { var name: String? = null var age: Int? = null } val person: Person = getPerson() print(person.name) print(person.age)قطعه کدی که در ادامه می‌بینید معادل کد بالا است با این تفاوت که در این‌جا از تابع with جهت حذف ارجاع‌های مکرر به متغیر person استفاده کرده‌ایم:val person: Person = getPerson() with(person) { print(name) print(age) }بسیار عالی! پس چه نیازی به چهار تابع دیگر است؟تفاوت بین apply ،with ،let ،also و runاگرچه این توابع یک کار انجام می‌دهند، اما تفاوت هایی نیز در امضا و نحو پیاده‌سازی آن‌ها، وجود دارد. این تفاوت‌ها مشخص می‌کند که چگونه باید از این توابع استفاده کرد.بیایید تابع with را با امضا و پیاده‌سازی یک تابع دیگر مانند also، مقایسه کنیم:inline fun <T, R> with(receiver: T, block: T.() -> R): R { return receiver.block() } inline fun <T> T.also(block: (T) -> Unit): T { block(this) return this }تابع with و also در سه مورد با هم تفاوت دارند:در تابع with، آرگومان گیرنده با یک پارامتر صریح از نوع T مشخص شده است. این در حالی است که در تابع also ما گیرنده‌ی ضمنی T را داریم.آرگومان قطعه کد (block) در تابع with، به صورت یک تابع با گیرنده‌ی ضمنی T تعریف می‌شود. در صورتی که تابع also یک پارامتر صریح از نوع T دارد.تابع with هر چه را که اجرای آرگومان قطعه کد برمیگرداند، برمیگرداند. ولی تابع also همان آبجکتی را که به عنوان گیرنده به آن داده شده بود را برمی‌گرداند.به خاطر این سه تفاوت از تابع also باید به شکل دیگری استفاده کرد:val person: Person = getPerson().also { print(it.name) print(it.age) }این قطعه کد، یک Person توسط تابع getPerson گرفته و آن را در متغیر person می‌ریزد. اما قبل از آن تابع also نام و سن person گرفته شده را چاپ می‌کند.در مورد سایر توابع (apply، let، run) چطور؟ تمامی آن‌ها در یکی از سه مورد بالا با یکدیگر تفاوت دارند.پارامتر گیرنده‌ی صریح یا گیرنده‌ی ضمنیآرگومان قطعه کد به صورت یک پارامتر صریح تعریف می‌شود یا به صورت یک گیرنده‌ی ضمنیبرگرداندن گیرنده یا برگرداندن آن چه آرگومان قطعه کد برمی‌گردانددر این‌جا تعریف هر پنج تابع را می‌بینید:inline fun <T, R> with(receiver: T, block: T.() -> R): R { return receiver.block() } inline fun <T> T.also(block: (T) -> Unit): T { block(this) return this } inline fun <T> T.apply(block: T.() -> Unit): T { block() return this } inline fun <T, R> T.let(block: (T) -> R): R { return block(this) } inline fun <T, R> T.run(block: T.() -> R): R { return block() }کی از کدام تابع استفاده کنیم؟حالا ما تفاوت‌های این پنج تابع را می‌دانیم ولی همچنان نمی‌دانیم کی از کدام تابع استفاده کنیم. این توابع ماهیت مشابهی دارند و اغلب قابل تعویض هستند.تعدادی روش ایده‌آل (Best practice) در مستندات کاتلین موجود است. با یادگیری این روش‌ها، هم می‌توانید کد‌های بهتری بنویسید و هم کد‌های دیگر برنامه نویسان را بهتر متوجه می‌شوید.روش ایده‌آل استفاده از تابع applyاز apply زمانی استفاده کنید که نیازی به دسترسی به تابعی از گیرنده ندارید و همچنین می‌خواهید همان گیرنده را برگردانید. این اتفاق معمولا زمانی می‌افتد که می‌خواهید شی جدید بسازید. برای مثال قطعه کد زیر را ببینید:val peter = Person().apply { // only access properties in apply block! name = &quotPeter&quot age = 18 }بدون استفاده از apply کد به این شکل در می‌آید:val clark = Person() clark.name = &quotClark&quot clark.age = 18روش ایده‌آل استفاده از تابع alsoاگر کد شما به پارامتر گیرنده خود دسترسی پیدا نمی کند ، یا اگر پارامتر گیرنده خود را تغییر ندهد از این تابع استفاده کنید. به عنوان مثال، استفاده از این تابع هنگام اجرای برخی از عوارض جانبی روی یک شی یا تایید اعتبار داده های آن قبل از اختصاص دادن آن به یک property بسیار مفید است:class Book(author: Person) { val author = author.also { requireNotNull(it.age) print(it.name) } }قطعه کد بالا بدون استفاده از also:class Book(val author: Person) { init { requireNotNull(author.age) print(author.name) } }روش ایده‌آل استفاده از تابع letاز تابع let در یکی از مواقع زیر استفاده کنید:اجرای قطعه کدی تنها اگر مقدار داده شده null نباشدتبدیل یک شی nullable به یک شی nullable دیگرمحدود کردن محدوده‌ی یک متغیر محلیgetNullablePerson()?.let { // only executed when not-null promote(it) } val driversLicence: Licence? = getNullablePerson()?.let { // convert nullable person to nullable driversLicence licenceService.getDriversLicence(it) } val person: Person = getPerson() getPersonDao().let { dao -> // scope of dao variable is limited to this block dao.insert(person) }کد زیر معادل کد بالاست بدون استفاده از let:val person: Person? = getPromotablePerson() if (person != null) { promote(person) } val driver: Person? = getDriver() val driversLicence: Licence? = if (driver == null) null else licenceService.getDriversLicence(it)val person: Person = getPerson() val personDao: PersonDao = getPersonDao() personDao.insert(person)روش ایده‌آل استفاده از تابع withاز این تابع زمانی استفاده کنید که گیرنده غیر null است و نیازی به نتیجه ندارید:val person: Person = getPerson() with(person) { print(name) print(age) }کد بالا بدون استفاده از with:val person: Person = getPerson() print(person.name) print(person.age)روش ایده‌آل استفاده از تابع runاز این تابع زمانی استفاده کنید که می‌خواهید مقداری را حساب کنید یا اینکه محدوده‌ی چند متغیر محلی را محدود‌‌تر کنید. همچنین از این تابع زمانی که می‌خواهید پارامتر‌های صریح را به گیرنده‌ی ضمنی تبدیل کنید.val inserted: Boolean = run { val person: Person = getPerson() val personDao: PersonDao = getPersonDao() personDao.insert(person) } fun printAge(person: Person) = person.run { print(age) }کد زیر برابر با کد بالاست بدون استفاده از تابع run:val person: Person = getPerson() val personDao: PersonDao = getPersonDao() val inserted: Boolean = personDao.insert(person)fun printAge(person: Person) = { print(person.age) }ترکیب کردن چند تابع محدوده‌بندیبخش‌های قبل نشان دادند که چگونه توابع محدوده‌بندی کد را خواناتر می‌کنند. بعضی مواقع ترکیب چند تابع محدوده‌بندی در یک قطعه کد وسوسه‌انگیز است.هنگامی که توابع محدوده تو در تو قرار می گیرند، کد به سرعت گیج کننده می شود. به عنوان یک قانون، سعی کنید از توابع محدوده‌بندی‌ای که آرگومان گیرنده‌شان را با گیرنده‌ی قطعه لامبدا متصل می‌کنند (apply، run، with) به شکل تو در تو استفاده نکنید. زمانی که از دیگر توابع (let، also) به شکل تو در تو استفاده می‌کنید، یک نام صریح برای پارامتر قطعه لامبدا مشخص کنید. یعنی از پارامتر ضمنی it استفاده نکنید.در کنار تو در تو استفاده کردن، توابع محدوده‌بندی می‌توانند به صورت زنجیره هم استفاده شوند. بر خلاف استفاده تو در تو در این حالت نه تنها از خوانایی کد کم نمی‌شود بلکه کد خواناتر هم می‌شود.در این‌جا مثالی از استفاده از این توابع به صورت زنجیره‌ای را می‌بینیم:private fun insert(user: User) = SqlBuilder().apply { append(&quotINSERT INTO user (email, name, age) VALUES &quot) append(&quot(?&quot, user.email) append(&quot,?&quot, user.name) append(&quot,?)&quot, user.age) }.also { print(&quotExecuting SQL update: $it.&quot) }.run { jdbc.update(this) > 0 }قطعه کد بالا یک تابع dao، برای درج یک user در دیتابیس را نشان می‌دهد. در تابع insert از قابلیت expression body کاتلین استفاده شده است. با این حال به خوبی دغدغه‌ها تفکیک شده‌اند: آماده‌سازی SQL، لاگ کردن SQL، و در نهایت اجرای SQL. در نهایت این تابع مقداری از نوع boolean که موفقیت یا عدم موفقیت عملیات درج را نشان می‌دهد را بر می‌گرداند.منبع