مفاهیم مقدماتی اسکالا (Scala) — به زبان ساده

۷۶۵ بازدید
آخرین به‌روزرسانی: ۲۶ شهریور ۱۴۰۲
زمان مطالعه: ۱۵ دقیقه
مفاهیم مقدماتی اسکالا (Scala) — به زبان ساده

اسکالا یک زبان برنامه‌نویسی است که در سال 2004 از سوی مارتین اودِرسکی (Martin Odersky) منتشر شده است. این زبان از برنامه‌نویسی تابعی پشتیبانی می‌کند و به منظور ارائه زبانی موجز که به صورت بایت‌کد جاوا کامپایل شود طراحی شده است. از این رو برنامه‌های اسکالا را می‌توان روی ماشین مجازی جاوا (JVM) اجرا کرد. در ادامه ویژگی‌های اساسی این زبان را بررسی می‌کنیم.

Hello World

ابتدا نگاهی به روش پیاده ساده برنامه ساده Hello World در اسکالا می‌اندازیم:

package io.teivah.helloworld

object HelloWorld {
  def main(args: Array[String]) {
    println("Hello, World!")
  }
}

ما یک شیء HelloWorld تعریف کرده‌ایم که شامل متد main است. این متد یک آرایه رشته‌ای (string) به عنوان ورودی می‌گیرد. در متد main، یک متد به نام println فراخوانی می‌کنیم که یک شیء به عنوان ورودی گرفته و چیزی را در کنسول نمایش می‌دهد. در عین حال HelloWorld بخشی از بسته io.teivah.helloworld نیز محسوب می‌شود.

مقادیر

نتیجه یک عبارت را با استفاده از کلیدواژه var می‌توان نامگذاری کرد. در مثال زیر هر دو عبارت، روشی معتبر برای تعریف یک مقدار هستند:

val v1: String = "foo"
val v2 = "bar"

ذکر نوع اختیاری است. در این مثال v1 و v2 هر دو از نوع رشته هستند. کامپایلر اسکالا می‌تواند نوع یک مقدار را بدون این به صورت صریح اعلان شده باشد، تشخیص دهد. این وضعیت به نام «استنباط نوع» شناخته می‌شود. همچنین باید اشاره کنیم که یک مقدار را در اسکالا می‌توان «تغییرناپذیر» (immutable) ساخت.

متغیرها

متغیر یک نوع «تغییرپذیر» (mutable) است. متغیر با استفاده از کلیدواژه var اعلان می‌شود.

var counter = 0
counter = counter + 5

همانند مقدار، نوع نیز اختیاری است. از طرفی اسکالا یک زبان با نوع استاتیک است. برای مثال کد زیر نامعتبر است، چون ما تلاش کرده‌ایم یک مقدار int را به مقدار String نگاشت کنیم:

var color = "red"
color = 5 // Invalid

بلوک‌ها

در اسکالا می‌توان عبارت‌ها را با قرار دادن درون {} ترکیب کرد. تابع ()println فوق را در نظر بگیرید که یک شیء به عنوان ورودی می‌گیرد. دو عبارت زیر مشابه هم هستند:

println(7) // Prints 7

println {
  val i = 5
  i + 2
} // Prints 7

توجه کنید که در println دوم، عبارت آخر یعنی i + 2 نتیجه کلی بلوک است. زمانی که مانند مثال println یک تابع را با یک آرگومان منفرد فراخوانی می‌کنیم، می‌توانیم پرانتزها را وارد نکنیم:

println 7

نوع‌های مقدماتی

اسکالا یک زبان کاملاً شیءگرا تلقی می‌شود، چون هر مقدار در آن یک شیء است. از این رو هیچ نوع اولیه (primitive) در آن وجود ندارد. در حالی که در جاوا نوع‌هایی مانند int وجود دارند.

8 نوع اولیه از متغیر در اسکالا وجود دارد:

  • Byte
  • Short
  • Int
  • Long
  • Float
  • Double
  • Char
  • Boolean
سلسله‌مراتب نوع در اسکالا

هر نوع مقدماتی از متغیر از AnyVal به ارث می‌رسد. از سوی دیگر AnyRef نام مستعاری برای java.lang.Object است. در نهایت باید گفت که هر دوی AnyVal و AnyRef از Any به ارث می‌رسند.

درون‌یابی رشته

اسکالا روش جالبی برای درج مستقیم ارجاع /مقدار در رشته‌های مورد پردازش معرفی کرده است. به عنوان مثال:

val name = "Bob"
println(s"Hello $name!") // Hello Bob!

این کار از طریق قرار دادن s درون‌یابی پیش از علامت گیومه ممکن می‌شود. در غیر این صورت کد فوق عبارت !Hello $name را نمایش می‌دهد.

اسکالا چندین درون‌یاب ارائه کرده است؛ اما این سازوکار قابلیت سفارشی‌سازی دارد. برای نمونه می‌توانیم یک درونیاب برای مدیریت عبارت‌های JSON مانند زیر بسازیم:

println(json"{name: $name}")

آرایه و لیست

آرایه‌ها نیز در اسکالا به عنوان یک شیء مدیریت می‌شوند:

val a = new Array[Int](2)
a(0) = 5
a(1) = 2

به دو نکته در این مورد می‌توان اشاره کرد.

  • اولین نکته روش تعیین عناصر است. به جای این که مانند اغلب زبان‌های دیگر از [a[0 استفاده شود، باید از ساختار (a(0 استفاده کنیم. این تغییر ساختاری موجب شده است بتوانیم یک شیء را مانند یک تابع فراخوانی کنیم. در واقع کامپایلر در پس‌زمینه، متد پیش‌فرض ()apply را برای گرفتن ورودی منفرد فراخوانی می‌کند.
  • دومین نکته این است که برخلاف مثال فوق که به وسیله یک val اعلان شده است، شیء آرایه، تغییرپذیر است و از این رو می‌توانیم مقدار اندیس‌های 0 و 1 را تغییر دهیم. val صرفاً الزام می‌کند که ارجاع شیء تغییر نیابد و نه خود شیء.

یک آرایه را می‌توان به روش زیر نیز مقداردهی اولیه کرد:

val a = Array(5, 2)

این عبارت مشابه عبارت فوق است. به علاوه از آنجا که با مقادیر 5 و 2 مقداردهی شده است، کامپایلر s را به عنوان یک Array[int] استنباط می‌کند.

برای مدیریت آرایه‌های چندبعدی از کدی مانند زیر استفاده می‌کنیم:

val m = Array.ofDim[Int](3, 3)
m(0)(0) = 5

این کد یک آرایه دوبعدی ایجاد کرده و عنصر نخست آن‌ها را برابر با مقدار 5 قرار می‌دهد. ساختارهای داده مختلفی وجود دارند که کتابخانه استاندارد اسکالا را تشکیل می‌دهند و یکی از آن‌ها ساختار تغییرناپذیر List است:

val list = List(5, 2)
list(0) = 5 // Compilation error

در مقایسه با Array، تغییر دادن یک اندیس پس از مقداردهی اولیه List منجر به خطای کامپایل می‌شود.

Map

ساختار Map یا نگاشت را می‌توان به صورت زیر مقداردهی کرد:

val colors = Map("red" -> "#FF0000", "azure" -> "#F0FFFF", "peru" -> "#CD853F")

دقت کنید که عملگر <- می‌تواند به منظور ارتباط دادن یک کلید رنگ به مقدار هگزادسیمال متناظرش استفاده شود. Map یک ساختمان داده تغییرناپذیر است. افزودن یک عنصر به آن به معنی ایجاد یک Map دیگر است:

val colors1 = Map("red" -> "#FF0000", "azure" -> "#F0FFFF", "peru" -> "#CD853F")
val colors2 = colors1 + ("blue" -> "#0033FF")

در عین حال، عناصر را نمی‌توان تغییر داد. در مواردی که به یک ساختمان تغییرپذیر نیاز داشته باشیم، می‌توانیم از scala.collection.mutable.Map استفاده کنیم:

val states = scala.collection.mutable.Map("AL" -> "Alabama", "AK" -> "tobedefined")
states("AK") = "Alaska"

در این مثال، باید کلید AK را تغییرپذیر (mutated) سازیم.

مفاهیم مقدماتی متدها/تابع‌ها

در اسکالا باید بین متدها و تابع‌ها تمایز قائل شویم. متد تابعی است که عضو یک کلاس، رفتار (trait) یا شیء باشد. در کد زیر نمونه‌ای از یک متد مقدماتی را مشاهده می‌کنید:

def add(x: Int, y: Int): Int = {
   x + y
}

در اینجا یک متد add را با کلیدواژه def تعریف کرده‌ایم. این متد دو مقدار int به عنوان ورودی می‌گیرد و یک مقدار int بازمی‌گرداند. هر دو ورودی تغییرپذیر هستند. به این معنی که گویا با استفاده از کلیدواژه val تعریف شده‌اند.

کلیدواژه return اختیاری است. این متد به طور خودکار آخرین عبارت را بازمی‌گرداند. به علاوه، لازم به ذکر است که در اسکالا (در مقایسه با جاوا) return در متد جاری قرار دارد و نه بلوک جاری.

آخرین نکته در مورد متد این است که تعیین نوع return اختیاری است. کامپایلر اسکالا می‌تواند آن را نیز استنباط کند. اما به منظور افزایش خوانایی کد بهتر است آن را به صورت صریح اعلام کنیم. به علاوه متدِ بدون خروجی را می‌توان به دو روش نوشت:

def printSomething(s: String) = {
  println(s)
}

def printSomething(s: String): Unit = {
  println(s)
}

چند خروجی را نیز می‌توان به صورت زیر بازگشت داد:

def increment(x: Int, y: Int): (Int, Int) = {
   (x + 1, y + 1)
}

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

def foo(): Unit = {
   bar()
   bar
}

بهترین رویه این است که تنها در صورتی پرانتز را حفظ کنیم که bar تأثیر جانبی داشته باشد. در غیر این صورت می‌توانیم bar را مانند عبارت دوم فراخوانی کنیم. ضمناً اسکالا امکان تعیین تکراری بودن یک آرگومان را نیز فراهم ساخته است. همانند جاوا این آرگومان قابل تکرار باید آخرین پارامتر باشد:

def variablesArguments(args: Int*): Int = {
  var n = 0
  for (arg <- args) {
    n += arg
  }
  n
}

در کد فوق روی عنصر args چرخه‌ای تعریف می‌کنیم و مجموع کلی را بازمی‌گردانیم. آخرین نکته در مورد متد/تابع‌ها این است که می‌توان یک مقدار پارامتر پیش‌فرض نیز تعریف کرد:

def default(x: Int = 1, y: Int): Int = {
   x * y
}

فراخوانی default بدون ارائه مقداری برای x به دو روش مقدور است. هم می‌توانیم از عملگر _ استفاده کنیم:

default(_, 3)

و همچنین می‌توانیم از آرگومان‌های دارای نام، مانند زیر استفاده کنیم:

default(y = 3)

مفاهیم پیشرفته متد/تابع‌ها

در این بخش برخی از مفاهیمی را در مورد تابع‌ها و متدها معرفی می‌کنیم که تا حدودی پیشرفته‌تر محسوب می‌شوند.

متدهای تودرتو

در اسکالا می‌توان تعاریف متد تودرتو داشت. نمونه زیر را در نظر بگیرید:

def mergesort1(array: Array[Int]): Unit = {
  val helper = new Array[Int](array.length)
  mergesort2(array, helper, 0, array.length - 1)
}

private def mergesort2(array: Array[Int], helper: Array[Int], low: Int, high: Int): Unit = {
  if (low < high) {
    val middle = (low + high) / 2
    mergesort2(array, helper, low, middle)
    mergesort2(array, helper, middle + 1, high)
    merge(array, helper, low, middle, high)
  }
}

در این مورد متد mergesort2 صرفاً از سوی mergesort1 استفاده می‌شود. برای محدودسازی این دسترسی می‌توان آن را به صورت private تعریف کرد. با این حال در اسکالا می‌توان تصمیم گرفت که متد دوم را در متداول قرار دارد. روش کار به صورت زیر است:

def mergesort1(array: Array[Int]): Unit = {
  val helper = new Array[Int](array.length)
  mergesort2(array, helper, 0, array.length - 1)

  def mergesort2(array: Array[Int], helper: Array[Int], low: Int, high: Int): Unit = {
    if (low < high) {
      val middle = (low + high) / 2
      mergesort2(array, helper, low, middle)
      mergesort2(array, helper, middle + 1, high)
      merge(array, helper, low, middle, high)
    }
  }
}

mergesort2 تنها در حیطه متد mergesort1 قابل دسترسی است.

تابع‌های درجه بالا

تابع‌های درجه بالا یک تابع را به عنوان پارامتر می‌گیرند و یک تابع را به عنوان نتیجه بازمی‌گردانند. به عنوان نمونه متد زیر را در نظر بگیرید که یک تابع را به عنوان پارامتر می‌گیرد:

def foo(i: Int, f: Int => Int): Int = {

f(i)

}

f تابعی است که یک int به عنوان ورودی می‌گیرد و یک int بازمی‌گرداند. در مثال فوق foo فرایند اجرا را با ارسال i به g واگذار می‌کند.

Function Literals

اسکالا یک زبان تابعی (functional) در نظر گرفته می‌شود، به این معنی که هر تابع، یک مقدار است. معنی گفته فوق این است که می‌توان تابع را در یک ساختار تابعی مانند زیر بیان کرد:

val increment: Int => Int = (x: Int) => x + 1

println(increment(5)) // Prints 6

increment تابعی با نوع Int => Int است که کامپایلر اسکالا می‌تواند آن را نیز استنباط کند. این تابع برای هر عدد صحیح x مقدار x+1 را بازمی‌گرداند. با در نظر گرفتن مثال فوق می‌توان increment را به foo ارسال کرد:

def foo(i: Int, f: Int => Int): Int = {
  f(i)
}

def bar() = {
  val increment: Int => Int = (x: Int) => x + 1

  val n = foo(5, increment)
}

همچنین می‌توان تابع‌های ناشناس (anonymous) را نیز مدیریت کرد:

val n = foo(5, (x: Int) => x + 1)

پارامتر دوم تابعی بدون نام است.

Closure

کلوژر یا بستار تابعی است که به مقدار یک یا چند متغیر/مقدار اعلان شده در خارج از خود وابسته است. مثال ساده آن چنین است:

val Pi = 3.14

val foo = (n: Int) => {
  n * Pi
}

در اینجا foo به Pi وابسته است که خارج از foo اعلان شده است.

تابع‌های جزئی (Partial Functions)

متد زیر برای محاسبه سرعت با استفاده از مسافت و زمان را در نظر بگیرید:

def speed(distance: Float, time: Float): Float = {
  distance / time
}

اسکالا امکان اِعمال جزئی speed را از طریق فراخوانی آن تنها با زیرمجموعه‌ای از ورودی‌های اجباری فراهم ساخته است:

val partialSpeed: Float => Float = speed(5, _)

دقت کنید که در مثال فوق هیچ یک از پارامترهای speed مقدار پیش‌فرض ندارند. بنابراین برای فراخوانی آن باید همه پارامترها را پر کنیم. در این مثال، partialSpeed یک تابع از نوع Float => Float است. در این صورت به روش مشابه increment می‌توانیم partialSpeed را به صورت زیر فراخوانی کنیم:

println(partialSpeed(2.5f)) // Prints 2.0

Currying

منظور از «کاری زدن» این است یک متد می‌تواند چند لیست پارامتر مختلف تعریف کند:

def multiply(n1: Int)(n2: Int): Int = {
   n1 * n2
}

این متد دقیقاً همان کار متد فوق را انجام می‌دهد:

def multiply2(n1: Int, n2: Int): Int = {
   n1 * n2
}

با این وجود روش فراخوانی multiply متفاوت است:

val n = multiply(2)(3)

همان طور که امضای متد الزام کرده است باید دو لیست از پارامترها به آن ارسال کنیم. اکنون ممکن است بپرسید در این صورت اگر multiply را با یک لیست از پارامتر به صورت زیر فراخوانی کنیم چه می‌شود؟

val partial: Int => Int = multiply(2)

در این حالت، ما به طور جزئی از multiply استفاده کرده‌ایم که مقدار بازگشتی به صورت تابع Int => Int در اختیار ما قرار می‌دهد.

مزیت این کار چیست؟ تابع زیر را که یک پیام را در چارچوب خاص ارسال می‌کند در نظر بگیرید:

def send(context: Context, message: Array[Byte]): Unit = {
   // Send message
}

همان طور که می‌بینید تلاش کرده‌ایم تا این تابع مستقل باشد و به جای وابستگی به چارچوب بیرونی، آن را به صورت یک پارامتر تابع send دربیاوریم. با این وجود، الزام به ارسال این چارچوب در طی هر بار فراخوانی send ممکن است ملال‌آور باشد، یا این که ممکن است تابعی در مورد این چارچوب خاص اطلاع نداشته باشد. یک راه‌حل این است که send را به طور جزئی به کار بگیریم یعنی با یک چارچوب از پیش تعریف شده و پیام به تابع Array[Byte] => Unit ارسال کنیم:

def send(message: Array[Byte])(implicit context: Context): Unit = {
    // Send message
}

در این حالت send را چگونه می‌توان ارسال کرد؟ می‌توانیم یک چارچوب implicit پیش از فراخوانی send تعریف کنیم:

implicit val context = new Context(...)
send(bytes)

کلیدواژه implicit به این معنی است که هر تابع یک پارامتر Context صریح را مدیریت می‌کند و حتی لازم نیست آن را ارسال کنیم. این فرایند به صورت خودکار از سوی کامپایلر اسکالا مدیریت می‌شود. در مورد مثال فوق، send شیء Context را به صوت بالقوه صریح (یعنی می‌توان آن را به صورت صریح ارسال کرد) می‌فرستد. بنابراین می‌توان به سادگی send را با لیست آرگومان اول ارسال کرد.

کلاس‌ها

کلاس در اسکالا مفهومی مشابه جاوا دارد:

class Point(var x: Int, var y: Int) {
  def move(dx: Int, dy: Int): Unit = {
    x += dx
    y += dy

    println(s"$x $y")
  }
}

بر اساس ساختار خط اول، Point یک سازنده پیش‌فرض (Int, Int) را نشان می‌دهد. در عین حال x و y دو عضو این کلاس هستند. کلاس می‌تواند شامل مجموعه‌ای از متدها مانند move در مثال فوق باشد. می‌توان با استفاده از کلیدواژه new از Point وهله‌هایی ساخت:

val point = new Point(5, 2)

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

کلاس‌های Case

کلاس‌های Case نوع خاصی از کلاس هستند. اگر با DDD یعنی «طراحی مبتنی بر دامنه» (Domain Driven Design) آشنا باشید، می‌دانید که کلاس Case یک شیء مقدار است. به طور پیش‌فرض کلاس Case تغییرناپذیر است:

case class Point(x: Int, y: Int)

مقدار x و y را نمی‌توان تغییر داد. از کلاس Case می‌توان بدون new نیز وهله سازی کرد:

val point = Point(5, 2)

کلاس‌های Case (در قیاس با کلاس‌های معمولی) بر اساس مقدار مقایسه می‌شوند (و نه ارجاع):

if (point1 == point2) {
  // ...
} else {
  // ...
}

شیءها

شیء در اسکالا به یک سینگلتون (singleton) گفته می‌شود:

object EngineFactory {
  def create(context: Context): Engine = {
    // ...
  }
}

شیءها به وسیله کلیدواژه object تعریف می‌شوند:

Trait-ها

در اسکالا trait مانند مفهوم اینترفیس در جاوا است و از آن برای اشتراک اینترفیس‌ها بین کلاس‌ها و همچنین فیلدها استفاده می‌شود. به عنوان نمونه:

trait Car {
  val color: String
  def drive(): Point
}

یک متد trait می‌تواند پیاده‌سازی پیش‌فرض نیز داشته باشد. Trait را نمی‌توان وهله سازی کرد؛ اما می‌توان به وسیله کلاس‌ها و شیءها آن را بسط داد.

پدیداری (Visibility)

در اسکالا هر عضوِ یک کلاس/شیء/trait به صورت پیش‌فرض عمومی است؛ البته دو mofidier دسترسی دیگر نیز وجود دارند:

  • Protected: اعضا تنها از زیرکلاس‌ها قابلیت دسترسی دارند.
  • private: اعضا تنها از کلاس/شیء جاری قابل دسترسی هستند.

به علاوه، می‌توانیم روش مشخص‌تری برای محدودسازی دسترسی با تعیین پکیجی که محدودیت بر آن اعمال می‌شود، داشته باشیم. فرض کنید کلاس Foo در پکیج bar قرار دارد. اگر بخواهیم یک متد تنها خارج از bar به صورت private باشد می‌توانیم به روش زیر عمل کنیم:

class Foo {
   private[bar] def foo() = {}
}

Generic

Generic نیز جز ویژگی‌هایی است که از سوی اسکالا ارائه شده است:

class Stack[A] {
  def push(x: A): Unit = { 
    // ...
  }
}

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

val stack = new Stack[Int]
stack.push(1)

If-else

ساختار If-else در اسکالا همانند اغلب زبان‌های برنامه‌نویسی دیگر است:

if (condition1) {

} else if (condition2) {

} else {

}

ولی در اسکالا گزاره If-else یک عبارت (expression) نیز محسوب می‌شود. این بدان معنی است که برای نمونه می‌توان متدهایی مانند زیر را تعریف کرد:

def max(x: Int, y: Int) = if (x > y) x else y

حلقه‌ها

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

// Include
for (a <- 0 to 10) {
  println(a)
}

// Exclude
for (a <- 0 until 10) {
  println(a)
}

وقتی از to استفاده می‌کنیم یعنی از 0 تا 10 شامل می‌شوند؛ اما وقتی از until استفاده می‌کنیم به این معنی است که 0 تا 10 شامل نمی‌شوند. همچنین می‌توان روی دو عنصر نیز حلقه‌ای تعریف کرد:

for (a <- 0 until 2; b <- 0 to 2) {

}

در این مثال، روی همه ترکیب‌های چندتایی ممکن حلقه‌ای تعریف می‌کنیم:

a=0, b=0
a=0, b=1
a=0, b=2
a=1, b=0
a=1, b=1
a=1, b=2

همچنین می‌توانیم شرایطی را برای for تعیین کنیم لیست عناصر زیر را در نظر بگیرید:

val list = List(5, 7, 3, 0, 10, 6, 1)

اگر بخواهیم روی همه عناصر list چرخه‌ای تعریف کنیم و تنها اعداد صحیح را در نظر بگیریم، می‌توانیم از کد زیر استفاده کنیم:

for (elem <- list if elem% 2 == 0) {

}

به علاوه اسکالا ساختار for comprehensions را برای ایجاد دنباله‌ای از عناصر به شکل for() yield element ارائه کرده است. برای مثال:

val sub = for (elem <- list if elem% 2 == 0) yield elem

در این مثال، یک مجموعه از اعداد صحیح زوج با تعریف حلقه‌ای روی هر عنصر و خروجی دادن آن در صورت زوج بودن ایجاد می‌شود. در نتیجه sub به عنوان یک دنباله از اعداد صحیح استنباط می‌شود. در روش مشابه استفاده از گزاره if-else، می‌توان گفت که for نیز یک عبارت است. بنابراین می‌توان برای آن متدهایی مانند زیر تعریف کرد:

def even(list: List[Integer]) = for (elem <- list if elem% 2 == 0) yield elem

تطبیق الگو

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

def matchA(i: Int): String = {
  i match {
    case 1 => return "one"
    case 2 => return "two"
    case _ => return "something else"
  }
}

اسکالا برای پیاده‌سازی معادل آن به روش زیر، اندکی تغییرات ساختاری ایجاد کرده است:

def matchB(i: Int): String = i match {

case 1 => "one"

case 2 => "two"

case _ => "something else"

}

ابتدا گزاره‌های return حذف شده‌اند. سپس تابع matchB به یک تطبیق‌دهنده الگو تبدیل شده است، چون گزاره بلوک پس از تعریف تابع حذف شده است. تطبیق الگو امکان خوبی است که به کلاس‌ها اضافه شده است. مثال زیر را که از مستندات اسکالا (+) انتخاب کرده‌ایم در نظر بگیرید: می‌خواهیم بسته به نوع یک اعلان، یک رشته (String) بازگشت دهیم. یک کلاس مجرد Notification و دو کلاس Email و SMS تعریف می‌کنیم:

abstract class Notification
case class Email(sender: String, title: String, body: String) extends Notification
case class SMS(caller: String, message: String) extends Notification

بهترین روش برای انجام این کار در اسکالا، استفاده از تطبیق الگو روی اعلان است:

def showNotification(notification: Notification): String = {
  notification match {
    case Email(email, title, _) =>
      s"You got an email from $email with title: $title"
    case SMS(number, message) =>
      s"You got an SMS from $number! Message: $message"
  }
}

این سازوکار امکان cast کردن notification مفروض و تجزیه خودکار پارامترهای مطلوب را فراهم می‌کند. برای نمونه در مورد یک ایمیل ممکن است نخواهیم متن ایمیل نمایش یابد و از این رو به راحتی با استفاده از کلیدواژه _ آن را حذف می‌کنیم.

موارد استثنا (Exceptions)

مثالی را که می‌خواهیم تعداد بایت‌های یک فایل مفروض را نمایش دهیم تصور کنید. برای اجرای عملیات I/O باید از java.io.FileReader استفاده کنیم که ممکن است موارد استثنایی را اعلام کند.

در صورتی که با جاوا آشنا باشید، می‌دانید که رایج‌ترین روش برای انجام این کار استفاده از گزاره try/catch به صورت زیر است:

try {
  val n = new FileReader("input.txt").read()
  println(s"Success: $n")
} catch {
  case e: Exception =>
    e.printStackTrace
}

روش دوم برای پیاده‌سازی آن تا حدودی مشابه Optional جاوا است. جهت یادآوری باید اشاره کنیم که Optional در جاوا 8 به عنوان کانتینری برای مقادیر اختیاری معرفی شده است. در اسکالا Try کانتینری برای اعلام موفقیت یا شکست است. این یک کلاس مجرد است که با استفاده از دو کلاس Case به صورت Success و Failure بسط یافته است.

val tried: Try[Int] = Try(new FileReader("notes.md")).map(f => f.read())

tried match {

case Success(n) => println(s"Success: $n")

case Failure(e) => e.printStackTrace

}

در ابتدا ایجاد یک FileReader جدید را درون فراخوانی Try پوشش می‌دهیم. از یک map و فراخوانی متد read برای تبدیل FileReader نهایی به یک int استفاده می‌کنیم. در نتیجه، یک Try[Int] به دست می‌آوریم. سپس می‌توانیم از تطبیق الگو برای تعیین نوع tried استفاده کنیم.

محاوره‌های صریح (Implicit Conversions)

مثال زیر را در نظر بگیرید:

case class Foo(x: Int)
case class Bar(y: Int, z: Int)

object Consumer {
  def consume(foo: Foo): Unit = {
    println(foo.x)
  }
}

object Test {
  def test() = {
    val bar = new Bar(5, 2)
    Consumer.consume(bar)
  }
}

دو کلاس Foo و Bar تعریف شده است و همچنین یک شیء Consumer، متد comsume را با گرفتن Foo به عنوان پارامتر ارائه می‌کند. در Test متد ()Consumer.consume را فراخوانی می‌کنیم؛ اما نه با یک Foo که امضای متد الزام می‌کند؛ بلکه با یک Bar. این امر چگونه ممکن است؟ در اسکالا می‌توان محاوره‌های صریحی بین دو کلاس تعریف کرد و در مثال فوق کافی است چگونگی تبدیل Bar به یک Foo را تعریف کنیم:

implicit def barToFoo(bar: Bar): Foo = new Foo(bar.y + bar.z)

در این متد barToFoo ایمپورت می‌شود و کامپایلر اسکالا مطمئن می‌شود که می‌توانیم consumer را با یک Foo یا Bar فراخوانی کنیم.

همزمانی

اسکالا برای مدیریت همزمانی (concurrency) در ابتدا از مدل actor استفاده می‌کرد. اسکالا کتابخانه scala.actors را به این منظور ارائه کرده بود. با این وجود، از نسخه 2.10 اسکالا به بعد این کتابخانه منسوخ شده و از Akka actors (+) استفاده می‌شود.

Akka مجموعه‌ای از کتابخانه‌ها برای پیاده‌سازی اپلیکیشن‌های همزمان و توزیع یافته است. با این حال در مقیاس یک پردازش منفرد نیز می‌توان از Akka استفاده کرد.

ایده اصلی به این صورت است که actor-ها به عنوان یک permitive برای مصرف همزمان، مدیریت می‌شوند. هر actor می‌توان پیام‌هایی به actor-های دیگر بفرستد، پیام‌ها را دریافت کند، به آن‌ها واکنش نشان دهد و actor-های جدیدی را تولید کند.

نمونه‌ای از ارتباط درون یک سیستم actor

در این روش نیز مانند اغلب مدل‌های مصرف همزمان دیگر مانند CSP یا «ارتباط بین پردازش‌های ترتیبی» (Communicating Sequential Processes) نکته مهم در ارتباط از طریق پیام به جای اشتراک حافظه بین نخ‌های مختلف نهفته است.

سخن پایانی

اسکالا زبانی بسیار ظریف است. با این وجود، یادگیری آن به اندازه زبان‌های دیگر مانند Go آسان نیست. خواندن یک کد اسکالا به عنوان یک مبتدی می‌تواند تا حدودی دشوار باشد؛ اما زمانی که در آن مهارت یافتید، توسعه اپلیکیشن می‌تواند به روشی کاملاً کارآمد صورت بگیرد.

اگر این مطلب برایتان مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

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

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *