Motif

Saya mulai bekerja dengan Elixir beberapa bulan yang lalu tetapi tidak pernah sepenuhnya mempelajari seluk-beluk bahasa tersebut. Elixir adalah bahasa terbaru yang berjalan pada BEAM, VM yang sama yang menjalankan Erlang. Jadi ia mewarisi semua properti yang menjadikan Erlang hebat seperti toleransi kesalahan, ketersediaan tinggi, dan komputasi terdistribusi. Satu-satunya hambatan masuk ke Erlang bagi sebagian besar pengembang adalah ekosistem dan sintaksis bahasanya. Elixir mengatasi masalah ini dengan sangat elegan dengan sintaksis modern dan rangkaian alat pengembangan yang lengkap.

Dalam proses belajar saya, saya menyadari menulis kompiler/penerjemah untuk beberapa bahasa sederhana akan menjadi cara terbaik untuk mempelajari dan menerapkan sebagian besar konstruksi yang ditawarkan Elixir. Saya menemukan "artikel" Peter Norvig tentang menulis penerjemah Lisp dengan Python sebagai titik awal yang sangat bagus. Lisp adalah bahasa yang sangat sederhana karena mengikuti struktur yang koheren. Namun terdapat banyak sekali varian Lisp, Skema menjadi salah satu yang paling sederhana, memiliki konstruksi yang sangat sedikit.

Jika Anda baru mengenal Elixir dan membaca artikel ini, Anda akan melihat bagaimana sebagian besar konstruksi di Elixir seperti Pencocokan Pola, Pemipaan, dan pemrosesan Daftar membuat hidup Anda lebih mudah sebagai seorang programmer.

Anda dapat melihat "kode sumber" dan "dokumentasi" untuk referensi.

Struktur Lisp (Varian Skema)

Di Lisp, setiap pernyataan dimodelkan sebagai daftar dengan 2 atau 3 elemen. Elemen pertama dapat berupa salah satu dari berikut ini: konstruksi bahasa, operator, atau fungsi yang ditentukan pengguna. Parameter kedua dan ketiga selalu merupakan argumen untuk elemen pertama dan parameter ini sendiri dapat berupa daftar sehingga menciptakan struktur rekursif.

Misalnya, mari kita cari maksimal dua angka dalam bahasa seperti C dan Skema.

C

if( x > y) {
 max = x; 
}
else {
 max = y;
}

Skema

(begin (if (> x y) (set! max x) (set! max y)) 

C memiliki kumpulan konstruksi yang lebih besar dengan hal-hal seperti terminator garis, berbagai jenis tanda kurung, dll, sedangkan sintaksis Skema jauh lebih seragam.

Dalam Skema semuanya adalah ekspresi, angka dan simbol disebut ekspresi atom, yang lainnya adalah ekspresi daftar.

Proses Penerjemah

Untuk menulis penerjemah untuk bahasa apa pun ada dua langkah utama yang diikuti: Parsing dan Evaluasi.

Penguraian

Pada tahap parsing program sumber dipecah menjadi daftar kata kunci setelah itu Pohon Sintaks Abstrak (AST) dari program tersebut dibuat. AST sangat mencerminkan struktur bahasa yang disarangkan dan dapat dimasukkan ke langkah evaluasi sebagai masukan.

Evaluasi

Pada langkah ini AST diproses sesuai dengan kaidah semantik bahasa. Misalnya operator > berarti mencari yang lebih besar dari dua node anak di AST.

Implementasi Parser di Elixir

Parser terdiri dari dua bagian, tokenizer memecah kode menjadi kata kunci dan pembuat AST.

  • Tokenizer

Dalam contoh yang diberikan di bawah ini, setiap Ekspresi Skema bersarang atau tidak bersarang dipisahkan oleh ( ) .

(begin (if (> x y) (set! max x) (set! max y))

Kita harus memberi token pada program sumber paling lambat ( ) jadi ayo lakukan itu.

Ada banyak hal yang terjadi di sini jadi mari kita buka paketnya: def adalah makro Elixir untuk mendeklarasikan fungsi bernama. Makro adalah konstruksi yang sangat kuat yang digunakan untuk metaprogramming di Elixir yang tidak akan dibahas dalam artikel ini. Fungsi dalam Elixir diidentifikasi berdasarkan nama dan aritasnya. Arity suatu fungsi adalah jumlah argumen yang dibutuhkan. Fungsi tokenize mengambil satu argumen str sehingga tokenize diidentifikasi sebagai tokenize/1 .

Elixir juga menyediakan fungsi penanganan string yang berguna dalam modul String. Modul di Elixir adalah sekelompok fungsi. Sebuah modul dapat didefinisikan menggunakan makro defmodule. Nanti kita akan menambahkan lebih banyak fungsi pada langkah penguraian dan mengelompokkan fungsi-fungsi tersebut dalam modul Parse.

Sekarang untuk langkah tokenisasi kita harus memastikan ada minimal satu spasi di antara kata kunci sehingga kita memberi jarak pada tanda kurung. |> disebut operator pipa dan cara kerjanya mirip dengan pipa Unix. Output dari fungsi pertama disalurkan sebagai argumen pertama ke fungsi berikutnya. Jadi str diteruskan sebagai argumen pertama ke String.replace/3 . Akhirnya String.split/2 mendapatkan string yang diberi spasi, String.split/1 memiliki argumen default kedua yaitu spasi ' ' dan mengembalikan daftar String. List adalah salah satu tipe yang dapat dihitung dalam Elixir, beberapa di antaranya adalah Tuple dan Map . Jadi untuk contoh kode di atas, output dari tokenize/1 adalah

[ "(", "begin", "(", "if", "(", ">", "x", "y", ")", "(", "set!", "max", "x", ")", "(", "set!", "max", "y", ")", ")" ]
  • Pembuat AST

Sebelum kita mendalami langkah ini, kita perlu mengetahui lebih banyak tentang Daftar, pencocokan pola, dan rekursi di Elixir.

Daftar Pemahaman

Daftar di Elixir dapat ditentukan dengan mengapit nilai yang dipisahkan koma dalam tanda kurung siku.

list = [ 1, 2, 3]
[1, 2, 3]
length(list)
3

Secara internal di Elixir, daftar direpresentasikan sebagai daftar tertaut. Ini memiliki beberapa efek halus seperti sekarang menemukan panjang daftar adalah operasi linier.

Kita dapat menggabungkan dua daftar menggunakan operator ++

[4] ++ list
[4, 1, 2, 3]
list ++ [4]
[1, 2, 3, 4]

Mempersiapkan daftar terjadi dalam waktu yang konstan sementara menambahkan ke daftar akan membutuhkan waktu linier.

Daftar di Elixir dapat dibagi menjadi kepala dan ekor mirip dengan daftar tertaut menggunakan operator |.

[head | tail] = list
head
1
tail
[2, 3]

head akan memiliki 1 dan tail akan menjadi daftar 2 elemen [2, 3]

Elixir menyediakan fungsi pembantu hd/1 dan tl/1 yang menemukan kepala dan ekor dari daftar yang disediakan sebagai argumen.

hd(list)
1
tl(list)
[2, 3]

Elixir juga menyediakan modul List khusus yang memiliki banyak fungsi untuk memanipulasi daftar.

List.first(list)
1
List.last(list)
3

Atom dalam Elixir

Atom adalah suatu konstanta yang namanya mewakili nilainya. Mereka dapat ditentukan dengan menambahkan titik dua : pada sebuah nama. Nilai boolean true dan false sebenarnya adalah atom Elixir. Elixir menyediakan fungsi pembantu untuk memeriksa atom.

my_atom = :john
is_atom(my_atom)
true
is_atom(false)
true

Pencocokan Pola Dijelaskan

Dalam Elixir = operator adalah operator pencocokan dan membandingkan nilai sisi kiri dan kanan dan jika salah satu sisi tidak cocok maka akan terjadi kesalahan.

[1, 2, 3] = list
[1, 2, 3]
[1, 2, 5] = list
** (MatchError) no match of right hand side value: [1, 2, 3]

Ini mungkin terlihat mirip dengan operator == tetapi operator tersebut hanya melakukan perbandingan. Operator pencocokan = di sini melakukan perbandingan sekaligus mengikat suatu nilai ke suatu variabel tetapi pengikatan variabel hanya dapat dilakukan di sebelah kiri operator pencocokan. Hal ini menjadi kasus penggunaan yang sangat menarik dalam mendestrukturisasi tipe kompleks seperti List atau Tuple .

[first, mid, last ] = list
[1, 2, 3]
[first, mid, last]
[1, 2, 3]
[1, 2, _] = list
[1, 2, 3]

Pada contoh ketiga elemen terakhir adalah garis bawah _ . Garis bawah adalah konstruksi khusus dalam Elixir yang akan cocok dengan apa pun dan tidak akan mengikat nilai pada suatu elemen. Ini biasanya digunakan dalam pencocokan pola di mana elemen an harus diabaikan.

Sekarang untuk memahami pencocokan pola, mari kita ambil contoh di mana kita menanyakan API untuk mengambil daftar tiga siswa dan menyimpannya dalam daftar yang disebut siswa. Berdasarkan elemen daftar kita harus melakukan tugas tertentu.

students = fetchCoolKids()
doCoolStuff(students)
def doCoolStuff(["Stu", "Alan", "_"]) do
   IO.puts "We don't need a third person."
end
def doCoolStuff(["Doug", _, _]) do
   IO.puts "I don't need nobody."
end
# Scenario 1
students = ["Doug", "Alan", "Mark"]
I don't need nobody
#Scenario 2
students = ["Stu", "Alan", "Mark"]
We don't need a third person
#Scenario 3
students = ["Mark", "Stu", "Alan"]
(FunctionClauseError) no function clause matching in doCoolStuff/1

Jenis Definisi

Skema memiliki beberapa tipe objek, ini adalah bagaimana kita akan merepresentasikannya di Elixir

  • Simbol -› Diimplementasikan sebagai Atom . begin => :begin
  • Atom -› Diimplementasikan sebagai Atom atau Number .
  • Nomor -› Diimplementasikan sebagai Float atau Integer .
  • Daftar -› Diimplementasikan sebagai List . (1, 2, 3) => [1, 2, 3]
  • Ekspresi -› diimplementasikan sebagai Atom atau List

Membuat AST

Di bawah ini adalah kode untuk membuat AST dengan beberapa komentar bermanfaat.

Saya telah menggunakan rekursi untuk menelusuri semua token karena tidak ada konsep while loop di Elixir karena kekekalan. parse/2 mengambil daftar token sebagai argumen pertama dan akumulator acc sebagai argumen kedua. Formulir [head | tail] telah digunakan untuk mewakili daftar.

Untuk memahami kode di atas dengan lebih baik mari kita ambil contoh kode cadel

(begin (define r 10) (* r r))

Bentuk token dari kode cadel di atas adalah seperti ini

["(", "begin", "(", "define", "r", "10", ")", "(", "*", "r", "r", ")", ")"]

Kami akan meneruskan daftar token di atas dan daftar akumulator kosong ke fungsi parse/2 kami. Pencocokan pola akan menangani fungsi spesifik yang akan dipanggil. Apa yang ingin kami capai adalah membuat daftar operator dan argumen setiap kali kami menemukan '(' dan ') . Dalam bentuk AST, node non-daun akan selalu berupa fungsi atau operator, dan node daun akan selalu berupa simbol (angka atau string).

Di bawah ini adalah output dari tahap parsing

[ :begin, [ :define, :r, 10 ], [ :*, :r, :r ] ]

Evaluasi AST di Elixir

Untuk mengevaluasi kode Lisp kita harus menelusuri AST dan mengevaluasi setiap pernyataan secara rekursif. Untuk mengevaluasi kode kita memerlukan Lingkungan, peta yang menyimpan konstruksi bahasa dan variabel yang ditentukan pengguna. Kunci dari peta ini adalah konstruksi Lisp seperti 'define', 'begin' dll dan nilainya adalah fungsi anonim yang melakukan tugas yang diperlukan. Kita akan menelusuri semua ini lebih jauh sebelum membahas lebih lanjut tentang evaluasi.

Peta

Map adalah tipe yang dapat dihitung dalam Elixir. Ini adalah pasangan nilai kunci, di mana tidak ada batasan pada jenis kuncinya. Peta dapat dibuat menggunakan sintaks %{} dan pasangan nilai kunci dapat direpresentasikan sebagai key => value .

id_map = %{"Name" => "John", "Age" => 45, "Citizen" => "USA"}
Map.get(id_map,"Name")
"John"
Map.put(id_map,"DateOfBirth","03-03-1973")
%{"Name" => "John",  "DateOfBirth" => "03-03-1973" "Age" => 45, "Citizen" => "USA"}

Fungsi Anonim

Kita sebelumnya telah melihat cara mendefinisikan fungsi bernama menggunakan makro def. Kita juga dapat mendefinisikan fungsi tanpa nama yang disebut fungsi Anonim di Elixir dan fungsi lambda di beberapa bahasa lain.

multiply = fn (a, b) -> a * b end
multiply.(3,4)
12
toss = fn "heads" -> "England won the toss."
          "tails" -> "Australia won the toss."
               _  -> "Invalid toss."
       end
toss.("heads")
England won the toss

Kami mendefinisikan fungsi anonim menggunakan kata kunci fn dan operator ->. Fungsi-fungsi ini dapat dipanggil dengan menggunakan operator . dan mereka juga dapat memanggil banyak badan menggunakan pencocokan pola.

Ada satu cara lagi untuk merepresentasikan fungsi anonim di Elixir.

multiply = &( &1 * &2 )

Jenis definisi fungsi ini tidak boleh memiliki parameter bernama atau beberapa badan fungsi. & disebut operator penangkapan. &1 dan &2 adalah argumen pertama dan kedua yang analog dengan a dan b dalam definisi fn.

Konstruksi skema diimplementasikan di Lispex

  • Definisi : (define symbol exp)
  • Tugas : (set! symbol exp)
  • Referensi variabel: symbol
  • Literal Konstan: number
  • Bersyarat: ( if test conseq alt)
  • Prosedur: (proc args…)

Prosedur adalah apa pun selain jika, tentukan, dan tetapkan! . Contohnya adalah semua operator aritmatika dan logika, fungsi matematika, operasi daftar seperti car, cdr, cons .

Peta lingkungan

Konstruksi skema di atas telah diimplementasikan melalui peta di Elixir, inti dari peta tersebut diberikan di bawah ini.

Peta Lingkungan ini didefinisikan dalam modul Env. Modul ini berisi fungsi untuk memasukkan dan mengambil nilai dari peta.

Fungsi get/2 tampaknya agak rumit. Saya akan menguraikannya untuk pengertian Anda. Mirip dengan bahasa imperatif, Skema memiliki cakupan lokal dan global. Cakupan global adalah kumpulan pertama ( ) . Cakupan lokal dimulai dengan ( ) baru di dalamnya. Ini dapat disarangkan untuk membuat sejumlah cakupan, yang biasa disebut sebagai cakupan induk dan anak.

Jadi ketika mengevaluasi kode Skema, untuk setiap contoh kita menemukan ( ) baru, kita membuat lingkungan baru, merujuk lingkungan induk dengan kunci:outer. Di get/2 kami mencari kunci secara rekursif hingga kami mencapai lingkungan global.

Pernyataan kasus dan Penjaga Fungsi

Pernyataan kasus memungkinkan kita mencocokkan pola suatu nilai dan mengeksekusi pernyataan yang sesuai dengan kecocokan tersebut.

case [1, 3, 5] do
   [_, 3, 5] -> 
             "Match at 1"
   [_, _, 5] ->
             "Match at 2"
    _ ->
        "No match found"
end

Penjaga fungsi merupakan tambahan pada pencocokan pola. Pencocokan pola hanya terjadi jika kondisi penjaga terpenuhi.

Penjaga dapat digunakan dalam fungsi bernama dan anonim serta pernyataan kasus. Penjaga didefinisikan dengan menggunakan kata kunci when diikuti dengan ekspresi.

def parse(x) when is_list(x) do
    IO.puts "List"
end

Jenis pemeriksaan seperti is_list/1 , is_atom/1 dll dapat digunakan dengan penjaga selain sejumlah operator lainnya.

Evaluasi AST

Kami mengevaluasi AST secara rekursif, mengevaluasi setiap konstruksi dan argumennya masing-masing.

Ketika ekspresi skemanya adalah:

  • bukan daftar dan merupakan atom, ambil nilai atom tersebut dari lingkungan.
  • bukan daftar dan merupakan nomor, kembalikan nomor itu.
  • daftar dan kepalanya adalah :if , evaluasi elemen kedua, jika benar evaluasi elemen ketiga, jika salah evaluasi ekornya.
  • list dan headnya adalah :define , maka elemen kedua adalah simbolnya. Evaluasi ekornya dan masukkan nilainya ke dalam peta Lingkungan dengan simbol sebagai kuncinya.
  • daftar dan kepalanya adalah :set!, maka perilakunya mirip dengan :define kecuali variabelnya harus ada di peta lingkungan.
  • daftar dan kepala adalah apa pun selain :if, :set!, atau :define maka kepala adalah prosedurnya, dan ekornya adalah daftar argumen untuk prosedur itu. Evaluasi setiap elemen di bagian ekor, dapatkan nilai prosedur dari peta lingkungan.

Membuat REPL

Memanggil fungsi interpret secara manual akan membosankan dan kita juga akan memetakan status lingkungan, jadi membuat REPL adalah cara terbaik untuk mencerminkan interpreter Skema.

Berikut ini contoh cara kerja Lispex REPL

iex> Lispex.repl
lispex> (begin (define a 4) (* a 5))
20
lispex> (if a<3 (* a 4) (* a 6))
24

Semua kode yang diberikan di sini disertakan dalam empat modul yang disebut Lispex , Parse , Env dan Eval .

Kesimpulan

Saya mencoba memberi Anda pengenalan praktis tentang Elixir karena membantu dalam mencerna konsep-konsep ini dengan mudah. Kode sumbernya juga memiliki banyak dokumentasi yang mungkin berguna jika Anda merasa beberapa fungsi agak samar.

Pada artikel saya berikutnya saya akan membahas tentang metaprogramming di Elixir dan Erlang OTP yang akan disertai dengan masalah praktisnya.

Referensi

https://hexdocs.pm/elixir

http://norvig.com/lispy.html