هناك نوعان من المطورين الذين يستخدمون 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
) ، لكنني أوصي بشدة بعدم القيام بذلك.
شكرا لاهتمامكم ، الصحة الكلية!