سكالا …الدرس الثالث
الدرس الثالث:
Built-in Control Structures
المقصود بهذا العنوان هو الجمل التي تتحكم من خلالها فى مسار البرنامج مثل if و while و for و لقد تكلمنا عنهم سابقا بشكل سريع و الان مع التفاصيل.قلنا بالسابق ان تقريبا فى سكالا كل شئ هو تعبير Expression و بالتالى فتلك الجمل هى فى الحقيقة Expressions اي انها ترجع قيمة و ذلك هو الفرق الجوهرى بين تلك الجمل و نظيرتها فى اللغات الاخري مثل الجافا.
هنلاحظ اننا قلنا تقريبا لان هناك استثناء من تلك القاعده و هو while loop فهى لا تعامل كتعبير.
If expressions
فى الغالب ﻻ يوجد If ﻻ تعود بناتجمثال على if
scala> val x =
if
(3>4) 11
else
12
x: Int = 12
الحلقة التكرارية For expressions
مثال يقوم باستخرج اسماء الملفات فى المجلد الحالى (.) Current directory و هنلاحظ اننا بنستخدم كلاس File من الجافا عادي جدا و هنلاحظ كمان ان الناتج هو صف Array عناصرها من النوع Filescala> val filesHere = (new java.io.File(
"."
)).listFiles
filesHere: Array1 = Array(.
/scalSession
#1,./scalSession#2,./scalSession#3,./scala-2.9.0.1,./scala-2.8.1.final,./hello.scala,./ChecksumAccumulator.scala,./Summer.scala,./rational.scala)
scala>
for
(
file
<- filesHere)
| println(
file
)
.
/scalSession
#1
.
/scalSession
#2
.
/scalSession
#3
.
/scala-2
.9.0.1
.
/scala-2
.8.1.final
.
/hello
.scala
.
/ChecksumAccumulator
.scala
.
/Summer
.scala
.
/rational
.scala
حتى الان نعرف كل هذا و قمنا به من قبل المثير هو الجزء القادم و هو
- التصفية (الترشيح) Filtering
scala>
for
(
file
<- filesHere
if
file
.getName.endsWith(
".scala"
))
| println(
file
)
.
/hello
.scala
.
/ChecksumAccumulator
.scala
.
/Summer
.scala
.
/rational
.scala
قمنا هنا بعمل for loop علي صف array الملفات و لكننا وضعنا شرط و هو ان ينتهى اسم الملف بـ .scala
هل تتذكر فى اللغات اخرى ماذا كان يحدث على سبيل المثال جافا
كنا نقوم بعمل for loop بداخلها عدة جمل من ال if لكى نقوم بعمل Filtering لنحصل ما نريده بالتحديد
فى سكالا الوضع مختلف هنا يمكن وضع كل الشروط التى تريد توافرها فى اى عنصر بداخل الجمله ال for loop نفسها
فى المثال السابق
يمكننا عمل هذا الشرط عن طريق اضافة if داخل اقواس for
يمكن فى الحقيقة اضافة العديد من الشروط عن طريق اضافة المزيد من if
scala>
for
(
|
file
<- filesHere
|
if
file
.isFile;
|
if
file
.getName.endsWith(
".scala"
)
| )println(
file
)
.
/hello
.scala
.
/ChecksumAccumulator
.scala
.
/Summer
.scala
.
/rational
.scala
مثال اخر
هذه المرة سنقوم بعمل مثال يقوم باستثناء المجلدات و اختيار الملفات فقط التى ينتهى اسمها ب .scala و تحتوى على pattern معين
و طبعا يمكننا ايضا ان نقوم ب nested iteration اي loop داخلها loop اخري
scala> def fileLines(
file
: java.io.File) =
| scala.io.Source.fromFile(
file
).getLines.toList
fileLines: (
file
: java.io.File)List[String]
scala> def
grep
(pattern: String) =
|
for
(
|
file
<- filesHere
|
if
file
.getName.endsWith(
".scala"
);
| line <- fileLines(
file
)
|
if
line.trim.matches(pattern)
| ) println(
file
+
": "
+line.trim)
grep
: (pattern: String)Unit
scala>
grep
(
".*gcd.*"
)
.
/rational
.scala: private val g = gcd(n.abs, d.abs)
.
/rational
.scala: private def gcd(a: Int, b: Int): Int =
.
/rational
.scala:
if
(b == 0) a
else
gcd(b, a % b)
قلنا من قبل ان الفلترة ليست جديدة و اننا اعتدنا عليها قبل ذلك و لكن ربما بشكل مختلف قليلا و يمكن تطبيق الشكل القديم فى سكالا ايضا
scala>
for
(
file
<- filesHere)
|
if
(
file
.getName.endsWith(
".scala"
))
| println(
file
)
.
/hello
.scala
.
/ChecksumAccumulator
.scala
.
/Summer
.scala
.
/rational
.scala
هذا هو نفس المثال الاول و يعطي نفس النتيجة و ربما تكون تلك الطريقة مألوفة لنا و استخدمناها سابقا مع لغات اخري كما ذكرنا
فلماذا اذا طريقة سكالا فى وضع الشروط داخل الاقواس و ليس فى جسم الحلقة التكرارية ؟؟
السبب هو جعلها كما قلنا سابقا expression ترجع قيمة
فى طريقه سكالا تجد ان اذا لم يتوافر الشروط فى بدايه FOR LOOP لن يدخل الى جسم الحلقة التكرارية و هذا افضل و اكثر منطقية
- Yield
scala> val scalaFiles =
for
(
|
file
<- filesHere
|
if
file
.getName.endsWith(
".scala"
)
| ) yield
file
scalaFiles: Array1 = Array(.
/hello
.scala, .
/ChecksumAccumulator
.scala, .
/Summer
.scala, .
/rational
.scala)
قمنا هنا باستقبال قيمة التعبير فى متغير scalaFiles و بعد تعيينه نلاحظ انه صف array عناصره من نوع java.io.File وقد تم هذا عن طريق استخدام كلمة yield وهي يجب ان تكون اول كلمة بعد الاقواس.
Exception handling with try expressions التقاط الاستثناءات
فى هذه الجزئيه ستجد اختلاف كبير عن اللغات الاخرىلنرى هذا المثال
import
java.io.FileReader
import
java.io.FileNotFoundException
import
java.io.IOException
try {
val f = new FileReader(
"input.txt"
)
//
Use and close
file
} catch {
case
ex: FileNotFoundException =>
//
Handle missing
file
case
ex: IOException =>
//
Handle other I
/O
error
}
- هنلاحظ فى syntax اننا نستخدم بلوك catch مرة واحدة ونضيف به استثناءات متعددة على عكس الجافا حيث نستخدم بلوك catch لكل اسثناء.
- فى سكالا ليس اجباري التقاط checked exceptions على عكس الجافا.
- في سكالا try catch block هو تعبير اي يرجع قيمة
scala>
import
java.net.URL
import
java.net.URL
scala>
import
java.net.MalformedURLException
import
java.net.MalformedURLException
scala> def urlFor(path: String) =
| try {
| new URL(path)
| } catch{
|
case
e: MalformedURLException =>
| new URL(
"http://www.google.com"
)
| }
urlFor: (path: String)java.net.UR
هنلاحظ فى هذه الحالة فان الدالة ترجع قيمة try اذا لم يلقى استثناء Exception is thrown و ترجع قيمة Catch اذا تم القاء استثناء و تم التقاطه.
Match expressions
يمكن اعتبار match بديل لـ switch فى اللغات الاخري مثل جافا و لكن مع بعض الاختلافات و المزايا الاخري.
مثال
مثال
object ScalaApp{
def main(args: Array[String]) {
val firstArg =
if
(args.length > 0) args(0)
else
""
val friend =
firstArg match {
case
"salt"
=>
"pepper"
case
"chips"
=>
"salsa"
case
_ =>
"huh?"
}
println(friend)
}
}
يمثل هذا الكود برنامج يصلح لل compilation يأخذ اول argument من سطر الاوامر و يطبع كلمة تختلف باختلاف argument
ملحوظه:- علامة _ تمثل فى سكالا معنى ائ شئ مجهول
~$ scalac ScalaApp.scala
~$ scala ScalaApp salt
pepper
نلاحظ الاختلاف بين match و switch
- تستخدم مع اي نوع من المتغيرات ليس فقط integers و الاعداد الرقمية كـ switch
- لا يوجد بها كلمة break.
- اهم اختلاف كالعادة انها ترجع قيمة كما فى المثال يمكن استقبال القيمة المرجعة فى متغير friend .
Living without break and continue
هذا ما كان يحدث فى جافا على سبيل المثالint i = 0;
//
This is Java
boolean foundIt =
false
;
while
(i < args.length) {
if
(args[i].startsWith(
"-"
)) {
i = i + 1;
continue
;
}
if
(args[i].endsWith(
".scala"
)) {
foundIt =
true
;
break
;
}
i = i + 1;
}
اذا حاولنا تحويل هذا المثال الى سكالا فلن نحتاج ل break , continue
الغرض هنا من المثال ان نثبت لك ان يمكن كتابه اى كود بدون break ,continue
يمكن ان يتم الاستغناء عن continue,break ب if و قيمه boolean
var i = 0
var foundIt =
false
while
(i < args.length && !foundIt) {
if
(!args(i).startsWith(
"-"
)) {
if
(args(i).endsWith(
".scala"
))
foundIt =
true
}
i = i + 1
}
مدي المتغيرات Variables Scope
المقصود بهذا المصطلح هو المدي الذي يظهر فيه المتغير و يكون قابل للاستخدام خلاله Accessible و هو مشابه تماما للجافا عدا فى اختلاف واحد و هو انه يمكنك ان تسمي متغير بنفس الاسم فى نطاقات متداخلة و بالتالي سمكنك فى المدي الداخلى استخدام المتغير الداخلىمثال
scala> val a=1
a: Int = 1
scala> {
| val a=2
| println (a)
| }
2
scala> println(a)
1
فى هذه الحالة سيطبع 2 و المرة الثانية 1 و يقال فى هذه الحالة على
المتغير الداخلى انه أظل على المتغير الخارجي shadow فلم يعد يمكننا رؤية
المتغير الخارجي داخل النطاق الداخلى لانه يوجد متغير بنفس الاسم فى
الداخل. هذا لم يكن مسموح به فى الجافا.Local function
الدوال Functionsتكلمنا سابقا عن الدوال و الان نكمل بشئ من التفصيل بعض خواص سكالا الفريدة فى التعامل مع الدوال
- الدوال المحلية Local functions
مثال يوضح الفرق بين الطريقتين
الطريقة الاولى عن طريق التصريح بالدالة المساعده خارجا
الدالة الرئيسية هي processFile و التى تاخذ اسم ملف و عرض معين كـ argument ثم تمر عبر سطور الملف و تمرر كل سطر كـ argument للدالة المساعدة processLine لتقوم باختبار طول السطر بالمقارنة بالعرض المعطى فى البداية و فى النهاية تقوم بطباعة السطور التى يتخطى عرضها هذا الرقم و طباعة ايضا اسماء ملفاتهم
import
scala.io.Source
object LongLines {
def processFile(filename: String, width: Int) {
val
source
= Source.fromFile(filename)
for
(line <-
source
.getLines)
processLine(filename, width, line)
}
private def processLine(filename: String,width: Int, line: String) {
if
(line.length > width)
println(filename +
": "
+ line.trim)
}
}
الان مع الطريقة الثانية باستخدام الدالة المحليةdef processFile(filename: String, width: Int) {
def processLine(filename: String,width: Int, line: String) {
if
(line.length > width)
print(filename +
": "
+ line)
}
val
source
= Source.fromFile(filename)
for
(line <-
source
.getLines) {
processLine(filename, width, line)
}
}
قمنا بتعريف الدالة processLine داخل الدالة processFile و سنلاحظ الان
ميزة اضافية و هى ان الدالة المحلية يمكن لها ان تستخدم معطيات الدالة الاب
مباشره فبدلا من ان نمرر للدالة المحلية اسم الملف و العرض و هما فى
الاساس تم تمريرهم مسبقا للدالة الاب يمكن الاكتفاء بتمرير السطر فقط لتصبح
كالاتي
import
scala.io.Source
object LongLines {
def processFile(filename: String, width: Int) {
def processLine(line: String) {
if
(line.length > width)
print(filename +
": "
+ line)
}
val
source
= Source.fromFile(filename)
for
(line <-
source
.getLines)
processLine(line)
}
}
Function literals الدوال الحرفية (البسيطة)
تعرفنا سابقا على الدوال الحرفية التى يمكن تمييزها فقط من شكلها و يمكن ان تستخدم كـ argument لدوال اخري First class functionsمثال
scala> (x:Int) => (x*x)
res16: (Int) => Int = <
function
>
يمكن تعيينها لمتغير حتي يمكن استخدامها
scala> val sqr =(x:Int) => (x*x)
sqr: (Int) => Int = <
function
>
scala> sqr(3)
res18: Int = 9
و يمكن ادخالها كـ argumment لدالة اخري
scala> val myList = List(1,2,3,4,5,6)
myList: List[Int] = List(1, 2, 3, 4, 5, 6)
scala> myList.filter( (x:Int)=> x>3 )
res20: List[Int] = List(4, 5, 6)
Short forms of function literals
الان سنقوم بتبسيط المثال الاخير قليلا- يمكننا ان نحذف نوع parameter لان الـ compiler يعلم ان عناصر myList هم Integers
- بعدها يمكننا حذف الاقواس حول x لتصبح هكذا
scala> myList.filter( x => x>3 )
res21: List[Int] = List(4, 5, 6)
فى النهاية يمكننا الوصول لهذا الشكل
scala> myList.filter( _>3 )
res22: List[Int] = List(4, 5, 6)
تلك الـ _ تمثل حاجز مكان placeholder للـ parameter ينتكلم عنه الان
Placeholder syntax
لجعل الموضوع اكثر اختصار بمكنك استخدام _ مكان parameter واحد او اكترscala> myList.filter( _>3 )
res22: List[Int] = List(4, 5, 6)
و لكن بشرط ان يتخدم هذا الـ parameter ليس اكثر من مرة واحده فقط فى جسم الدالة
scala> val f = _ + _
<console>:4: error: missing parameter
type
for
expanded
function
((x$1, x$2) => x$1.$plus(x$2))
val f = _ + _
^
ﻻحظ ان + هى فى الاصل دالة تاخذ قيمتين
و لكن يكتب مثل هذا المثال هكذا
scala> val f = (_: Int) + (_: Int)
f: (Int, Int) => Int = <
function
>
scala> f(5, 10)
res11: Int = 15
الدوال غير مكتملة التطبيق Partially applied functions
تلك الدوال هي التعبير الذي ينتج من عدم تمرير كل الـ arguments التي تطلبها الدالة و يمكن عمل ذلك باستخدام placeholderscala> def
sum
(a: Int, b: Int, c:Int) = a + b + c
sum
: (a: Int, b: Int, c: Int)Int
scala>
sum
(1,2,3)
res0: Int = 6
تمثل الـ underscore هنا placeholder لكل الـ argument
scala> val a =
sum
_
a: (Int, Int, Int) => Int = <
function
>
scala> a(1,2,3)
res1: Int = 6
تمثل الـ underscore هنا placeholder لـلـ argument الثاني فقط فتخرج لنا دالة ينقصها argument واحد
scala> val b =
sum
(1, _: Int, 3)
b: (Int) => Int = <
function
>
scala> b(2)
res2: Int = 6
scala> b(5)
res3: Int = 9
الاغلاق Closures
تطلق تلك التسمية على الدالة التي الدوال التى تستجدم متغيرات حرة من خارجها حيث انها عند التصريح بها تقوم الاحتفاظ بـمرجع reference لذلك المتغير (تقوم بالغلاق عليه داخلها)مثال
scala> var
more
= 1
more
: Int = 1
scala> val addMore = (x: Int) => x +
more
addMore: (Int) => Int = <
function
>
scala> addMore(10)
res4: Int = 11
عند طلب الدالة قامت باضافة 10 لـقيمة المتغير more الذي تحتفظ بمرجع له و الدليل على انها تحتفظ بمرجع و ليست تحتفظ بقيمته الفعلية هو المثال القادم حيث نغير قيمة more و نلاحظ تغير نتيجة الدالة
scala>
more
= 2
more
: Int = 2
scala> addMore(10)
res6: Int = 12
ملحوظة هامة :-
فى سكالا يمكنك اثناء التصريح بالدالة اعطاءها امكانية استقبال عدد غير معروف من arguments من نفس النوع عن طريق وضع الرمز * بجانب النوع
مثال
scala> def
echo
(args: String*) =
for
(arg <- args) println(arg)
def
echo
(args: String*) =
for
(arg <- args) println(arg)
scala>
echo
(
"one"
,
"two"
)
one
two
الاستدعاء الذاتي Recursion
الاستدعاء الذاتي هو عندما تنادي الدالة على نفسها و هو يعتبر فى Functional style بديل للحلقات التكرارية loops حيث يمكننا من تجنب المتغيرات vars و لكن كانت هناك دوما مشكلة فى الاداء performance فعلى عكس الحلقات التكرارية التى تعتمد فى عملها فى الذاكرة على قفزات jumps من نهاية الحلقة لبدايتها فى حالة recursion تقوم الدالة بمناداة نفسها اكثر من مرة و ذلك يعتبر عبء على stack فى الذاكرة.من هنا جاء مصطلح اخر و هو
- Tail Recursion
كيف نكتب tail recursion ؟؟
الشرط الرئيسي هو أن يكون نداء الاستدعاء الذاتي هو اخر شئ فى جسم الدالة
مثال : نقوم بتعريف دالة تنادي نفسها عده مرات ثم تلقي استثناء exception.
فى تلك الحالة ليست tail recursion لانها تقوم باضافة 1 بعد الاستدعاء و بالتالي نلاحظ فى output انها استخدمت اكثر من اطار فى stack
scala> def boom(x: Int): Int =
|
if
(x==0) throw new Exception(
"boom!"
)
else
| boom(x-1) + 1
boom: (x: Int)Int
scala> boom(3)
java.lang.Exception: boom!
at .boom(<console>:8)
at .boom(<console>:9)
at .boom(<console>:9)
at .boom(<console>:9)
at .<init>(<console>:9)
at .<clinit>(<console>)
at .<init>(<console>:11)
at .<clinit>(<console>)
at $
export
(<console>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl...
- اما فى الحالة التالية فاصبح الاستدعاء هو اخر ما تنفذه الدالة و بالتاى يمكن تطبيق tail recursion و سنلاحظ فى output انه استخدم اطار stack واحد.
scala> def boom(x: Int): Int =
|
if
(x==0) throw new Exception(
"boom!"
)
else
| boom(x-1)
boom: (x: Int)Int
scala> boom(3)
java.lang.Exception: boom!
at .boom(<console>:8)
at .<init>(<console>:9)
at .<clinit>(<console>)
at .<init>(<console>:11)
at .<clinit>(<console>)
at $
export
(<console>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl....
فى النهاية ارجو ان اكون قد قدمت معلومة جديدة و بشكل جديد
ليست هناك تعليقات:
إرسال تعليق