تعرّف على F# عمليا

خالد الشقروني   05 05 2016

هذه المقالة محاولة للتقرب من مفاهيم البرمجة الدالّية عموما والبرمجة بلغة F# خصوصا. مدخلنا في ذلك إنشاء برنامج ويندوز مكتبي بسيط بإستخدام عناصر واجهة الاستخدام Windows Forms في بيئة .Net ومن خلاله سنرى ماذا تعني مفاهيم ومصطلحات مثل Immutability و Pattern matching و Functional programming و Recursive function و Type inference و Lambda وغيرها من المفاهيم.

سنتعرّف أيضا عن كيفية دعم F# للمذاهب paradigm البرمجية غير الدالّية مثل المنحى للكائن Object Oriented والأسلوب الحتمي / الإجرائي imperative/ procedural وكيفية استخدامها للتعامل مع مكتبات بيئة .Net

في هذه المقالة سأفترض بأن لديك إلمام عاما بالبرمجة، وأنك ستستخدم Visual Studio .

إنشاء تطبيق مكتبي لويندوز باستخدام مكتبة Windows Forms

تحتوي نافذة برنامجنا على مربعي كتابة، يقوم المستخدم بكتابة نصّ في إحداها، ويتولّى البرنامج تحويل هذا النص إلى معكوسه في المربع الثاني. كما يحتوي على زرّ الذي سينادي على الدالّة المسؤولة على تحويل النص وعرضه. أيضا تحوي مكون ملصق label لعرض قيمة المفتاح التي يتم الضغط عليه في لوحة المفاتيح. هذا بالإضافة إلى لائحة أوامر.

الشكل النهائي للبرنامج سيكون كالتالي:

الخطوات كالتالي:

يقوم فيجوال ستوديو بإنشاء ملفات المشروع .

نقوم بتغيير خصائص المشروع من خلال الأمر Project > WinFormProj Properties.. حيث ستظهر نافذة خصائص المشروع:

في خانة Output type نختار Windows Application

ثم نقوم بحفظ التغيير. (الأمر Save Selected Item) وغلق نافذة خصائص المشروع.

نضيف مرجعيات References للتجميعات assemblies التي تتيح لنا بناء تطبيق ويندوز، وذلك بأن نستدعي شاشة المرجعيات من خلال لائحة الأوامر Project > Add References..

المرجعيات تندرج تحت مجموعة من التقسيمات، نأتي للتقسيم Framework، سنجد قائمة طويلة من التجميعات، منها ما تم اختياره مسبقا. ما يهمنا إضافته الآن:

ثم نضغط موافق.

لنتأكد من سلامة خطواتنا السابقة وأننا قمنا بما يلزم؛ نقوم بإنشاء نموذج Form بسيط.

نأتي للملف Program.fs ونعيد صياغة محتوياته لتكون كالتالي:

(الكود)

open System
open System.Windows.Forms
 
module Program =
 
  type MainForm() =
    inherit Form()

  let myForm = new MainForm()
 
  Application.EnableVisualStyles() 
  [<STAThread>]
  [<EntryPoint>]
  Application.Run myForm
  //Application.Run(new MainForm())

في الكود السابق قمنا بالتغييرات الأساسية التالية:

ملاحظة: يمكن دمج التعبيرين 3 و 4 في تعبير واحد مركّب كما هو واضح في السطر الأخير المجمد.

نقوم الآن بتشغيل البرنامج (F5) للتأكد من سلامة خطواتنا السابقة.

عند تنفيذ أمر التشغيل، قد تظهر نافذة حوار تفيد بأنه قد طرأ تغيير على المشروع وتسأل عمّ إذا كنا نرغب في إعادة بنائه قبل تشغيله، نجيب بنعم.

عند اشتغال البرنامج يفترض أن تظهر نافذة النموذج الذي أنشأناها. النافذة تأتي بمواصفاتها الافتراضية فهي اعتيادية قابلة للتحريك والتحجيم، وهي الآن خالية من أية عناصر.

إنشاء ملف خاص بالنموذج

حتى الآن قمنا بالخطوات الأساسية لإنشاء مشروع برنامج ويندوز.

الآن نحتاج إلى إضافة بعض العناصر أو المتحكمات لنافذة النموذج حتى يكون لبرنامجنا معنى، ولكن قبل ذلك لو لاحظت؛ فإن التعليمات الخاصة بإنشاء النموذج MainForm موجودة داخل الملف Program.fs ، هنا يستحسن أن نفصل التعليمات الخاصة بإنشاء النموذج ونضعها في ملف منفصل خاص به. بهذا نضمن أن ملف Program.fs يحوي فقط التعليمات الخاصة بتشغيل البرنامج، بينما تكون التعليمات المتعلقة بإنشاء النموذج وعناصره في ملف آخر.

لإضافة ملف جديد نأتي لعنصر المشروع ومن لائحة الأوامر المنبثقة منه نختار الأمر:

Add > New Item..

تظهر نافذة إضافة عنصر ، ومن عناصر Visual F# Items نختار Source File، ونحدد اسم الملف ب MainForm.fs ونضغط موافق.

الملف الجديد سينظم إلى قائمة ملفات المشروع وسيكون في آخر القائمة.

ما نريد فعله الآن هو وضع التعريفات الخاصة بإنشاء النافذة ومكوناتها في الملف الخاص MainForm.fs الذي أنشأناه قبل قليل، وأن يتم إستدعاء هذه النافذه من ملف Program.fs والذي هو واضح أنه الملف الأساسي لإنطلاق البرنامج .

لكن قبل ذلك؛ يجب أن نلاحظ أن ملف Program.fs (الذي يحوي الدالّة التي تم تزيينها بالخاصية EntryPoint) يجب أن يكون آخر ملف في ترتيب ملفات مشروعنا، حتى يتمكن من رؤية تعريف النموذج في ملف MainForm.fs . (سوف نشرح ذلك بعد قليل) لذلك وجب نقل موضع الملف MainForm.fs وذلك باستدعاء لائحة الأومر المنبثقة منه وتنفيذ الأمر Move Up ووضعه قبل ملف Program.fs .

وبذلك يكون ترتيب الملفات كما في الشكل التالي:

ننقل تعريف النموذج إلى ملف MainForm.fs ، وقبلها نضيف بعض المرجعيات التي سيحتاجها تموذج النافذة، بحيث تكون محتويات الملفّ كالتالي:

module MainForm 

open System
open System.Drawing
open System.Windows.Forms
 
  type MainForm() =
    inherit Form()

نقوم بتعديل محتويات ملف Program.fs كالتالي:

open System
open System.Windows.Forms
open MainForm
 
module Program =
 
  let myForm = new MainForm()
 
  [<EntryPoint>]
  Application.Run myForm
  //Application.Run(new MainForm())

هنا أضفنا إشارة إلى التركيبة module المسمى MainForm والموجود في ملف MainForm.fs والتي بها تعريف النموذج MainForm ، وأبقينا فقط على تعليمات إنشاء النموذج وتشغيله.

ترتيب الملفات في F#

يتم فحص وتقييم evaluate الأنواع والتعابير البرمجية في لغة F# من فوق لتحت Top-down evaluation أي أن أي تعبير أو تعليمة برمجية داخل ملف البرنامج لا يمكن لها أن تشير لأي تعبير آخر ما لم يكن معرفا قبله. فمثلا لا يمكنك استدعاء دالّة function تم تعريفها بعد السطر الذي يتم فيه استدعاء هذه الدالّة، وذلك لأن مترجم F# يقوم بتقييم كل تعليمة سطرا بسطر، فإذا وجد إشارة لمعرّف أو دالّة لم يسبق له أن مرّ عليها فإنه سيعتبر ذلك خطأ في البرنامج. وعلى نفس المنوال فإن مترجم F# يقوم بفحص وتقييم الملفات حسب الترتيب الذي تحدّده، فإذا وجد تعليمة تشير إلى أي تعريف لم يمّر عليه في نفس الملف أو في ملفات سابقة فإنه يعتبر ذلك خطأ في التعليمة.

لذلك يجب مراعاة مبدأ ترتيب الإعتمادية dependency order عند ترتيب الملفات وما تحويه في F#.

 

إضافة متحكمات Controls إلى النموذج

نأتي الآن إلى نافذة MainForm ونبدأ في وضع عناصر واجهة الاستخدام، سوف نقوم بإنشاء بعض المتحكمات controls ونحدد خصائصها، وأنواع الأحداث التي نريد لها أن تستقبلها، والاجراءات التي تفوم بها وفق هذه الأحداث.

سنقوم بإضافة التعليمات التالية:

  type MainForm() as x =
    inherit Form()

لاحظ كيف أضفنا المعرّف x لنتمكن لاحقا من استخدامه للإشارة الصنفية MainForm داخلها. في F# يمكننا أن نختار أي رمز ليكون إسما لمعرّف ذاتي للصنفية. يقابل هذا؛ المعرّف this في لغات مثل C++ و C# و Java، والمعرّف Me في Visual Basic ، و self في Delphi .

    let lbl = new Label()
    let txt1 = new TextBox()
    let txt2 = new TextBox()
    let btReverse = new Button()

كما نلاحظ، فقد أنشأنا أربع متحكمات جديدة من أنواع مختلفة تضم متحكم Label (ملصق نصي)، إثنان متحكم TextBox صندوق نصي، ومتحكم زرّ Button. وربطنا كل واحد منهم بمعرّف identifier خاص به. فمثلا التعليمة:

    let lbl = new Label()

أنشأنا بها متحكم من نوع Label وخصصناه أو ربطناه binding بالمعرّف/ الرمز lbl

التعليمة new تعني إنشاء عنصر طبيعته iDisposale أي أنه كائن سيتم إفناؤه آليا عند انتهاء الحاجة إليه، عملية الإفناء تقوم بها نيابة عنا بيئة .Net بواسطة نظام جمع النفايات فيها garbege collection .

لاحظ أيضا أن التعليمات السابقة تمت داخل الصنفية MainForm

نقوم الآن بتحديد خصائص كل متحكم:

    do
      lbl.Location  <- new Point(10, 30) 
      lbl.Size <- Size(640, 30)
      lbl.AutoSize <- true
      lbl.Text <- "Hi"
      lbl.Font <- new Font("Tahoma", 16.0f);
       
      txt1.Location <- new Point(10, 70) 
      txt1.Size <- Size(240, 140)
      txt1.Multiline <- true
      txt1.AcceptsTab <- true
      txt1.ScrollBars <- ScrollBars.Vertical
      
      txt2.Location <- new Point(260, 70) 
      txt2.Size <- Size(240, 140)
      txt2.Multiline <- true
      txt2.ScrollBars <- ScrollBars.Vertical 
      
      btReverse.Location  <- new Point(10, 220)
      btReverse.TabIndex <- 2
      btReverse.Text <- "Reverse" 

كما نحدد بعض الخصائص لنافذة النموذج نفسه، ثم نطلب من النموذح إضافة المتحكمات التي أنشأناها ضمن قائمة المتحكمات لديه.

      x.ClientSize <- new Size(520, 262);
      x.Text <- "Text Reverse"
      x.Controls.Add(lbl)
      x.Controls.Add(txt1)
      x.Controls.Add(txt2)
      x.Controls.Add(btReverse)
 

لاحظ كيف يتم تغيير قيمة المتغيرات والخصائص باستخدام المعامل <- بدلا من المعامل =.

= مقابل <-

في F# المعامل = تعني تعريف أو تحديد definition ولا تعني التخصيص assignment للقيم، كما قد تعني المساواة equality فإذا قلنا let a = 5 فإن هذا يعني أن ننشئ رمزا إسمه a ونربط قيمته بـ 5 ، وقيمته لاتتبدل لأن الرمز a هنا ليس متحوّل immutable. فإذا أتبعناه بتعبير a = 6 فإن F# يتعامل معه على أنه تعبير منطقي boolean وناتجه في هذه الحالة false لأن a لا تساوي 6.

بالمقابل فإن المعامل <- تعني تخصيص قيمة لمتغير متحوّل mutable مثل:

    let mutable a = 5
    a <- 6

لأن المتغير a تم تعريفة على أنه mutable أي متحول؛ يتم استخدام المعامل <- لتغيير قيمته. ولأن خصائص المكونات مثل خاصية Text للمكون lbl هي متغيرات متحولة؛ نستخدم المعامل <- لتغيير قيمها.

الصورة التالية تبين شكل كود البرنامج الخاص بالنموذج.

المسافات والهوامش

في الإصدارات الأخيرة للغة F# اعتمدت التركيبات اللغوية المخففة lightweight syntax كنمط افتراضي لكتابة الكود، بحيث يتم التعبيرعن بدايات ونهايات كتل التركيبات البنيوية للكود بالمسافات والهوامش بدلا من وسوم مثل begin و end و done وغيرها. لذلك فإن هوامش الكود تؤثر في معناه.

إذا قمنا بتشغيل البرنامج سوف نحصل على نافذة مثل الشكل التالي:

تعيين الأحداث على المتحكمات

نقوم بتعيين الأحداث التي نريد للمتحكمات أن تستقبلها، ومع كل حدث نقوم بتعيين دالّة function تقوم باستقبال الحدث وتنفيذ أية متطلبات تخص هذا الحدث.

نبدأ بالمتحكم txt1 وهو مربع كتابة TextBox ، الحدث الذي يهمنها معالجته هنا هو حدث KeyPress ، هذا الحدث يطرأ كلما ضغط المستخدم على مفتاح في لوحة المفاتيح.

طريقة إضافة هذا الحدث والاشتراك فيه كالتالي:

txt1.KeyPress.Add( دالّة )

كما نلاحظ في التعليمة أعلاه؛ يتم تحديد الدالّة بين القوسين. إما أن نكتب بين القوسين إسم الدالّة التي تستقبل الحدث وتتناوله، أو أن نكتب كامل الدالّة في عين المكان، وهذا ما سنفعلة بعد قليل بالنسبة لهذا الحدث.

المتحكم txt1 له خاصية من نوع Event اسمها KeyPress وبها الدالّة Add والتي تتطلب معطى argument من نوع دالّة ، هذه الدالّة نحددها نحن لتكون بمثابة المستمع listener لتستقبل الحدث وبياناته، أو كما يشار إليها عادة بأنها دالّة callback .

الدالّة التي سنحددها يجب أن تستقبل معطى من نوع EventArgs لذلك وجب استقبال هذه المعطى في محدّد parameter يتم ربطه عادة بالرمز e .

      txt1.KeyPress.Add(fun e -> 
                                lbl.Text <- e.KeyChar.ToString()  
                                + " " + int(e.KeyChar).ToString()
                                )

الدالّة التي كتبناها هنا ليس لها إسم anonymous (تسمّى أيضا lambda) ويعبّر عن وجودها بالكلمة المفتاحية fun متبوعة بمعطى argument وهو e ثم المعامل -> وبعده جسم الدالّة الذي يحتوي هنا على تعليمة واحدة قامت بتغيير قيمة الخاصية Text في المكوّن lbl بحيث تكون نصا يحتوي الحرف الذي تم ضغطه + رقم الحرف، فإذا ضغط المستخدم داخل المكون txt1 على المفتاح k ؛ يقوم مناول الحدث بتغيير كتابة المكون lbl لتصير k 107 كما في الصورة التالية:

الحدث التالي الذي سنضيفه هو حدث Click الخاص بالمكون btReverse ، إضافة هذه الحدث ستكون كالتالي:

      btReverse.Click.Add(x.Reverse) 

هنا طلبنا من المكون إضافة الحدث Click وكتبنا إسم الدالّة التي سيتم مناداتها عند وقوع الحدث. (لنكون أكثر دقة؛ حددنا دالّة تستقبل حدث Click وأعلمنا المكوّن بها كمشتركين لهذا الحدث، المكوّن من جهته عند وقوع الحدث إذا وجد دالّة مرتبطة بهذا الحدث يقوم بمناداتها مع ارسال البيانات اللازمة الخاصة بهذا الحدث.)

لو لاحظت: فإننا هنا اكتفينا فقط بذكر إسم الدالّة، ولم نكتبها كاملة كما فعلنا مع الحدث السابق. (لاحظ أيضا أن الدالّة Reverse لم نكتبها بعد).

ما دمنا هنا، لم لا نعالج حدثا آخر من الأحداث التي تقع على txt1 وهو حدث TextChanged الذي يقع كلما طرأ تغيير على مربع النص فيه. ونستفيد منه بأن يتم تحويل النص عند تغيّره ثم إظهاره في مربع txt2 ، دون أن نضطر إلى استخدام الزرّ كلما أردنا تحويله. إضافة الحدث تكون كالتالي:

      txt1.TextChanged.Add(x.Reverse)
	  

تعريف الدالّة Reverse

الدالّة التي سنقوم بإنشائها ستكون نهجية method تابع للصنفية MainForm كالتالي:

    member private x.Reverse _ = 
      txt2.Text <- txt1.Text

وهذه صورة لمقطع الكود يوضّح السياق الذي أتت فيه:

الدالّة عضو في صنفية MainForm والتي سبق أن أعطيناها إسما إشاريا لها وهو x . لذلك كان تعريف الدالّة Reverse ملحقا بهذا الإسم أي: x.Reverse .

التعليمة في جسم الدالّة (مؤقتا) مجرد تعليمة واحدة تقوم بنسخ محتويات txt1 إلى txt2، وسوف نقوم بعد قليل باستبدالها بالتعليمات اللازمة لقلب محتويات النص في txt1 ووضعه في txt2.لكن قبل ذلك أودّ أن نتفحّص تعريف هذه الدّالة بشيء من التفصيل حتى نتعرّف من خلالها على بعض جوانب الدوال في F# عموما.

الدالّة يجب أن تستقبل معطى argument من نوع EventArgs ، وهنا لم نرمز للمعطى ونربطه بأي إسم كما فعلنا سابقا عندما أعطينا الرمز e لأنه ببساطة لن نحتاج له لكي نستخدمه، لذلك اكتفينا بوضع علامة _ (علامة wildcard character)، أي كأننا نقول: لا يهمنا المعطى الذي تستقبله الدالّة ولا داعي لربط قيمته بأي إسم.

بعد تحديد اسم الدالّة و تحديد المعطى الذي تستقبله نضع علامة = التي بعدها يكون جسم الدالّة. وبهذا كأننا نقول بأن قيمة Reverse تساوي قيمة ناتج التعبيرات التي في جسم الدالّة.

لو مررنا بمؤشّر الفأرة في المحرر فوق إسم الدالّة Reverse سيقوم الاستشعار الذكي للكود intellisense بإظهار تلميحة يظهر فيها توقيعة signature الدّالة كما في الشكل التالي:

Reverse : EventArgs -> unit

هذه التوقيعة تقول بأن دالّة Reverse تستقبل قيمة من نوع EventArgs وتردّ قيمة من نوع unit . (سوف نشرح نوع unit بعد قليل)

السؤال هنا: كيف تعرّف المحرّر (ثم بعد ذلك مترجم F#) على نوع المحدد parameter وحددّه بأنه من نوع EventArgs ، وبأن ناتج الدالّة من نوع unit ، بالرغم من أننا لم نحدد ذلك صراحة؟

السرّ في ذلك هو نظام الاستدلال أو استدلال النوع type inference في F#. نظام الاستدلال يمكّن F# من استنتاج نوع البيانات للقيم والمتغيرات والمعطيات ونتائج الدوال وذلك من سياق التعابير في البرنامج.

فمثلا في حالتنا هذه قام F# بالإستدلال على نوع بيانات المعطى _ بسبب ما تم سابقا عندما حددنا هذه الدالّة كمعطى للحدث Click :

btReverse.Click.Add(x.Reverse)

وحيث أن هذه الدالّة يجب أن تستقبل مُعطى argument من نوع EventArgs فإنه بالضرورة على المُعطى الذي تستقبله دالّة Reverse أن يكون من نفس النوع. وهكذا الأمر بالنسبة لناتج هذه الدالّة.

لذلك في أحيان كثيرة لا نضطّر إلى التنويه annotation لنوع البيانات في برامجنا ونجعل F# يتكفّل باستنتاج ذلك آليا، ما لم يكن هناك تناقضا أو التباسا؛ عندها يقوم F# إما بالتنبيه أو الإشارة للخطأ. (يقوم المحرر بفحص تناسق وصحة أنواع البيانات بدون الاضطرار لبناء البرنامج)

مع هذا، بإمكانا إذا شئنا أن نحدّد نوع البيانات صراحة كما في النسخة التالية من الدالّة:

    member private x.Reverse (_: EventArgs) : unit =
      txt2.Text <- txt1.Text 

حيث حدّدنا بين قوسين المعطى ونوعه، ثم نوع ناتج الدالّة وذلك قبل جسم الدالّة.

نوع unit

في F#، كل دالّة لا بد لها أن تستقبل قيمة من نوع ما وأن تردّ قيمة من نوع ما. ماذا لو أردنا للدالّة أن لا ترد أي قيمة نحتاجها؟ أو أن طبيعة عمل الدالّة لا تحتاج أن تردّ قيمة؛ كأن تقوم بطباعة شيء ما أو تقوم بتغيير قيمة في مكان ما. هنا تقوم F# بالاستعاضة عن ذلك بأن تردّ قيمة من نوع unit وتعني عدم وجود قيمة، ويعبّر عن قيمتها بـ: () .

نوع unit شبيه بـ void في بعض اللغات. وفي حين أن النوع unit هو نوع بدون قيمة، أو بشكل أدق؛ نوع له قيمة وحيدة تشير لعدم وجود قيمة، نجد أن void في لغات أخرى قد تعني لاشيء أو بلا نوع. لذلك يمكنك في F# تعريف اسم أو رمز من نوع unit وتربطه بقيمته (أي بدون قيمة) كأن تقول: let x = () بينما لا يمكنك ذلك في C#:
void x;

استخدام الدالّة ignore

أحيانا نستخدم دالّة ما أو تعبير ما كتعليمة statment تقوم بأمر محدد، ولا يهمنا قيمة ناتجها، ولا نحتاج لربطها بمعرّف (خاصة عند التعامل مع دوال مكتبات .Net) فإذا كانت هذه الدالّة أو التعبير لا تردّ قيمة من نوع unit ؛ فإن F# ينبّه بأن قيمة ناتج هذه الدالّة لم يتم ربطها بأي إسم. فمثلا عند كتابة التعليمة التالية:

  System.Console.ReadKey()

تقوم الدالّة ReadKey() بانتظار المستخدم للضغط على مفتاح، ,وفي الوقت نفسه؛ نحن لا يهمنا قيمة ناتجها، عندها نحتاج إلى تحويل قيمة ناتج هذه الدالّة إلى نوع unit كطريقة نخبر بها F# أن يتجاوز هذه القيمة ويهملها. دالّة ignore تقوم بهذا الأمر، ونستخدمها كاتالي:

  ignore( System.Console.ReadKey() )

أو بطريقة أكثر شيوعا وارتباطا بأسلوب F#

  System.Console.ReadKey() |> ignore

القاعدة العامة، أن كلّ تعبير نوع ناتجه ليس unit يُفضل أن يربط بإسم أو يتم تحويل نوعه إلى unit. فمثلا: التعبير التالي نوعه int لأن قيمته وهي 4 من نوع int:

  2 + 2

التعبير أعلاه يجب ربط قيمته بإسم مثل:

  let a = 2 + 2

أو أن يتّم تحويل قيمته وبالتالي نوعه إلى unit مثل:

  2 + 2 |> ignore

كتابة دالّة Reverse

يهدف برنامجنا إلى أخذ النص المكتوب في إحدى مربعات الكتابة وعرضه معكوسا في مربع آخر، وهذا ما سوف تفعله دالّة Reverse.

سوف نقوم بهذا الأمر بعدة طرق، نحاول أن نتعرّف من خلالها على بعض جوانب F# . ثم نجري مقارنة سريعة بين هذه الطرق لمعرفة خصائص وعيوب كل واحدة منها.

باستخدام دورانية for

هذه الطريقة التي نستخدمها عادة في برامجنا باللغات الإلزامية imperative languages مثل C# وجافا، سننشئ دالّة reverseChars مهمتها استقبال سلسلة حروفية string والمرور على حروفها بداية من آخر حرف وحتى أول حرف.

لبناء سلسلة جديدة معكوسة باستخدام تركيبة for يمكن أن نصنع شيئا مثل التالي:

    member private x.Reverse _ = 
      
      let reverseChars (s: string) =
        let mutable newText = ""
        for x = s.Length - 1 downto 0 do
          newText <- newText + s.[x..x]  
        newText

      let s2 = reverseChars txt1.Text
      txt2.Text <- s2

كما تلاحظ؛ دالّة reverseChars هي دالّة داخلية nested function قمنا بوضعها داخل دالّة x.Reserve حتى تكون قريبة منا لأغراض هذا الشرح، لكن يمكننا طبعا وضعها في مكان آخر قبل هذه النقطة أو حتى في ملف آخر. ثم أنشأنا المعرّف s2 وربطناه بقيمة ناتج الدالّة بعد أن أمررنا لها قيمة txt1.Text . أخيرا قمنا بإسناد قيمة s2 للخاصية txt2.Text.

الدالّة تقريبا واضحة ولا تحتاج لشرح كبير، لكن لا يمنع من الإشارة لبعض الملاحظات:

اختبار الدالّة

يمكننا اختبار الدالّة بدون تنفيذ البرنامج، وذلك عبر أداة F# Interactive . قم بتضليل كامل الدّالة ثم اضغط Alt + Enter فيقوم المحرّر عندها بإرسال الكود المضلل إلى F# Interactive التي يباشر بتقييمه evaluate وطباعة ناتج التقييم.

فور قيام F# Interactive بتقييم الدالّة، يظهر توقيعة signature الدالّة إذا كان تعابير الدالّة صحيحة بدون أخطاء. التوقيعة تبيّن إسم الدالّة والمدخلات التي تستقبلها ونوعها، وكذلك نوع ناتج الدالّة كالتالي:

  val reverseChars : s:string -> string

بذلك تكون الدالّة جاهزة لإختبارها، نأتي لمربّع F# Interactive ونقوم بكتابة إسم الدالّة وقيمة المعطى لها، وإنهاء التعبير بفاصلتين منقوطتين ثم مفتاح الادخال.

  > reverseChars "abcdef";;
  val it : string = "fedcba"

المخرجات التي يظهرها F# Interactive هي قيمة ناتج الدالّة وبيان نوع القيمة وهي string في هذه الحالة.

يمكنك أيضا مناداة الدالّة بضخّ المعطى النصي لها بواسطة المعامل |> كالتالي:

  > "abcdef" |> reverseChars;;
  val it : string = "fedcba"

لاحظ وجود كلمة it وهي مجرد متغير داخلي يتم ربطه بقيمة آخر تعبير تم انتاجه في F# Interactive ، فلو كتبنا المعرّف it وطلبنا قيمته فإن الناتج سيكون قيمة آخر تعبير تم تقييمه مثل التالي:

  > it;;
  val it : string = "fedcba"

باستخدام دالّة ارتدادية

في البرمجة الدّالية functional programming، الأسلوب الافتراضي لإنجاز العمليات المتكررة هو استخدام دالّة ارتدادية recursive function أي الدّالة التي تنادي على نفسها كل مرة، ولاتتوقف الدالّة على هذه المناداة إلا إذا تحقّق شرطا محددا (يعرف هذا الشرط بـ base case).

في F# يتم وضع الكلمة المفتاحية rec قبل إسم الدالّة الارتدادية، كما في النسخة التالية من الدالّة:

    member private x.Reverse _

      let rec reverseChars s newText =
        match s with                                     //(1)
        |"" -> newText                                   //(2)
        |_  -> reverseChars s.[1..] (s.[0..0] + newText) //(3)

      let s2 = reverseChars txt1.Text ""
      txt2.Text <- s2

في هذه الدّالة استخدمنا إحدى أشهر أدوات البرمجة الدالّية لفرز البيانات ,والتحكّم في تدفّقها؛ وهي مطابقة الأنماط pattern matching . الشبيه بـ switch-case . استخدمنا تركيبة match with لمراقبة نمطين لقيمة s ، النمط الأول أن تكون قيمة s تساوي ""، في هذه الحالة ترد قيمة newText ، والنمط الثاني أن تكون قيمة s أية حروف أخرى، وقد عبّرنا عن النمط التالي بعلامة _ ، وفي هذه الحالة يتم مناداة الدالة لنفسها يمعطيات جديدة.

لا ننسى أن ناتج أية دالّة هو آخر تعبير يتحقّق فيها.

تتجلّى فائدة pattern matching أكثر عند تعدد الخيارات والشروط وكذلك تنوع وتعدد القيم بتركيباتها المختلفة التي يتعيّن تفكيكها وفحصها، بطريقة تتفوق بها على أدوات if أو switch-case، لذلك وبسبب بساطة الشرط الذي احتجناه في الدالّة، ربما يكون الكود أكثر مقروئية وبساطة لو كان بالشكل التالي:

  if s = "" then newText
  else  
    reverseChars s.[1..] (s.[0..0] + newText)

عند مناداة الدالّة لأول مرة: ( let s2 = reverseChars txt1.Text "") تكون قيمة المعطى الأول هو نص txt1.Text ، وتكون قيمة المعطى الثاني نصا فارغا.

(1) تقوم الدالّة أولا بفحص قيمة المعطى الأول s فإذا كان فارغا (2) تردّ قيمة المعطى الثاني newText وينتهي عمل الدالّة.

أما إذا كانت قيمة المعطى s تحمل أحرفا؛(3) تقوم الدالّة بمناداة نفسها بحيث تكون قيمة المعطى الأول هي قيمة النص في S ناقصا الحرف الأول s.[1..] ، وقيمة المعطى الثاني هي قيمة الحرف الأول من النص s.[0..0] مضافا بعده قيمة newText.

تقوم الدالّة عند استدعائها مرة أخرى بنفس الخطوات، حتى يتم استخلاص كل الأحرف وتصير قيمة s فارغة، عندها تردّ قيمة المعطى الثاني الذي استقبلته وينتهي دوران الدالّة حول نفسها.

لمتابعة عمل الدالّة خطوة خطوة؛ نفترض أن المطلوب من الدالّة قلب الأحرف ABC ، لذلك فإن أول مناداة لها تكون كالتالي:

  s2 = reverseChars "ABC" ""

حبث تستقبل الدالّة المعطى s وقيمته "ABC" والمعطى newText وقيمته ""، فتقوم بمناداة نفسها مرة أخرى مع تمرير قيم جديد في معطيات النداء. قيمة المعطى الأول ستكون قيمةS بعد حذف الحرف الأول فيكون "BC" وقيمة المعطى الثاني هو أول حرف من S وهو A وتضيف قبله قيمة newText وهي "" فتصبح A كالتالي:

  reverseChars "BC" "A"

تقوم الدالّة في استدعائها التالي بتطبيق نفس الخطوات على القيم التي استقبلتها وتنتج قيما جديدة تنادي بها نفسها:

  reverseChars "C" "BA" 
  reverseChars "" "CBA" 

وهكذا حتى تصير قيمة s فارغة ، عندها تتوقف عند مناداة نفسها، وتردّ قيمة المعطى الثاني CBA لنقطة الاستدعاء قبلها وهكذا لتكون هذه القيمة هي الناتج النهائي للدالّة.

قد يجد غير المتعوّد على الدوال الارتدادية صعوبة في تصوّرها وتتبع عملها وتوقّع مخرجاتها، وهذا أمر طبيعي، إلإ أنه مع الممارسة وترويض الذهن وتمرينه على التفكير بهذا الأسلوب؛ سيجد أن استخدام الدوال الارتدادية أكثر أريحية وربّما سيفضّلها عن استخدام أدوات التكرار مثل for و while.

باستخدام دوال F#

الطريقة الثالثة التي سنستخدمها لمعالجة النص وإظهاره معكوسا؛ هي باستخدام الدوال الجاهزة في F# ومكتبات .Net سوف لن ننشئ دالّة لهذا الغرض كما فعلنا في الطريقتين السابقتين، بل فقط ربط مجموعة من الدوال الجاهزة التي من شأنها تحقيق المطلوب، كالتعبير التالي:

    member private x.Reverse _ = 
 
      let s2 = txt1.Text.ToCharArray() |> Array.rev |> String

      txt2.Text <- s2

التعبير السابق عبارة عن توالي سلسلة من الدوال، كل دالّة تستقبل قيمة من ما قبلها وتحيل ناتجها إلى الدالّة التي تليها باستخدام المعامل pipeline |> . وترجمتها كالتالي:

في هذه الطريقة استخدمنا ما هو متوفر من دوال جاهزة في مكتبات F# و .Net بحيث أخذنا النص الأصلي وحولناه إلى مصفوفة أحرف باستخدام ToCharArray وذلك لنستغل دالّة أخرى مهمتها قلب ترتيب عناصر مصفوفة وهي دالّة Array.rev التي قامت بذلك ووضعتها في مصفوفة جديدة، ثم تسليمها للدالّة String لتقوم بتحويل أحرف المصفوفة إلى نصّ. هذا النص يتم وضعه في المربع txt2.

المعامل |> يُسمى forward pipeline حيث يقوم بأخذ قيمة ما قبله ويحيلها كمدخل لدالّة بعده، بحيث إذا استعمل المعامل بين مجموعة دوال؛ تبدو وكأنها خط أنابيب، كل دالّة تضخ ناتجها للدالّة التي تليها:

  
      let s2 = txt1.Text.ToCharArray() |> Array.rev |> String

وهذا يعادل التعبير التالية :

      txt2.Text <- String(Array.rev(txt1.Text.ToCharArray()))

 

مقارنة بين نسخ الدالّة

فيما يلي النسخ الثلاث من الدالّة:

let reverseChars (s: string) =
  let mutable newText = ""
  for x = s.Length - 1 downto 0 do
    newText <- newText + s.[x..x]
  newText
 
 
let rec reverseChars s newText =
  match s with
  |"" -> newText
  |_  -> reverseChars s.[1..] (s.[0..0] + newText)
 
 
txt1.Text.ToCharArray() |> Array.rev |> String

الطريقة الأولى هي الأكثر سرعة وكفاءة، لأنها الأكثر مقاربة لطريقة عمل الحاسوب، لكن احتواء هذه الطريقة على متغير متحوّل mutable لايثبت على قيمة محددة؛ قد يزعج مبرمجي اللغات الدالّية، لأنه إذا تضخّم حجم الكود داخل الدورانية وتكاثرت فيها المتغيّرات المتحولّة ، فإن هذه المتغيّرات قد تكون مصدرا محتملا للثغرات وما تسبّبه من متاعب لتعقّبها.

الطريقة الثانية؛ باعتماد دالّة ارتداية، تبدو أنيقة وموجزة ، وتتماشى مع الأسلوب العام للبرمجة الدالّية، وهي بالنسبة للمتمرسين في البرمجة الدالّية أكثر مقروئية من سابقتها، وأكثر صلابة (يمكن توقع خصائص الكود ونتيجته بدون تجربته reason about). أيضا فإن كفاءة وسرعة الكود في هذه النسخة من الدالّة ستكون مقاربة لسرعة الطريقة الأولى.

الطريقة الثالثة؛ تبدو أكثر إيجازا وأناقة وأكثر تعبيرية والفضل في ذلك إلى دالّة Array.rev التي قامت بالعمل الأساسي، لكن يعيبها تعدد البناءات والعمليات التي تمت فيها، فهي أولا قامت ببناء مصفوفة جديدة ووضعت فيها الأحرف الأصلية، ثم قامت ببناء مصفوفة أخرى بالمواضع الجديدة للأحرف، ثم تحويل هذه المصفوفة ثانية إلى نص. لاشك في أن هذه الطريقة هي الأكثر كلفة من حيث الموارد والوقت.

أي الطرق أفضل؟ الإجابة المعتادة: الأمر يعتمد على ...!

في البرمجة عادة ما يتم تفضيل الكود الموجز الذي يسهل استيعابه وتتبعه وتوقّع نتائجه، وأن يكون خال ما أمكن من المؤثرات والتأثيرات الجانبية side effects (أي أن الدالّة لا تتأثر إلا بالمعطيات التي تستقبلها، ولا تؤثّر في شيء خارجها)، وهذا يمكن ضمانه باتباع الأعراف والأساليب التي تحكم البرمجة الدالّية مثل؛ تفضيل القيم غير المتحولة على المتغيرات المتحولة، الدول الارتدادية على البناءات التكرارية iterative ، مطابقة الأنماط.

من جهة أخرى فإن المهام التي تكون فيها سرعة الأداء وكفاءته أمرا حيويا، فإن الأمر يتطلب استخدام الأساليب البرمجية التي تضمن الوصول إلى هذه الأهداف، وهنا تأتي أفضلية استخدام المتغيرات المتحولة mutable variables وتكرارات for و while فهي بلا شك الأكثر سرعة وكفاءة لأنها الأقرب للغة العتاد machine code وطريقة عمله.

ولكن المحك الرئيسي لاختيار طريقة أو أسلوب ما في البرمجة عموما هو: القياس. أن تقيس نتائج كل طريقة على البيانات الحقيقية التي يتعامل معها برنامجك وعلى مقدار الجهد الذي يواجهه، وعلى ما هو متوقع منه. لذلك قد ترى نفسك تنتقل من طريقة إلى أخرى كلما تغيرت طبيعة البيانات، أو تغيّر حجمها أو تغيّر معدل طلبات النفاذ إليها عبر الزمن.

ضبط الدّالة الارتدادية

يجب الانتباه إلى أنه مالم يتم ضبط optimize الدالّة الارتدادية بحيث تراعي مبدأ tail call ؛ فإن تضخم عدد النداءات التي تقوم بها تجعل الدّالة مرشّحة للتوقّف والتسبب في عطل برنامجك. (عدد النداءات في برنامجنا سيكون مساويا لعدد الأحرف في النصّ).

ماذا يحدث لو أزلنا الأقواس في التعبير التالي، ثم نجرّب البرنامج على نصّ كبير الحجم؟

    reverseChars s.[1..] (s.[0..0] + newText)

لنتعرّف على تأثير هذا المبدأ على عمل الدالّة حاول أن تقوم بتعديل الكود وذلك بحذف القوسين المحيطين بالمعطى الثاني، ليكون آخر استدعاء في الدالّة بالشكل التالي:

reverseChars s.[1..] s.[0..0] + newText

هنا تم تعديل المعطى الثاني ليكون فقط s.[0..0] . معنى هذا؛ أن ناتج هذا التعبير لن يتحقق حتى يتم جمع ناتج استدعاء الدالّة مع قيمة newText.

لذلك بعد أن تتم سلسلة الاستدعاءات المتوالية، وفي آخر استدعاء، سيعود المترجم للخلف لنقطة الاستدعاء السابقة لالتقاط وحساب القيمة المتعقبة (في حالتنا newText) ، ثم يحمل نتيجته ويعود لنقطة الاستدعاء التي قبلها ليقوم بنفس الأمر حتى أول استدعاء، هذه العودة المتوالية للإستدعاءات السابقة؛ تجبر المترجم على تخصيص مرجع ومساحة لكل استدعاء حتى يعود إليه ، فإذا تضخمت عدد المساحات وتراكمت تختنق الذاكرة ويتوقف البرنامج مع عطل stack over flow.

لذلك في نسخة الدالّة التي اعتمدناها راعينا مبدأ tail call عند استدعائها من داخلها مرة أخرى، وجعلناها تحيل ناتج تراكم النص المقلوب (newText) إلى الإستدعاء الذي يليه دون الاضطرار للعودة لمراكمة القيمة، وبذلك فإن الدالّة ستقوم بعملها مهما كان النص ضخما.

جرى العرف في الدوال الارتدادية أن يتم تسمية المعطى الذي تتراكم فيه القيم ب acc اختصارا لكلمة accumulator أو accumulation بمعني مراكم أو تراكم. وقد أسمياه هنا newText للتوضيح.

تحسين الدالّة

بالرغم من قيام الدوال السابق عرضها بقلب حروف النص الأصلي؛ إلا أن فيها عيب بسيط، وهو أنه مهما كان عدد الأسطر التي يحتويها النص الأصلي؛ فإن الناتج يظهر دائما كسطر واحد. كما في الصورة التالية:

السبب في ذلك أن علامتي إنهاء السطر (carriage return) وبدء سطر جديد (new line) يتم عكس ترتيبهما أيضا بعد التحويل مما يربك مفعولهما داخل مربّع النص. العلامتين يشار لهما ب ‘\r’ و ‘\n’ أو برقميهما ‘\013’ و ‘\010’ على التوالي. العلاماتان غير منظورتين ، ولكن يمكن أن نعبّر عنهما داخل الكود كالتالي:

"ABCD\r\nEFG"

وعند قلب الأحرف تصير بالشكل التالي:

"ABCD\n\rEFG"

لذا الأمر يتطلب معالجة ذلك بحيث نحافظ على العلامتين بموضعهما الأصلي.

كحلّ سريع يمكن يمكن إضافة دالّة جاهزة تقوم باستبدال الأحرف بأخرى واستغلالها بحيث يعود الحرفان إلى ترتيبهما الأول:

      let s2 = reverseChars txt1.Text ""
      let s2 = s2.Replace("\n\r""\r\n")

استبدال الأحرف باستخدام الدالّة map

لو أخذنا آخر نسخة من دالّة Reverse يمكننا حشر دالّة في المنتصف تقوم باستبدال موضع العلامتين، وذلك باستخدام دالّة map . للتذكير؛ التعبير السابق كان كالتالي:

      let s2 = txt1.Text.ToCharArray() |> Array.rev |> String

بعد حشر دالّة map ستكون كما يلي:


      let s2 =
        txt1.Text.ToCharArray() 
        |> Array.rev 
        |> Array.map (fun x -> if x = '\r' then '\n' 
                               elif x = '\n' then '\r' 
                               else x) 
        |> String

دالّة map هي إحدى أشهر الدوال في لغات البرمجة الدالّية، حيث يتم إعطاؤها دالّة تقوم بتطبيقها على كل عنصر من عناصر متوالية (مصفوفة، قائمة،..) لتنتج متوالية أخرى بالقيم الجديدة للعناصر.

في مثالنا السابق تم ضخّ المصفوفة التي انتجتها Arrary.rev إلى دالّة Array.map ، وأعطيناها دالّة بدون إسم لتقوم بتطبيقها على كل عنصر من عناصر المصفوفة. ستقوم دالّة map بأخذ العنصر الأول من المصفوفة وتعطيه لدالتنا التي ستستقبله في الرمز x ، تقوم دالتنا بفحص x فإذا كانت قيمتها تساوي‘\r’ يكون ناتج الدالّة ‘\n’ وإذا كانت تساوي ‘\n’ يكون ناتجها ‘\r’ ، غير ذلك يكون ناتجها نفس قيمة X. تأخذ الدالّة map الناتج وتضعه في مصفوفة جديدة، وتكرّر نفس الخطوات مع العنصر التالي في المصفوفة الأولى حتى آخر عنصر، المصفوفة الجديدة تكون ناتج دالّة map .

إضافة لائحة أوامر للنافذة MainForm

تبقّى لنا الآن إضافة لائحة أوامر لنافذة برنامجنا. لائحة الأوامر ستكون كالشكل التالي:

سيتم بناء كود كامل عناصر لائحة الأوامر وما يتبعها في دالّة واحدة، ويكون ناتج الدالّة مكوّن من نوع MenuStrip الذي سيتم ضمّه إلى بقية مكوّنات النافذة. يمكن أن نضع هذه الدالّة بعد تعريف btReverse الذي سبق وعرّفناه. كود الدالّة كالتالي:

    
    let txt2 = new TextBox()
    let btReverse = new Button()
 
    let menuStrip =
      let menu = new MenuStrip()
    
      let mnuFile = new ToolStripMenuItem("&File")
      let mnuNew = new ToolStripMenuItem("&New")
      let mnuExit = new ToolStripMenuItem("E&xit")
 
      mnuFile.DropDownItems.Add(mnuNew) |> ignore   
      mnuFile.DropDownItems.Add(mnuExit) |> ignore
      mnuNew.Click.Add(fun _ -> txt1.Clear()) 
      mnuExit.Click.Add(fun _ -> x.Close())
 
      let mnuEdit = new ToolStripMenuItem("&Edit")
      let mnuCopy = new ToolStripMenuItem("&Copy")
      let mnuPaste = new ToolStripMenuItem("&Past")
  
      mnuEdit.DropDownItems.Add(mnuCopy) |> ignore
      mnuEdit.DropDownItems.Add(mnuPaste) |> ignore
      mnuCopy.Click.Add(fun _ -> txt1.Copy() )
      mnuPaste.Click.Add(fun _ -> txt1.Paste() )
 
      menu.Items.Add(mnuFile) |> ignore
      menu.Items.Add(mnuEdit) |> ignore
 
      menu

في هذه الدالّة قمنا بإنشاء مكوّن من نوع MenuStrip وأسميناه menu والذي سيشكل مجموع نظام لائحة الأوامر وعناصرها، ثم أنشأنا مجموعة من العناصر من نوع ToolStripMenuItem تتبع بعضها البعض، هذه العناصر الفرعية تندرج تحت عنصرين أساسيين mnuFile و mnuEdit وتحتهما تتفرع بقية العناصر، بينما سوف يتبع العنصران الأساسيان لنظام لائحة الأوامر الأساسية menu. أيضا حددنا لبعض العناصر تعليمات يتم تنفيذها بناء على حدث Click فيها.

هذه الدالّة سوف تردّ ناتجا عبارة عن متحكّم من نوع MenuStrip والذي يتمثل في العنصر أو الكائن menu والذي تم التعبير عنه أو الإشارة إليه في آخر الدالّة. (تذكر أن آخر تعبير في الدالّة يمثل قيمتها أو ناتجها).

رأينا سابقا كيف أضفنا العناصر التي أنشأناها إلى النافذة/ النموذج، مثل عنصر btReverse الذي أضفناه باستخدام التعليمة x.Controls.Add(btReverse) ، نفس الأمر سوف نقوم به بالنسبة لشريط الأوامر كالتالي:

      
      x.Controls.Add(btReverse)  
      x.Controls.Add(stripMenu)

هنا أضفنا ناتج الدالّة stripMenu والذي هو متحكم شريط الأوامر في النافذة.

لاحظ أنه في دالّة stripMenu إن تعليمة مثلmnuEdit.DropDownItems.Add(mnuCopy) تم ضخ ناتجها إلى دالّة ignore وذلك لأن ناتجها ليس من نوع unit لذا وجب ربط قيمتها بإسم بواسطة let أو تحويلها إلى قيمة من نوع unit باستخدام دالّة ignore.

استخدام F# Interactive للتعامل مع النوافذ

أحيانا، نحتاج إلى أن نتعامل بصورة مؤقتة مع النوافذ، فقط لإجراء اختبارات على بعض نواحي برامجنا. هذا الأمر بالإمكان تحقيقيه دون أن نضطر إلى إنشاء برنامج خاص بذلك، حيث يمكن أن نستعين بنظام F# التفاعلي F# Interactive لإنشاء نافذة وتوصيفها وتشغيلها.

(يجب إنهاء التعبير بفاصلتين منقوطتين ثم مفتاح الإدخال؛ ليقوم التفاعلي بإرساله للمترجم)

أولا نطلب من التفاعلي التعرّف على مكتبة System.Windows.Forms :

> open System.Windows.Forms;;

ثم إنشاء نافذة وتحديد عنوانها:

> let f = new Form(Text = "My Test Form");;

وسيقوم التفاعلي بتقييم هذا التعبير وطباعة نتيجته وهي قيمة f:

val f : Form = System.Windows.Forms.Form, Text: My Test Form 

الآن علينا فقط إظهار هذه النافذة بالأمر التالي:

> f.Show();;

وسيردّ علينا التفاعلي بطباعة نتيجة التعبير وهي unit

val it : unit = ()
> 

وفي نفس الوقت يظهر لنا النافذة كما في الشكل التالي:

التعبير التالي لإنشاء زرّ وتحديد النصّ عليه:

> let btn = new Button(Text = "OK");;
val btn : Button = System.Windows.Forms.Button, Text: OK

ثم تحديد دالّة لتنفيذها عند الحدث Click للزرّ:

> btn.Click.Add(fun _ -> f.Text <- "My new form is Ok.");;
val it : unit = ()

ثم إضافة مكوّن الزرّ إلى النافذة:

> f.Controls.Add(btn);;
val it : unit = ()

سيظهر مكوّن الزرّ فورا على النافذة. وإذا ضغطنا على الزر؛ سيتم تغيير عنوان النافذة:

التعابير السابقة، يمكن أيضا كتابتها في أي مكان في المحرّر وإرسالها للتفاعلي لترجمتها ووطباعة قيمتها، وذلك بوضع مؤشر المحرر على السطر المراد تقييمه والضغط على مفتاحي Alt + Enter ، أو من لائحة الأوامر Execute In Interactive .

أيضا يمكن تظليل الأسطر التي تحوي هذه التعابير وإرسالها دفعة واحدة للتفاعلي بنفس الطريقة لتقييم أو تنفيذ هذه التعابير. (كما فعلنا عند اختبار الدالّة سابقا)


open System.Windows.Forms
let f = new Form(Text ="My Test Form")
f.Show()
let btn = new Button(Text = "OK")
f.Controls.Add(btn)

 

نظام REPL

يعرف نظام تشغيل الكود تفاعليا بإسم REPL وهي اختصار لكلمات Read, Evaluate, Print, Loop وتعني قراءة التعبير البرمجي، تقييمه (تحويله إلى قيمة)، طباعة القيمة، تكرار العملية.

نظام REPL في F# يُسمّى F# Interactive ، يتم فيه كتابة التعبير في سطرأو أكثر، ويختم التعبير بفاصلتين منقوطتين ثم مفتاح الإدخال، ليتم إرساله لمترجم F# لتقييمه، فإذا كان التعبير صحيحا؛ يقوم النظام بطباعة القيمة الناتجة ونوعها.

يسهم التفاعلي بشكل كبير في زيادة انتاجية المبرمج واختصار وقته البرمجي؛ حيث يمكنه اختبار التعابير البرمجية والدوال في برامجه من خلاله مباشرة، وبذلك لن يحتاج لتشغيل برنامجه كل مرة يريد فيها اختبار شيئ جديد.

خاتمة

لم أستطع أن أخرج بخاتمة ترضيني لهذا المقال، لكن أثناء تفكيري في ذلك؛ خطر على بالي هذا السؤال:

كم مرّة وردت كلمة F# في هذه المقالة؟

لمعرفة ذلك، قمت بحفظ محتوى هذه المقالة في ملف نصي، ثم نفذت التعابير التالية:

open System
open System.IO

let s = File.ReadAllText(@"C:\Trash\article.txt")
s.Split([|' '|]) |> Array.filter (fun x -> x.Contains("F#")) |> Array.length

وكانت النتيجة:

  val it : int = 47

 

 

الملفات المصدرية للمشروع