مواصفات محلل: مسار جدي

هناك نوعان من المطورين الذين يستخدمون erlang و elixir: أولئك الذين يكتبون المواصفات لـ Dialyzer ، وأولئك الذين لم يكتبوا بعد. في البداية ، يبدو الأمر وكأنه مضيعة للوقت ، خاصة لأولئك الذين يأتون من لغات ذات كتابة فضفاضة. ومع ذلك ، فقد ساعدوني في اللحاق بأكثر من خطأ واحد قبل مرحلة CI ، - عاجلاً أم آجلاً - يفهم أي مطور أنها مطلوبة ؛ ليس فقط كأداة توجيه للكتابة شبه الصارمة ، ولكن أيضًا كأداة مساعدة كبيرة في توثيق الكود.


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


 defs is_forty_two(n: integer) :: boolean do n == 42 end 

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


حسنًا ، لنبدأ.


الخطوة 1. دراسة الوضع.


بادئ ذي بدء ، سوف نفهم نوع AST الذي سيتلقاه الماكرو لدينا كوسيطات.


 defmodule CustomSpec do defmacro defs(args, do: block) do IO.inspect(args) :ok end end defmodule CustomSpec.Test do import CustomSpec defs is_forty_two(n: integer) :: boolean do n == 42 end end 

هنا يقوم المنسق بالتمرد وإضافة أقواس وتنسيق الكود بداخلها حتى تتدفق الدموع من العينين. فطم منه. تغيير .formatter.exs التكوين .formatter.exs مثل هذا:


 [ inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"], export: [locals_without_parens: [defs: 2]] ] 

دعنا نعود إلى defs/2 ونرى ما defs/2 عليه defs/2 . تجدر الإشارة إلى أن IO.inspect/2 لدينا IO.inspect/2 في مرحلة التجميع (إذا كنت لا تفهم السبب ، لست بحاجة إلى اللعب مع وحدات الماكرو حتى الآن ، اقرأ كتاب Chris McCord's Metaprogramming Elixir الرائع ). حتى لا يقسم المترجم ، نرجع :ok (يجب على وحدات الماكرو إرجاع AST الصحيح). لذلك:


 {:"::", [line: 7], [ {:is_forty_two, [line: 7], [[n: {:integer, [line: 7], nil}]]}, {:boolean, [line: 7], nil} ]} 

هاه. يعتبر المحلل اللغوي أن الشيء الرئيسي هنا هو عامل التشغيل :: ، الذي يقوم بلصق تعريف الوظيفة ونوع الإرجاع. يحتوي تعريف الوظيفة أيضًا على قائمة المعلمات في شكل Keyword ، "اسم المعلمة → النوع".


الخطوة 2. فشل بسرعة.


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


 defmacro defs({:"::", _, [{fun, _, [args_spec]}, {ret_spec, _, nil}]}, do: block) do 

حسنا ، لقد حان الوقت لبدء التنفيذ.


الخطوة 3. توليد المواصفات والإعلانات وظيفة.


 defmodule CustomSpec do defmacro defs({:"::", _, [{fun, _, [args_spec]}, {ret_spec, _, nil}]}, do: block) do #     args = for {arg, _spec} <- args_spec, do: Macro.var(arg, nil) #    args_spec = for {_arg, spec} <- args_spec, do: Macro.var(spec, nil) quote do @spec unquote(fun)(unquote_splicing(args_spec)) :: unquote(ret_spec) def unquote(fun)(unquote_splicing(args)) do unquote(block) end end end end 

كل شيء هنا شفاف لدرجة أنه لا يوجد شيء حتى يمكن التعليق عليه.


حان الوقت لمعرفة ما CustomSpec.Test.is_forty_two(42) إليه الدعوة إلى CustomSpec.Test.is_forty_two(42) إلى:


 iex> CustomSpec.Test.is_forty_two 42 #⇒ true iex> CustomSpec.Test.is_forty_two 43 #⇒ false 

حسنا ، إنه يعمل.


الخطوة 4. هل هذا كل شيء؟


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


من حيث المبدأ ، لا يزال بإمكانك مفاجأة الزملاء بشيء من هذا القبيل:


 defmodule CustomSpec do defmacro __using__(_) do import Kernel, except: [def: 2] import CustomSpec defmacro def(args, do: block) do defs(args, do: block) end end ... end 

(ستظل هناك حاجة إلى تصحيح defs/2 ، Kernel.def بدلاً من def ) ، لكنني أوصي بشدة بعدم القيام بذلك.


شكرا لاهتمامكم ، الصحة الكلية!

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


All Articles