แรงจูงใจ
ฉันเริ่มทำงานกับ Elixir เมื่อสองสามเดือนก่อน แต่ไม่เคยเจาะลึกถึงความซับซ้อนของภาษาเลย Elixir เป็นภาษาที่ค่อนข้างใหม่ซึ่งทำงานบน BEAM ซึ่งเป็น VM เดียวกันกับที่รัน Erlang ดังนั้นจึงสืบทอดคุณสมบัติทั้งหมดที่ทำให้ Erlang ดีเยี่ยม เช่น ความทนทานต่อข้อผิดพลาด ความพร้อมใช้งานสูง และการประมวลผลแบบกระจาย อุปสรรคเดียวในการเข้าสู่ Erlang สำหรับนักพัฒนาส่วนใหญ่คือระบบนิเวศและไวยากรณ์ของภาษา Elixir จัดการกับปัญหาเหล่านี้อย่างสวยงามด้วยไวยากรณ์ที่ทันสมัยและกลุ่มเครื่องมือการพัฒนาที่สมบูรณ์
ในกระบวนการเรียนรู้ของฉัน ฉันพบว่าการเขียนคอมไพเลอร์/ล่ามสำหรับภาษาง่ายๆ บางอย่างจะเป็นวิธีที่ดีที่สุดในการเรียนรู้และประยุกต์ใช้โครงสร้างส่วนใหญ่ที่ Elixir มีให้ ฉันพบว่า "บทความ" ของ Peter Norvig เกี่ยวกับการเขียนล่าม Lisp ใน Python นั้นเป็นจุดเริ่มต้นที่ยอดเยี่ยมมาก Lisp เป็นภาษาที่ง่ายมากเนื่องจากมีโครงสร้างที่สอดคล้องกัน แต่มี Lisp หลากหลายรูปแบบ Scheme เป็นหนึ่งในรูปแบบที่ง่ายที่สุดและมีโครงสร้างน้อยมาก
หากคุณยังใหม่กับ Elixir ที่ได้อ่านบทความนี้ คุณจะเห็นว่าโครงสร้างส่วนใหญ่ใน Elixir เช่น Pattern Matching, Piping และ List Processing ทำให้ชีวิตของคุณง่ายขึ้นในฐานะโปรแกรมเมอร์ได้อย่างไร
คุณสามารถตรวจสอบ ซอร์สโค้ด และ เอกสารประกอบ เพื่อเป็นข้อมูลอ้างอิง
โครงสร้าง Lisp (Scheme Variant)
ใน Lisp ทุกคำสั่งจะถูกจำลองเป็นรายการที่มีองค์ประกอบ 2 หรือ 3 รายการ องค์ประกอบแรกอาจเป็นอย่างใดอย่างหนึ่งต่อไปนี้: โครงสร้างภาษา ตัวดำเนินการ หรือฟังก์ชันที่ผู้ใช้กำหนด พารามิเตอร์ตัวที่สองและสามจะเป็นอาร์กิวเมนต์ขององค์ประกอบแรกเสมอ และพารามิเตอร์เหล่านี้อาจเป็นรายการในตัวมันเอง ดังนั้น จึงสร้างโครงสร้างแบบเรียกซ้ำ
ตัวอย่างเช่น ลองค้นหาตัวเลขสูงสุดสองตัวในภาษาเช่น C และในภาษา Scheme
C
if( x > y) { max = x; } else { max = y; }
โครงการ
(begin (if (> x y) (set! max x) (set! max y))
C มีชุดโครงสร้างที่ใหญ่กว่าด้วยสิ่งต่างๆ เช่น ตัวยุติบรรทัด วงเล็บประเภทต่างๆ เป็นต้น ในขณะที่ไวยากรณ์ของ Scheme มีความสม่ำเสมอมากกว่ามาก
ใน Scheme ทุกอย่างคือนิพจน์ ตัวเลขและสัญลักษณ์เรียกว่านิพจน์อะตอมมิก ส่วนอย่างอื่นเป็นนิพจน์รายการ
กระบวนการล่าม
การเขียนล่ามสำหรับภาษาใดๆ มีสองขั้นตอนหลักดังนี้: การแยกวิเคราะห์และการประเมินผล
การแยกวิเคราะห์
ในขั้นตอนการแยกวิเคราะห์ โปรแกรมต้นฉบับจะถูกแบ่งออกเป็นรายการคำหลัก หลังจากนั้นจึงสร้าง Abstract Syntax Tree (AST) ของโปรแกรมนั้นขึ้นมา AST สะท้อนโครงสร้างที่ซ้อนกันของภาษาอย่างใกล้ชิด และสามารถป้อนเข้าสู่ขั้นตอนการประเมินเป็นอินพุตได้
การประเมินผล
ในขั้นตอนนี้ AST จะถูกประมวลผลตามกฎความหมายของภาษา ตัวอย่างเช่น โอเปอเรเตอร์ >
หมายถึงค้นหาค่าที่มากกว่าจากโหนดย่อยทั้งสองใน AST
การใช้งาน Parser ใน Elixir
parser ประกอบด้วยสองส่วน ส่วน tokenizer จะแบ่งโค้ดออกเป็นคำหลัก และตัวสร้าง AST
- โทเค็นไนเซอร์
ในตัวอย่างที่ให้ไว้ด้านล่าง ทุก Scheme Expression ที่ซ้อนกันหรือไม่ซ้อนกันจะถูกคั่นด้วย ( )
(begin (if (> x y) (set! max x) (set! max y))
เราต้องโทเค็นโปรแกรมต้นทางด้วย ( )
เรามาทำสิ่งนั้นกันเถอะ
มีอะไรเกิดขึ้นมากมายที่นี่ ดังนั้นเรามาแกะมันกันดีกว่า: def
เป็นมาโคร Elixir เพื่อประกาศฟังก์ชันที่มีชื่อ มาโครเป็นโครงสร้างที่ทรงพลังมากซึ่งใช้สำหรับการเขียนโปรแกรมเมตาใน Elixir ซึ่งไม่อยู่ในขอบเขตของบทความนี้ ฟังก์ชั่นใน Elixir จะถูกระบุด้วยชื่อและความสมบูรณ์ Arity ของฟังก์ชันคือจำนวนอาร์กิวเมนต์ที่ใช้ ฟังก์ชัน tokenize
รับอาร์กิวเมนต์เดียว str
ดังนั้นโทเค็นจึงถูกระบุเป็น tokenize/1
Elixir ยังมีฟังก์ชันการจัดการสตริงที่เป็นประโยชน์ในโมดูล String
โมดูลใน Elixir คือกลุ่มของฟังก์ชัน โมดูลสามารถกำหนดได้โดยใช้แมโคร defmodule
หลังจากนั้นเราจะเพิ่มฟังก์ชันเพิ่มเติมในขั้นตอนการแยกวิเคราะห์ของเราและจัดกลุ่มฟังก์ชันเหล่านั้นในโมดูล Parse
สำหรับขั้นตอนการสร้างโทเค็น เราต้องแน่ใจว่ามีช่องว่างอย่างน้อยหนึ่งช่องระหว่างคีย์เวิร์ด ดังนั้นเราจึงเว้นวรรคในวงเล็บ |>
เรียกว่าตัวดำเนินการไปป์ และทำงานคล้ายกับไปป์ Unix ผลลัพธ์ของฟังก์ชันแรกจะถูกส่งไปเป็นอาร์กิวเมนต์แรกของฟังก์ชันถัดไป ดังนั้น str
จึงถูกส่งผ่านเป็นอาร์กิวเมนต์แรกไปยัง String.replace/3
ในที่สุด String.split/2
ได้รับสตริงที่เว้นระยะห่าง String.split/1
มีอาร์กิวเมนต์เริ่มต้นที่สองซึ่งเป็นช่องว่าง ' '
และส่งคืนรายการสตริง List
เป็นหนึ่งในประเภทที่นับได้ใน Elixir และอีกประเภทหนึ่งคือ Tuple
และ Map
ดังนั้นสำหรับโค้ดตัวอย่างข้างต้น ผลลัพธ์ของ tokenize/1
จะเป็น
[ "(", "begin", "(", "if", "(", ">", "x", "y", ")", "(", "set!", "max", "x", ")", "(", "set!", "max", "y", ")", ")" ]
- ตัวสร้าง AST
ก่อนที่เราจะเจาะลึกขั้นตอนนี้ เราจำเป็นต้องทราบข้อมูลเพิ่มเติมเกี่ยวกับ รายการ การจับคู่รูปแบบ และการเรียกซ้ำ ใน Elixir
รายการทำความเข้าใจ
รายการใน Elixir สามารถกำหนดได้โดยใส่ค่าที่คั่นด้วยเครื่องหมายจุลภาคในวงเล็บเหลี่ยม
list = [ 1, 2, 3] [1, 2, 3] length(list) 3
ภายใน Elixir รายการจะแสดงเป็นรายการที่เชื่อมโยง สิ่งนี้มีผลกระทบเล็กน้อยบางอย่าง เช่น การค้นหาความยาวของรายการเป็นแบบเชิงเส้นในขณะนี้
เราสามารถเชื่อมสองรายการเข้าด้วยกันโดยใช้ตัวดำเนินการ ++
[4] ++ list [4, 1, 2, 3] list ++ [4] [1, 2, 3, 4]
การเติมหน้ารายการจะเกิดขึ้นในเวลาคงที่ ในขณะที่การต่อท้ายรายการจะต้องใช้เวลาเป็นเส้นตรง
รายการใน Elixir สามารถแบ่งออกเป็นส่วนหัวและส่วนท้ายได้คล้ายกับรายการที่เชื่อมโยงโดยใช้ตัวดำเนินการ |
[head | tail] = list head 1 tail [2, 3]
head
จะมี 1
และ tail
จะเป็นรายการของ 2 องค์ประกอบ [2, 3]
Elixir มีฟังก์ชันตัวช่วย hd/1
และ tl/1
ซึ่งจะค้นหาส่วนหัวและส่วนท้ายของรายการที่ระบุเป็นอาร์กิวเมนต์
hd(list) 1 tl(list) [2, 3]
Elixir ยังมีโมดูล List
เฉพาะซึ่งมีฟังก์ชันมากมายในการจัดการกับรายการต่างๆ
List.first(list) 1 List.last(list) 3
อะตอมในน้ำอมฤต
อะตอมคือค่าคงที่ซึ่งชื่อแสดงถึงคุณค่าของมัน สามารถกำหนดได้โดยเติมโคลอน :
นำหน้าชื่อ ค่าบูลีน true
และ false
แท้จริงแล้วคืออะตอมของ Elixir Elixir มีฟังก์ชันตัวช่วยในการตรวจสอบอะตอม
my_atom = :john is_atom(my_atom) true is_atom(false) true
การจับคู่รูปแบบไม่ชัดเจน
ใน Elixir =
โอเปอเรเตอร์คือโอเปอเรเตอร์การจับคู่และเปรียบเทียบทั้งค่าทางด้านซ้ายและด้านขวามือ และหากด้านใดด้านหนึ่งไม่ตรงกันก็จะเกิดข้อผิดพลาด
[1, 2, 3] = list [1, 2, 3] [1, 2, 5] = list ** (MatchError) no match of right hand side value: [1, 2, 3]
ซึ่งอาจดูคล้ายกับตัวดำเนินการ ==
แต่ตัวดำเนินการดังกล่าวจะทำการเปรียบเทียบเท่านั้น ตัวดำเนินการจับคู่ =
ที่นี่จะทำการเปรียบเทียบและผูกค่ากับตัวแปร แต่การเชื่อมโยงตัวแปรจะเกิดขึ้นที่ด้านซ้ายของตัวดำเนินการจับคู่เท่านั้น สิ่งนี้ทำให้เกิดกรณีการใช้งานที่น่าสนใจมากในการลดโครงสร้างประเภทที่ซับซ้อน เช่น List
หรือ Tuple
[first, mid, last ] = list [1, 2, 3] [first, mid, last] [1, 2, 3] [1, 2, _] = list [1, 2, 3]
ในตัวอย่างที่สาม องค์ประกอบสุดท้ายคือขีดล่าง _
ขีดล่างเป็นโครงสร้างพิเศษใน Elixir ซึ่งจะจับคู่กับสิ่งใดๆ และจะไม่ผูกค่ากับองค์ประกอบ โดยทั่วไปจะใช้ในการจับคู่รูปแบบโดยที่องค์ประกอบต้องถูกละเว้น
ตอนนี้เพื่อทำความเข้าใจการจับคู่รูปแบบ มาดูตัวอย่างที่เราสืบค้น API เพื่อดึงรายชื่อนักเรียนสามคนและจัดเก็บไว้ในรายการที่เรียกว่านักเรียน ตามองค์ประกอบของรายการเราต้องทำงานเฉพาะเจาะจง
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
คำจำกัดความประเภท
Scheme มีออบเจ็กต์อยู่ไม่กี่ประเภท นี่คือวิธีที่เราจะนำเสนอใน Elixir
- สัญลักษณ์ -> นำไปใช้เป็น
Atom
begin => :begin
- Atom -› นำไปใช้เป็น
Atom
หรือNumber
- หมายเลข -› ดำเนินการเป็น
Float
หรือInteger
- รายการ -> นำไปใช้เป็น
List
(1, 2, 3) => [1, 2, 3]
- นิพจน์ -> นำไปใช้เป็น
Atom
หรือList
การสร้าง AST
ด้านล่างนี้คือโค้ดสำหรับสร้าง AST พร้อมความคิดเห็นที่เป็นประโยชน์
ฉันใช้การเรียกซ้ำเพื่อดูโทเค็นทั้งหมดเนื่องจากไม่มีแนวคิดเรื่อง while loop ใน Elixir เนื่องจากความไม่เปลี่ยนรูป parse/2
รับรายการโทเค็นเป็นอาร์กิวเมนต์แรกและตัวสะสม acc
เป็นอาร์กิวเมนต์ที่สอง มีการใช้แบบฟอร์ม [head | tail]
เพื่อแสดงรายการ
เพื่อให้เข้าใจโค้ดข้างต้นได้ดีขึ้น ลองใช้โค้ดเสียงกระเพื่อมตัวอย่าง
(begin (define r 10) (* r r))
รูปแบบโทเค็นของรหัสเสียงกระเพื่อมข้างต้นจะเป็นเช่นนี้
["(", "begin", "(", "define", "r", "10", ")", "(", "*", "r", "r", ")", ")"]
เราจะส่งรายการโทเค็นด้านบนและรายการตัวสะสมว่างไปยังฟังก์ชัน parse/2
ของเรา การจับคู่รูปแบบจะดูแลฟังก์ชันเฉพาะที่จะเรียกใช้ สิ่งที่เราพยายามทำให้สำเร็จคือการสร้างรายการโอเปอเรเตอร์และอาร์กิวเมนต์ทุกครั้งที่เราพบ '('
และ ')
ในรูปแบบ AST โหนดที่ไม่ใช่ลีฟจะเป็นฟังก์ชันหรือตัวดำเนินการเสมอ และโหนดลีฟจะเป็นสัญลักษณ์ (ตัวเลขหรือสตริง)
ด้านล่างนี้คือผลลัพธ์ของขั้นตอนการแยกวิเคราะห์
[ :begin, [ :define, :r, 10 ], [ :*, :r, :r ] ]
การประเมิน AST ใน Elixir
ในการประเมินโค้ด Lisp เราต้องสำรวจผ่าน AST และประเมินแต่ละคำสั่งแบบวนซ้ำ ในการประเมินโค้ดเราจำเป็นต้องมีสภาพแวดล้อม ซึ่งเป็นแผนที่ที่เก็บโครงสร้างภาษาและตัวแปรที่ผู้ใช้กำหนด คีย์ของแผนที่นี้คือโครงสร้าง Lisp เช่น 'define', 'begin' ฯลฯ และค่าต่างๆ นั้นเป็นฟังก์ชันที่ไม่ระบุตัวตนซึ่งทำงานที่ต้องการ เราจะสำรวจทั้งหมดนี้เพิ่มเติมก่อนที่จะพูดคุยเพิ่มเติมเกี่ยวกับการประเมิน
แผนที่
Map
เป็นประเภทที่นับได้ใน Elixir เป็นคู่ของค่าคีย์ ซึ่งไม่มีข้อจำกัดเกี่ยวกับประเภทคีย์ สามารถสร้างแผนที่ได้โดยใช้ไวยากรณ์ %{}
และคู่ของค่าคีย์สามารถแสดงเป็น 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"}
ฟังก์ชันที่ไม่ระบุชื่อ
ก่อนหน้านี้เราได้เห็นวิธีการกำหนดฟังก์ชันที่มีชื่อโดยใช้มาโคร def
นอกจากนี้เรายังสามารถกำหนดฟังก์ชันที่ไม่มีชื่อเรียกว่าฟังก์ชัน Anonymous ในฟังก์ชัน Elixir และ lambda ในภาษาอื่นบางภาษาได้
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
เรากำหนดฟังก์ชันที่ไม่ระบุชื่อโดยใช้คำหลัก fn
และตัวดำเนินการ ->
ฟังก์ชันเหล่านี้สามารถเรียกใช้ได้โดยใช้ตัวดำเนินการ .
และยังสามารถเรียกใช้หลายเนื้อหาได้โดยใช้การจับคู่รูปแบบ
มีอีกวิธีหนึ่งในการแสดงฟังก์ชันที่ไม่ระบุชื่อใน Elixir
multiply = &( &1 * &2 )
คำจำกัดความฟังก์ชันประเภทนี้ไม่สามารถตั้งชื่อพารามิเตอร์หรือเนื้อหาของฟังก์ชันหลายรายการได้ &
เรียกว่าตัวดำเนินการจับภาพ &1
และ &2
เป็นอาร์กิวเมนต์ที่หนึ่งและที่สองที่คล้ายคลึงกับ a
และ b
ในคำจำกัดความ fn
โครงสร้างโครงการที่นำไปใช้ใน Lispex
- คำจำกัดความ :
(define symbol exp)
- งานที่ได้รับมอบหมาย :
(set! symbol exp)
- การอ้างอิงตัวแปร:
symbol
- ตัวอักษรคงที่:
number
- มีเงื่อนไข:
( if test conseq alt)
- ขั้นตอน:
(proc args…)
ขั้นตอนเป็นอย่างอื่นนอกเหนือจาก if,defense และ set! . ตัวอย่างได้แก่ ตัวดำเนินการทางคณิตศาสตร์และตรรกะ ฟังก์ชันทางคณิตศาสตร์ การดำเนินการรายการ เช่น car
, cdr
, cons
แผนที่สภาพแวดล้อม
โครงสร้างโครงร่างข้างต้นได้ถูกนำมาใช้ผ่านแผนที่ใน Elixir ส่วนสำคัญของแผนที่นั้นได้รับด้านล่าง
แมปสภาพแวดล้อมนี้ถูกกำหนดไว้ในโมดูล Env
โมดูลประกอบด้วยฟังก์ชันสำหรับการแทรกและดึงค่าจากแผนที่
ฟังก์ชัน get/2
ดูเหมือนจะซับซ้อนเล็กน้อย ฉันจะทำลายมันลงเพื่อความเข้าใจของคุณ เช่นเดียวกับภาษาที่จำเป็น Scheme มีขอบเขตระดับท้องถิ่นและระดับโลก ขอบเขตส่วนกลางคือชุดแรกของ ( )
ขอบเขตท้องถิ่นเริ่มต้นด้วย ( )
ใหม่อยู่ข้างใน ซึ่งสามารถซ้อนกันเพื่อสร้างขอบเขตจำนวนเท่าใดก็ได้ โดยทั่วไปเรียกว่าขอบเขตหลักและขอบเขตย่อย
ดังนั้น เมื่อประเมินโค้ด Scheme ทุกครั้งที่เราพบ ( )
ใหม่ เราจะสร้างสภาพแวดล้อมใหม่ โดยอ้างอิงสภาพแวดล้อมหลักด้วยคีย์ an:outer
ใน get/2
เราค้นหาคีย์ซ้ำๆ จนกระทั่งเข้าถึงสภาพแวดล้อมทั่วโลก
คำแถลงกรณีและตัวป้องกันฟังก์ชัน
คำสั่ง Case ช่วยให้เราสามารถจัดรูปแบบการจับคู่ค่าและดำเนินการคำสั่งที่สอดคล้องกับการจับคู่นั้น
case [1, 3, 5] do [_, 3, 5] -> "Match at 1" [_, _, 5] -> "Match at 2" _ -> "No match found" end
ตัวป้องกันฟังก์ชันเป็นส่วนเสริมของการจับคู่รูปแบบ การจับคู่รูปแบบจะเกิดขึ้นเฉพาะเมื่อตรงตามเงื่อนไขการป้องกันเท่านั้น
Guards สามารถใช้ในฟังก์ชันที่มีชื่อและไม่ระบุชื่อและคำสั่ง case Guards ถูกกำหนดโดยใช้คีย์เวิร์ด when
ตามด้วยนิพจน์
def parse(x) when is_list(x) do IO.puts "List" end
การตรวจสอบประเภท เช่น is_list/1
, is_atom/1
ฯลฯ สามารถใช้กับการ์ดได้ นอกเหนือจากโฮสต์ของโอเปอเรเตอร์อื่นๆ
การประเมิน AST
เราประเมิน AST ซ้ำ ๆ โดยประเมินแต่ละโครงสร้างและข้อโต้แย้งตามลำดับ
เมื่อนิพจน์โครงร่างคือ:
- ไม่ใช่รายการและเป็นอะตอม ให้ดึงค่าสำหรับอะตอมนั้นจากสภาพแวดล้อม
- ไม่ใช่รายการและเป็นตัวเลข ให้ส่งคืนหมายเลขนั้น
- รายการและส่วนหัวคือ
:if
ให้ประเมินองค์ประกอบที่สอง หากเป็นจริง ให้ประเมินองค์ประกอบที่สาม หากเป็นเท็จ ให้ประเมินส่วนท้าย - รายการและส่วนหัวคือ
:define
จากนั้นองค์ประกอบที่สองคือสัญลักษณ์ ประเมินส่วนท้ายและใส่ค่าลงในแผนที่สิ่งแวดล้อมโดยมีสัญลักษณ์เป็นกุญแจ - รายการและส่วนหัวคือ
:set!
ดังนั้นพฤติกรรมจะคล้ายกับ:define
ยกเว้นว่าตัวแปรจะต้องมีอยู่ในแผนผังสภาพแวดล้อม - รายการและส่วนหัวเป็นอย่างอื่นที่ไม่ใช่
:if
,:set!
หรือ:define
ดังนั้น head คือโพรซีเดอร์ และส่วนท้ายคือรายการอาร์กิวเมนต์สำหรับโพรซีเดอร์นั้น ประเมินแต่ละองค์ประกอบในส่วนท้าย รับค่าของกระบวนการจากแผนผังสภาพแวดล้อม
การสร้าง REPL
การเรียกใช้ฟังก์ชัน การตีความ ด้วยตนเองนั้นน่าเบื่อ และเรายังต้องระบุสถานะแผนที่สภาพแวดล้อมด้วย ดังนั้นการสร้าง REPL จึงเป็นวิธีที่ดีที่สุดในการสะท้อนล่าม Scheme
นี่คือตัวอย่างของ Lispex REPL ที่ใช้งานจริง
iex> Lispex.repl lispex> (begin (define a 4) (* a 5)) 20 lispex> (if a<3 (* a 4) (* a 6)) 24
รหัสทั้งหมดที่ให้ไว้ที่นี่รวมอยู่ในสี่โมดูลที่เรียกว่า Lispex
, Parse
, Env
และ Eval
บทสรุป
ฉันพยายามให้คำแนะนำที่เป็นประโยชน์แก่คุณเกี่ยวกับ Elixir เนื่องจากช่วยในการย่อยแนวคิดเหล่านี้ได้อย่างง่ายดาย ซอร์สโค้ดยังมีเอกสารจำนวนมากซึ่งอาจเป็นประโยชน์หากคุณพบว่าฟังก์ชันบางอย่างเป็นความลับเล็กน้อย
ในบทความถัดไป ฉันจะพูดถึงเกี่ยวกับการเขียนโปรแกรมเมตาใน Elixir และ Erlang OTP ซึ่งจะมาพร้อมกับปัญหาในทางปฏิบัติ