آموزش مختصر اسکالا برای توسعهدهندگان جاوا

مقدمه
در جاواکاپ مطلبی در مورد چندپارادایمی بودن ماشین مجازی جاوا مطالعه کردیم، به طور خلاصه گفته شد که به خاطر کامپایل شدن تمام زبانهای اسکالا، Groovy، کلوژر و کاتلین به بایتکد و اجرا توسط ماشین مجازی جاوا، این امکان وجود دارد که برای قسمتهای مختلف فضای مسئله خود، از زبانهای مختلف استفاده کنیم. یکی از مطرحترین و پرکاربردترین زبانهای بر پایه ماشین مجازی جاوا، اسکالا است. در این مطلب با اسکالا آشنا شده و مزایا و معایب آن را بررسی میکنیم.
اسکالا به عنوان یک زبان بر پایه ماشین مجازی جاوا و نوع داده ایستا در ژانویه سال ۲۰۰۴ میلادی توسط مارتین اردرسکی معرفی شد. اسکالا هم برنامهنویسی شیگرا و هم تابعی را پشتیبانی میکند. از معروفترین پروژههای توسعه دادهشده با این زبان میتوان به آپاچی اسپارک، آپاچی کافکا و آپاچی فلینک اشاره کرد. در لیست محبوبیت زبانها هم اسکالا جایگاه خوبی دارد. (رتبه ۱۳ ام)
مزایای اسکالا: آن چه که اسکالا را عالی میکند
سینتکس مختصر
اسکالا برای مختصر بودن طراحی شده است. بسیاری از تصمیمات طراحی در آن، با هدف بهبود مشکل طولانی بودن کدهای جاوا گرفته شدهاند. برای مثال، کد زیر یک کلاس با نام UserInfo را نشان میدهد که دو فیلد دارد. فیلد اول (نام) هم قابل خواندن و هم تغییر دادن (read-write) است. فیلد دوم تاریخ تولد است که فقط قابل خواندن است.
کد جاوا:
class UserInfo { private String name; private LocalDate birthDate; public UserInfo(String name, LocalDate birthDate) { this.name = name; this.birthDate = birthDate; } public String getName() { return name; } public void setName(String name) { this.name = name; } public LocalDate getBirthDate() { return birthDate; } }
و کد اسکالا:
class UserInfo(var name: String, val birthDate: LocalDate)
اینجا خوب است کمی در مورد سینتکس توضیح دهیم. در اسکالا اسامی متغیرها ابتدا میآیند و سپس نوع متغیر میآید. کلیدواژهی var برای تصریح متغیر است. این متغیر یک ارجاع قابل تغییر به یک مقدار است. در طرف دیگر کلیدواژه val را داریم که یک ارجاع غیرقابل تغییر را مشخص میکند. در یک خط میتوانید شفافیت سینتکس اسکالا را مشاهده کنید.
کلاسهای Case
عملگر مقایسه اشیا در جاوا، رفرنسها را مقایسه میکند. برای مثال کد زیر false برمیگرداند.
LocalDate date = LocalDate.now(); UserInfo a = new UserInfo("John", date); UserInfo b = new UserInfo("John", date); return (a == b);
اما گاهی میخواهیم واقعا مقادیر اشیا را مقایسه کنیم و نه رفرنسشان را. برای اینکار در جاوا متدهای equals و hashcode را پیادهسازی میکنیم. در اسکالا از Case Classها استفاده میکنیم. در استفاده از Case Class، اسکالا به طور خودکار متدهای equals و hashcode و سازنده و getter و setter را ایجاد میکند. مورد مهمتر اینکه با تعریف کلاس خود به عنوان Case Class، میتوانیم از تطبیق الگو استفاده کنیم.
با پیادهسازی کلاس UserInfo به عنوان Case Class نتیجه زیر را میگیریم:
case class UserInfo(var name: String, birthDate: LocalDate)val date = LocalDate.now() val a = new UserInfo("John", date) val b = new UserInfo("John", date) a == b // returns True
تطبیق الگو
تطبیق الگو (یا pattern matching) مکانیسمی برای بررسی یک مقدار در یک الگو است. میتوان به مکانیسم تطبیق الگو، به عنوان یک نسخه قدرتمندتر از سوییچ در جاوا نگاه کرد. دستور match یک مقدار ورودی میگیرد، یک کلیدواژه match
دارد و حداقل یک عبارت case
. مثال:
def matchTest(x: Int): String = x match { case 1 => "one" case 2 => "two" case _ => "many" }
همچنین امکان تطبیق الگو روی نوع داده هم وجود دارد:
def matchOnType(x: Any): String = x match { case x: Int => s"$x is integer" case x: String => s"$x is string" case _ => "unknown type" } matchOnType(1) // returns 1 is integer matchOnType("1") // returns 1 is string
و یک مثال از ویژگیهای قدرتمندتر هم ببینیم: تطبیق الگو روی لیستی از اعداد صحیح:
def matchList(x: List[Int]): String = x match { case List(_) => "a single element list" case List(_, _) => "a two elements list" case List(1, _*) => "a list starting with 1" } matchList(List(3)) // returns a single elements list matchList(List(0, 1)) // returns a two elements list matchList(List(1, 0, 0)) // returns a list starting with 1
و نهایتا، قدرتمندترین حالت را ببینیم: تطبیق الگو روی Case Classها:
case class UserInfo(var name: String, birthDate: LocalDate) def isUserJohn(x: Any): Boolean = x match { case UserInfo("John", _) => true case _ => false } val list = List( "wrong user type", UserInfo("Ben", LocalDate.now()), UserInfo("John", LocalDate.now())) list.filter(isUserJohn) // list with one element UserInfo John
کلاسهای ضمنی
کلیدواژه implicit
باعث میشود که اجازه استفاده از سازندهی اصلی کلاس برای تبدیل نوعِ ضمنی، وقتی که این کلاس در محدوده قابلدسترسی است، داده شود.
فرض کنید که از شما خواسته شده که به کلاس UserInfo، یک متد getAge اضافه کنید. برای این کار ۲ راه پیش رو دارید. راه اول اینکه یک کتابخانه جدا برای متدهای کمکی UserInfo به نام UserInfoUtil ایجاد کنید. یا این که یک کلاس فرزند از UserInfo ایجاد کنید که همه ویژگیهای آن را ارثبری کند و getAge را هم به آن اضافه کند.
در اسکالا شما میتوانید رفتارهای دلخواه خودتان را با استفاده از کلاسهای ضمنی اضافه کنید.
object Extensions { implicit class UserInfoExt(user: UserInfo) { def getAge(): Int = { Years.yearsBetween(LocalDate.now(), user.birthDate,).getYears } } }
این به شما اجازه میدهد کدی مانند کد زیر بنویسید:
import Extensions._ val user = new UserInfo("John", LocalDate.now()) user.getAge()
توابع مرتبه بالا
اسکالا تعریف توابع مرتبه بالا را امکانپذیر میکند. اینها توابعی هستند که توابع دیگر را به عنوان ورودی میگیرند و یا نتیجهشان یک تابع است. مثال معمول از توابع مرتبه بالا، map و filter هستند.
تابع map یک تابع را روی همه عناصر یک کالکشن اعمال میکند. برای مثال، بیاید همهی اعضای یک لیست را ضربدر دو کنیم.
def multiply(x: Int): Int = x * 2 List(1, 2, 3).map(multiply) // returns 2 4 6
تابع filter یک لیست از اعضایی میسازد که تابع ورودی به ازای آنها true برگردانده. این یک مثال کوتاه و مختصر است:
def isEven(x: Int): Boolean = x % 2 == 0 List(1, 2, 3).filter(isEven) // returns 2
و بیایید تابع مرتبه دوم خودمان را تعریف کنیم:
def apply(f: Int => String, x: Int): String = f(x) def printInt(x: Int): String = s"Printing integer $x" apply(printInt, 3)
مونادِ Option
موناد یک الگوی طراحی است که اجازهی ساختیافته کردن برنامه به صورت کلی را میدهد. در همین حال نیاز به کد تکراریای که برای منطق برنامه لازم است را از بین میبرد و راه آسان و سرراستی برای ترکیب عملیاتها روی دادهها فراهم میکند.
(یادداشت مترجم: موناد یا Monad مبحث پیچیدهای است و برای فهم عمیق آن نیاز به مطالعات بیشتر مثلا در زمینه «نظریه انواع» داریم، بنابراین اگر با جمله بالا کاملا متوجه موناد نشدید جای نگرانی نیست.)
اینجا قرار نیست درباره تئوری مونادها عمیق شویم. هدف من این است که نشان دهم چگونه موناد Option میتواند به «مقابله با خطاها» کمک کند که یکی از مشکلات مرسوم در زبانهای برنامهنویسی است.
کد زیر را در نظر بگیرید:
class Document { def getActivePage: Page = ??? } class Page { def getSelectedText: String = ??? }
هدف این است که متد getSelectedTextLength را پیادهسازی کنیم که هدفش محاسبه متن انتخابشده در صفحه کنونی است. در غیر این صورت صفر بر گرداند.
راه ساده برای پیادهسازی این مورد، کدی شبیه به کد زیر است:
def getSelectedTextLength(doc: Document): Int = { if(doc != null) { val page = doc.getActivePage if(page != null){ val text = page.getSelectedText if(text != null){ text.length } else 0 } else 0 } else 0 }
این پیادهسازی خوب به نظر میرسد اما مشکل تورفتگیهای زیاد دارد. بیشتر بخوانید: pyramid of doom
راه دیگر برای پیادهسازی این متد، چیزی شبیه به کد زیر است:
def getSelectedTextLength(doc: Document): Int = { if(doc == null) return 0 val page = doc.getActivePage if(page == null) return 0 val text = page.getSelectedText if(text == null) return 0 text.length }
این کد تمیز و بدون تورفتگی است اما کد if (x == null) return 0
در آن بارها تکرار شده.
ما میتوانیم با استثناها این کد را باز هم بهتر کنید:
def getSelectedTextLength(doc: Document): Int = { try { doc.getActivePage.getSelectedText.length } catch { case _: NullPointerException => 0 case e: Exception => throw e } }
این نسخه نیز ایرادات خود را دارد. اگر استثنای NullPointer از یکی از متدهای getActivePage یا getSelectedText رخ دهد، توسط این کد دریافت میشود و باگ احتمالی پنهان میشود.
در اسکالا این مشکل میتواند با استفاده از مونادِ Option حل شود. آپشن میتواند مقدار هر تایپ معینی را در خودش نگه دارد و دو پیادهسازی دارد. None وقتی که مقدار وجود ندارد (null است) و Some برای زمانی که مقدار وجود دارد. همچنین عملیات flatMap را پیادهسازی کرده تا امکان ترکیب و پشت هم قرار دادن عملیاتها وجود داشته باشد.
trait Option[A] { def flatMap[B](f: A => Option[B]): Option[B] } case class None[A]() extends Option[A] { def flatMap[B](f: A => Option[B]): Option[B] = new None } case class Some[A](a: A) extends Option[A] { def flatMap[B](f: A => Option[B]): Option[B] = { f(a) } }
حالا با استفاده از مونادِ Option میتوانیم به صورت زیر کد را پیادهسازی کنیم.
class Document { def getActivePage: Option[Page] = ??? } class Page { def getSelectedText: Option[String] = ??? } def getSelectedTextLength(doc: Option[Document]): Int = { doc .flatMap(_.getActivePage) .flatMap(_.getSelectedText) .map(_.length).getOrElse(0) }
خلاصه
اسکالا بهترین نیست! مانند هر تکنولوژی دیگری محدودیتها و معایبی دارد. یادگیریاش با سختی همراه است چرا که مفاهیمی را که از ریاضیات (به طور خاص نظریه انواع) قرض گرفتهاست، مفاهیم انتزاعیای هستند. اگر کد اسکالا به خوبی نوشته و نگهداری نشود، خواندن و فهمیدن آن دشوار خواهد بود. برخی شرکتهای بزرگ مثل لینکدین و توییتر و یامر، گزارش دادهاند که در حال کاهش وابستگیهای خود به این زبان هستند.
در طرف دیگر، اسکالا انعطافپذیر و مولد است و با یک جعبهابزار قوی به کمک ما میآید. سازگاری با جاوا به برنامهنویسان اسکالا این امکان را میدهد که از کتابخانهها، چارچوبهای نرمافزاری و دیگر ابزارهای جاوا استفاده کنند. همه این خوبیها باعث میشود من زبان اسکالا را برای توسعه ابزارهای بیگدیتا و سرورهایی با لود بالا انتخاب کنم.
منبع: medium (برداشت از این مطلب در مرداد 99 انجام شد.)
.
.
.
با ما همراه باشید
آدرس کانال تلگرام: JavaCupIR@
آدرس اکانت توییتر: JavaCupIR@
آدرس صفحه اینستاگرام: javacup.ir
آدرس گروه لینکدین: Iranian Java Developers