برمجة Java الوظيفية مع Vavr

سمع الكثير من اللغات الوظيفية مثل هاسكل وكلوجور. ولكن هناك لغات مثل سكالا ، على سبيل المثال. فهو يجمع بين OOP ونهج وظيفي. ماذا عن جافا القديمة الجيدة؟ هل من الممكن كتابة البرامج بأسلوب وظيفي عليها وكم يمكن أن تؤذي؟ نعم ، هناك Java 8 و lambdas مع الجداول. هذه خطوة كبيرة للغة ، لكنها لا تزال غير كافية. هل من الممكن الخروج بشيء في هذه الحالة؟ اتضح نعم.



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

دعونا ننظر في الهياكل الوظيفية بمزيد من التفصيل. يجب أن يستوفي أي هيكل من هذا القبيل شرطين على الأقل:

  • غير قابل للتغيير - يجب أن يكون الهيكل غير قابل للتغيير. هذا يعني أننا نصلح حالة الكائن في مرحلة الخلق ونتركه على هذا النحو حتى نهاية وجوده. مثال واضح على انتهاك شرط: ArrayList القياسي.
  • دائم - يجب تخزين الهيكل في الذاكرة لأطول فترة ممكنة. إذا أنشأنا كائنًا ، فبدلاً من إنشاء كائن جديد بنفس الحالة ، يجب أن نستخدم الكائن الجاهز. بشكل أكثر رسمية ، تحتفظ هذه الهياكل بجميع حالاتها السابقة عند التعديل. يجب أن تظل الإشارات إلى هذه الشروط عاملة بالكامل.

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

هياكل البيانات الرئيسية لمكتبة vavr


الصف


tuples واحدة من أبسط هياكل البيانات الوظيفية والأساسية. tuple هو مجموعة مرتبة ذات طول ثابت. على عكس القوائم ، يمكن أن يحتوي الصف على بيانات من أي نوع.

Tuple tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42) 

الحصول على العنصر المطلوب يأتي من استدعاء الحقل برقم العنصر في المجموعة.

 ((Tuple4) tuple)._1 // 1 

يرجى ملاحظة ما يلي: تبدأ فهرسة الصفوف من 1! بالإضافة إلى ذلك ، للحصول على العنصر المطلوب ، يجب أن نقوم بتحويل كائننا إلى النوع المطلوب باستخدام مجموعة الطرق المناسبة. في المثال أعلاه ، استخدمنا مجموعة من 4 عناصر ، مما يعني أن التحويل يجب أن يكون من النوع Tuple4 . في الواقع ، لا أحد يمنعنا من صنع النوع الصحيح في البداية.

 Tuple4 tuple = Tuple.of(1, "blablabla", .0, 42L); // (1, blablabla, 0.0, 42) System.out.println(tuple._1); // 1 

أعلى 3 مجموعات فافر


قائمة


إنشاء قائمة باستخدام vavr أمر بسيط للغاية. أسهل من دون vavr .

 List.of(1, 2, 3) 

ماذا يمكننا أن نفعل مع هذه القائمة؟ حسنًا ، أولاً ، يمكننا تحويلها إلى قائمة جافا قياسية.

 final boolean containThree = List.of(1, 2, 3) .asJava() .stream() .anyMatch(x -> x == 3); 

لكن في الواقع ، هذا ليس ضروريًا جدًا ، لأنه يمكننا القيام بذلك ، على سبيل المثال ، مثل:

 final boolean containThree = List.of(1, 2, 3) .find(x -> x == 1) .isDefined(); 

بشكل عام ، تحتوي قائمة مكتبة vavr القياسية على العديد من الطرق المفيدة. على سبيل المثال ، هناك وظيفة التفاف قوية إلى حد ما تسمح لك بدمج قائمة القيم حسب بعض القواعد والعنصر المحايد.

 //   final int zero = 0; //   final BiFunction<Integer, Integer, Integer> combine = (x, y) -> x + y; //   final int sum = List.of(1, 2, 3) .fold(zero, combine); //   

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

القائمة المرتبطة كقائمة افتراضية

دعونا نصنع قائمة مرتبطة بكل بساطة بأشياء ثابتة. سيبدو شيء من هذا القبيل:

الصورة

مثال على الرمز
 List list = List.of(1, 2, 3); 


يحتوي كل عنصر من عناصر القائمة على طريقتين رئيسيتين: الحصول على عنصر الرأس (الرأس) وجميع العناصر الأخرى (الذيل).

مثال على الرمز
 list.head(); // 1 list.tail(); // List(2, 3) 


الآن ، إذا أردنا تغيير العنصر الأول في القائمة (من 1 إلى 0) ، فنحن بحاجة إلى إنشاء قائمة جديدة مع إعادة استخدام الأجزاء النهائية.

الصورة
مثال على الرمز
 final List tailList = list.tail(); //    tailList.prepend(0); //      


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

قائمة الانتظار


هناك قائمة بيانات أخرى مفيدة للغاية وهي قائمة الانتظار. كيف تصنع طابورًا لبناء برامج فعالة وموثوقة بأسلوب وظيفي؟ على سبيل المثال ، يمكننا أن نأخذ هياكل البيانات المعروفة لنا بالفعل: قائمتان ومجموعة tuple.

الصورة

مثال على الرمز
 Queue<Integer> queue = Queue.of(1, 2, 3) .enqueue(4) .enqueue(5); 


عندما ينتهي الأول ، نوسع الثاني ونستخدمه للقراءة.

الصورة

الصورة

من المهم أن نتذكر أن قائمة الانتظار يجب أن تكون دون تغيير ، مثل جميع الهياكل الأخرى. ولكن ما فائدة الطابور الذي لا يتغير؟ في الواقع ، هناك خدعة. نظرًا لقيمة قائمة الانتظار المقبولة ، نحصل على مجموعة من عنصرين. أولاً: عنصر الصف المطلوب ، ثانيًا: ما حدث لقائمة الانتظار بدون هذا العنصر.

 System.out.println(queue); // Queue(1, 2, 3, 4, 5) Tuple2<Integer, Queue<Integer>> tuple2 = queue.dequeue(); System.out.println(tuple2._1); // 1 System.out.println(tuple2._2); // Queue(2, 3, 4, 5) 

تيارات


بنية البيانات الهامة التالية هي الدفق. الدفق هو دفق من تنفيذ بعض الإجراءات على مجموعة معينة من القيم مجردة في كثير من الأحيان.

قد يقول أحدهم أن Java 8 لديها بالفعل تيارات كاملة ولا نحتاج إلى تيارات جديدة على الإطلاق. هل هذا صحيح؟

للبدء ، دعنا نتأكد من أن تدفق جافا ليس بنية بيانات وظيفية. تحقق من بنية قابلية التغيير. للقيام بذلك ، قم بإنشاء دفق صغير:
 IntStream standardStream = IntStream.range(1, 10); 

سنفرز جميع العناصر في الدفق:

 standardStream.forEach(System.out::print); 

ردًا على ذلك ، نحصل على الإخراج إلى وحدة التحكم: 123456789 . دعونا نكرر عملية القوة الغاشمة:

 standardStream.forEach(System.out::print); 

عفوًا ، حدث الخطأ التالي:

 java.lang.IllegalStateException: stream has already been operated upon or closed 

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

على سبيل المثال ، لا يشير تعريف الدفق إلى أي شيء حول تحديد عدد العناصر. لسوء الحظ ، إنه موجود في المكرر ، مما يعني أنه في التدفقات القياسية.

لحسن الحظ ، تحل مكتبة vavr هذه المشاكل. تأكد من ذلك:

 Stream stream = Stream.range(1, 10); stream.forEach(System.out::print); stream.forEach(System.out::print); 

رداً على ذلك نحصل على 123456789123456789 . مما يعني أن العملية الأولى لم "تفسد" تيارنا.

دعونا نحاول إنشاء دفق لا نهائي:

Stream infiniteStream = Stream.from (1) ؛
System.out.println (infiniteStream) ؛ // Stream (1 ،؟)

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

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

 Stream oddNumbers = Stream .from(1, 2) //  1   2 .map(x -> x + " "); //  //   oddNumbers.take(5) .forEach(System.out::print); // 1 3 5 7 9 oddNumbers.take(10) .forEach(System.out::print); // 1 3 5 7 9 11 13 15 17 19 

بسيط جدا.

الهيكل العام للمجموعات


بعد أن ناقشنا الهياكل الأساسية ، حان الوقت للنظر في الهندسة العامة للمجموعات الوظيفية vavr :



يمكن استخدام كل عنصر من عناصر الهيكل على أنه قابل للتكرار:

 StringBuilder builder = new StringBuilder(); for (String word : List.of("one", "two", "tree")) { if (builder.length() > 0) { builder.append(", "); } builder.append(word); } System.out.println(builder.toString()); // one, two, tree 

ولكن يجب أن تفكر مرتين وترى قفص الاتهام قبل استخدامه. تتيح لك المكتبة تسهيل الأمور المألوفة.

 System.out.println(List.of("one", "two", "tree").mkString(", ")); // one, two, tree 

العمل مع الوظائف


تحتوي المكتبة على عدد من الوظائف (8 قطع) وطرق مفيدة للعمل معها. إنها واجهات وظيفية عادية مع العديد من الطرق المثيرة للاهتمام. يعتمد اسم الدالات على عدد الوسائط المقبولة (من 0 إلى 8). على سبيل المثال ، لا تأخذ الدالة 0 أية وسيطات ، بينما تأخذ الدالة 1 وسيطة واحدة ، بينما تأخذ الدالة 2 اثنتين ، وما إلى ذلك.

 Function2<String, String, String> combineName = (lastName, firstName) -> firstName + " " + lastName; System.out.println(combineName.apply("Griffin", "Peter")); // Peter Griffin 

في وظائف مكتبة vavr ، يمكننا القيام بالكثير من الأشياء الرائعة. من حيث الوظائف ، فإنها تتقدم كثيرًا على الوظيفة القياسية والوظيفة الحيوية وما إلى ذلك. على سبيل المثال ، الكاري. الكاري هو بناء الوظائف في أجزاء. دعونا نلقي نظرة على مثال:

 //    Function2<String, String, String> combineName = (lastName, firstName) -> firstName + " " + lastName; //           Function1<String, String> makeGriffinName = combineName .curried() .apply("Griffin"); //      System.out.println(makeGriffinName.apply("Peter")); // Peter Griffin System.out.println(makeGriffinName.apply("Lois")); // Lois Griffin 

كما ترون ، بإيجاز شديد. طريقة الكاري بسيطة للغاية ، ولكنها يمكن أن تكون مفيدة جدًا.

تنفيذ طريقة الكاري
 @Override default Function1<T1, Function1<T2, R>> curried() { return t1 -> t2 -> apply(t1, t2); } 


هناك العديد من الطرق المفيدة في مجموعة الوظائف . على سبيل المثال ، يمكنك تخزين النتيجة المرجعة لدالة ما في ذاكرة التخزين المؤقت:

 Function0<Double> hashCache = Function0.of(Math::random).memoized(); double randomValue1 = hashCache.apply(); double randomValue2 = hashCache.apply(); System.out.println(randomValue1 == randomValue2); // true 


محاربة الاستثناءات


كما قلنا سابقًا ، يجب أن تكون عملية البرمجة آمنة. للقيام بذلك ، من الضروري تجنب العديد من الآثار الدخيلة. الاستثناءات هي مولداتهم الصريحة.

يمكنك استخدام الفئة Try للتعامل مع الاستثناءات بأسلوب وظيفي بأمان. في الواقع ، هذا monad نموذجي. إن الخوض في نظرية الاستخدام ليس ضروريًا. ما عليك سوى إلقاء نظرة على مثال بسيط:

 Try.of(() -> 4 / 0) .onFailure(System.out::println) .onSuccess(System.out::println); 

كما ترون من المثال ، كل شيء بسيط للغاية. نحن ببساطة نعلق الحدث على خطأ محتمل ولا نتجاوزه عن حدود الحساب.

مطابقة الأنماط


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

 import static io.vavr.API.*; import static io.vavr.Predicates.*; public class PatternMatchingDemo { public static void main(String[] args) { String s = Match(1993).of( Case($(42), () -> "one"), Case($(anyOf(isIn(1990, 1991, 1992), is(1993))), "two"), Case($(), "?") ); System.out.println(s); // two } } 

يرجى ملاحظة أن الأحرف كبيرة ، مثل الحالة عبارة عن كلمة رئيسية مأخوذة بالفعل.

الخلاصة


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

Source: https://habr.com/ru/post/ar421839/


All Articles