Spesifikasi dialyzer: jalan Jedi

Ada dua jenis pengembang yang menggunakan erlang dan elixir: mereka yang menulis spesifikasi untuk Dialyzer, dan mereka yang belum. Pada awalnya sepertinya semua itu buang-buang waktu, terutama bagi mereka yang datang dari bahasa dengan pengetikan yang longgar. Namun, mereka membantu saya menangkap lebih dari satu kesalahan sebelum tahap CI, dan - cepat atau lambat - setiap pengembang mengerti bahwa mereka dibutuhkan; tidak hanya sebagai alat panduan untuk pengetikan semi-ketat, tetapi juga sebagai bantuan besar dalam mendokumentasikan kode.


Tetapi, seperti yang selalu terjadi di dunia kita yang kejam ini, dalam segala hal baik, Anda tidak dapat melakukannya tanpa sendok. Intinya, arahan @spec menduplikasi kode deklarasi fungsi. Di bawah ini saya akan memberi tahu Anda bagaimana dua puluh baris kode akan membantu menggabungkan spesifikasi dan deklarasi fungsi dalam desain formulir


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

Seperti yang Anda tahu, di elixir tidak ada yang lain selain makro. Bahkan Kernel.defmacro/2 adalah . Oleh karena itu, yang perlu kita lakukan adalah mendefinisikan makro kita sendiri, yang dari konstruksi di atas akan membuat pernyataan spesifikasi dan fungsi.


Baiklah, mari kita mulai.


Langkah 1. Pelajari situasinya.


Untuk memulainya, kita akan mengerti AST macam apa yang akan diterima makro sebagai argumen.


 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 

Di sini formatter akan memberontak , menambahkan tanda kurung dan memformat kode di dalamnya sehingga air mata mengalir dari mata. Singkirkan dia dari ini. Ubah .formatter.exs konfigurasi .formatter.exs seperti ini:


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

Mari kita kembali ke domba-domba kita dan melihat defs/2 apa yang ada di sana. Perlu dicatat bahwa IO.inspect/2 bekerja pada tahap kompilasi (jika Anda tidak mengerti mengapa, Anda belum perlu bermain dengan makro, baca buku Metaprogramming Elixir Chris McCord yang brilian). Agar kompiler tidak bersumpah, kami kembali :ok (makro harus mengembalikan AST yang benar). Jadi:


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

Ya Parser menganggap bahwa hal utama di sini adalah operator :: , yang menempelkan definisi fungsi dan tipe pengembalian. Definisi fungsi juga berisi daftar parameter dalam bentuk Keyword , β€œnama parameter β†’ tipe”.


Langkah 2. Gagal cepat.


Karena kita sejauh ini memutuskan untuk hanya mendukung sintaks panggilan seperti itu, kita perlu menulis ulang definisi makro defs sehingga jika, misalnya, tipe pengembalian tidak ditentukan, kompiler langsung bersumpah.


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

Nah, saatnya untuk memulai implementasi.


Langkah 3. Hasilkan spesifikasi dan deklarasi fungsi.


 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 

Semuanya di sini sangat transparan sehingga tidak ada yang bisa dikomentari.


Saatnya untuk melihat apa panggilan ke CustomSpec.Test.is_forty_two(42) akan mengarah ke:


 iex> CustomSpec.Test.is_forty_two 42 #β‡’ true iex> CustomSpec.Test.is_forty_two 43 #β‡’ false 

Ya berhasil.


Langkah 4. Apakah itu semua?


Tidak, tentu saja Dalam kehidupan nyata, Anda harus menangani dengan benar panggilan yang tidak valid, definisi header untuk fungsi dengan beberapa parameter default berbeda, dengan hati-hati mengumpulkan spek, dengan nama variabel, memastikan bahwa semua nama argumen berbeda, dan banyak lagi. Tetapi sebagai bukti kinerja akan dilakukan.


Pada prinsipnya, Anda masih bisa mengejutkan kolega dengan sesuatu seperti ini:


 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 

(Masih ada defs/2 perlu diperbaiki, menghasilkan Kernel.def alih-alih def ), tapi saya akan sangat menyarankan untuk tidak melakukan ini.


Terima kasih atas perhatian Anda, kesehatan makro!

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


All Articles