Elixir - Hướng dẫn nhanh

Elixir là một ngôn ngữ chức năng, năng động được thiết kế để xây dựng các ứng dụng có thể mở rộng và có thể bảo trì. Nó thúc đẩy Erlang VM, được biết đến với việc chạy các hệ thống có độ trễ thấp, phân tán và chịu lỗi, đồng thời cũng được sử dụng thành công trong phát triển web và miền phần mềm nhúng.

Elixir là một ngôn ngữ động, chức năng được xây dựng trên Erlang và Erlang VM. Erlang là một ngôn ngữ ban đầu được viết vào năm 1986 bởi Ericsson để giúp giải quyết các vấn đề về điện thoại như phân phối, khả năng chịu lỗi và đồng thời. Elixir, được viết bởi José Valim, mở rộng Erlang và cung cấp một cú pháp thân thiện hơn vào Erlang VM. Nó thực hiện điều này trong khi vẫn giữ hiệu suất ngang bằng với Erlang.

Đặc điểm của Elixir

Bây giờ chúng ta hãy thảo luận một vài tính năng quan trọng của Elixir -

  • Scalability - Tất cả mã Elixir chạy bên trong các quy trình nhẹ được cách ly và trao đổi thông tin qua tin nhắn.

  • Fault Tolerance- Elixir cung cấp trình giám sát mô tả cách khởi động lại các bộ phận trong hệ thống của bạn khi có sự cố, quay trở lại trạng thái ban đầu đã biết được đảm bảo hoạt động. Điều này đảm bảo ứng dụng / nền tảng của bạn không bao giờ bị lỗi.

  • Functional Programming - Lập trình chức năng thúc đẩy phong cách viết mã giúp các nhà phát triển viết mã ngắn, nhanh và dễ bảo trì.

  • Build tools- Elixir đi kèm với một bộ công cụ phát triển. Mix là một trong những công cụ giúp bạn dễ dàng tạo dự án, quản lý tác vụ, chạy thử nghiệm, v.v. Nó cũng có trình quản lý gói riêng - Hex.

  • Erlang Compatibility - Elixir chạy trên Erlang VM cho phép các nhà phát triển truy cập hoàn toàn vào hệ sinh thái của Erlang.

Để chạy Elixir, bạn cần thiết lập nó cục bộ trên hệ thống của mình.

Để cài đặt Elixir, trước tiên bạn sẽ yêu cầu Erlang. Trên một số nền tảng, các gói Elixir có Erlang trong đó.

Cài đặt Elixir

Bây giờ chúng ta hãy hiểu việc cài đặt Elixir trong các Hệ điều hành khác nhau.

cài đặt Windows

Để cài đặt Elixir trên windows, hãy tải xuống trình cài đặt từ https://repo.hex.pm/elixirwebsetup.exe và chỉ cần nhấp vào Nextđể tiến hành tất cả các bước. Bạn sẽ có nó trên hệ thống cục bộ của mình.

Nếu bạn gặp bất kỳ sự cố nào trong khi cài đặt nó, bạn có thể kiểm tra trang này để biết thêm thông tin.

Thiết lập Mac

Nếu bạn đã cài đặt Homebrew, hãy đảm bảo rằng đó là phiên bản mới nhất. Để cập nhật, hãy sử dụng lệnh sau:

brew update

Bây giờ, hãy cài đặt Elixir bằng lệnh dưới đây:

brew install elixir

Thiết lập Ubuntu / Debian

Các bước cài đặt Elixir trong thiết lập Ubuntu / Debian như sau:

Thêm repo Giải pháp Erlang -

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo 
dpkg -i erlang-solutions_1.0_all.deb 
sudo apt-get update

Cài đặt nền tảng Erlang / OTP và tất cả các ứng dụng của nó -

sudo apt-get install esl-erlang

Cài đặt Elixir -

sudo apt-get install elixir

Các phân phối Linux khác

Nếu bạn có bất kỳ bản phân phối Linux nào khác, vui lòng truy cập trang này để thiết lập thuốc tiên trên hệ thống cục bộ của bạn.

Kiểm tra thiết lập

Để kiểm tra thiết lập Elixir trên hệ thống của bạn, hãy mở thiết bị đầu cuối của bạn và nhập iex vào đó. Nó sẽ mở shell elixir tương tác như sau:

Erlang/OTP 19 [erts-8.0] [source-6dc93c1] [64-bit] 
[smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]  

Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help) 
iex(1)>

Elixir hiện đã được thiết lập thành công trên hệ thống của bạn.

Chúng ta sẽ bắt đầu với chương trình 'Hello World' theo thông lệ.

Để khởi động trình bao tương tác Elixir, hãy nhập lệnh sau.

iex

Sau khi trình bao bắt đầu, hãy sử dụng IO.putshàm để "đặt" chuỗi trên đầu ra bảng điều khiển. Nhập thông tin sau vào trình bao Elixir của bạn -

IO.puts "Hello world"

Trong hướng dẫn này, chúng tôi sẽ sử dụng chế độ tập lệnh Elixir, nơi chúng tôi sẽ giữ mã Elixir trong tệp có phần mở rộng .ex. Bây giờ chúng ta hãy giữ đoạn mã trên trongtest.extập tin. Trong bước tiếp theo, chúng tôi sẽ thực thi nó bằng cách sử dụngelixirc-

IO.puts "Hello world"

Bây giờ chúng ta hãy thử chạy chương trình trên như sau:

$elixirc test.ex

Chương trình trên tạo ra kết quả sau:

Hello World

Ở đây chúng tôi đang gọi một hàm IO.putsđể tạo một chuỗi cho bảng điều khiển của chúng tôi làm đầu ra. Hàm này cũng có thể được gọi theo cách chúng ta làm trong C, C ++, Java, v.v., cung cấp các đối số trong dấu ngoặc đơn theo sau tên hàm:

IO.puts("Hello world")

Bình luận

Nhận xét dòng đơn bắt đầu bằng ký hiệu '#'. Không có nhận xét nhiều dòng, nhưng bạn có thể xếp chồng nhiều nhận xét. Ví dụ -

#This is a comment in Elixir

Kết thúc dòng

Không có phần cuối dòng bắt buộc như ';' trong Elixir. Tuy nhiên, chúng ta có thể có nhiều câu lệnh trong cùng một dòng, sử dụng ';'. Ví dụ,

IO.puts("Hello"); IO.puts("World!")

Chương trình trên tạo ra kết quả sau:

Hello 
World!

Định danh

Các số nhận dạng như biến, tên hàm được sử dụng để xác định một biến, hàm, v.v. Trong Elixir, bạn có thể đặt tên cho các số nhận dạng của mình bắt đầu bằng bảng chữ cái viết thường với số, dấu gạch dưới và chữ hoa sau đó. Quy ước đặt tên này thường được gọi là solid_case. Ví dụ: sau đây là một số số nhận dạng hợp lệ trong Elixir:

var1       variable_2      one_M0r3_variable

Xin lưu ý rằng các biến cũng có thể được đặt tên bằng dấu gạch dưới ở đầu. Giá trị không được sử dụng phải được gán cho _ hoặc cho một biến bắt đầu bằng dấu gạch dưới -

_some_random_value = 42

Ngoài ra elixir dựa vào dấu gạch dưới để làm cho các chức năng riêng tư cho các mô-đun. Nếu bạn đặt tên một hàm có dấu gạch dưới ở đầu trong một mô-đun và nhập mô-đun đó, thì hàm này sẽ không được nhập.

Có rất nhiều điều phức tạp khác liên quan đến đặt tên hàm trong Elixir mà chúng ta sẽ thảo luận trong các chương tới.

Từ dành riêng

Các từ sau được dành riêng và không được dùng làm biến, tên mô-đun hoặc hàm.

after     and     catch     do     inbits     inlist     nil     else     end 
not     or     false     fn     in     rescue     true     when     xor 
__MODULE__    __FILE__    __DIR__    __ENV__    __CALLER__

Để sử dụng bất kỳ ngôn ngữ nào, bạn cần hiểu các kiểu dữ liệu cơ bản mà ngôn ngữ đó hỗ trợ. Trong chương này, chúng ta sẽ thảo luận về 7 kiểu dữ liệu cơ bản được ngôn ngữ elixir hỗ trợ: số nguyên, phao, Boolean, nguyên tử, chuỗi, danh sách và bộ giá trị.

Kiểu số

Elixir, giống như bất kỳ ngôn ngữ lập trình nào khác, hỗ trợ cả số nguyên và số float. Nếu bạn mở elixir shell của mình và nhập bất kỳ số nguyên hoặc float nào làm đầu vào, nó sẽ trả về giá trị của nó. Ví dụ,

42

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

42

Bạn cũng có thể xác định các số trong cơ sở bát phân, hex và nhị phân.

Bát phân

Để xác định một số trong cơ số bát phân, hãy đặt tiền tố nó bằng '0o'. Ví dụ: 0o52 trong bát phân tương đương với 42 trong thập phân.

Hệ thập lục phân

Để xác định một số trong cơ số thập phân, hãy đặt tiền tố bằng '0x'. Ví dụ: 0xF1 trong hệ thập lục phân tương đương với 241 trong hệ thập phân.

Nhị phân

Để xác định một số trong cơ sở nhị phân, hãy đặt tiền tố nó bằng '0b'. Ví dụ: 0b1101 trong hệ nhị phân tương đương với 13 trong hệ thập phân.

Elixir hỗ trợ độ chính xác kép 64 bit cho các số dấu phẩy động. Và chúng cũng có thể được xác định bằng cách sử dụng kiểu lũy thừa. Ví dụ: 10145230000 có thể được viết là 1,014523e10

Nguyên tử

Nguyên tử là hằng số có tên là giá trị của chúng. Chúng có thể được tạo bằng cách sử dụng ký hiệu màu (:). Ví dụ,

:hello

Booleans

Elixir hỗ trợ truefalsenhư Booleans. Trên thực tế, cả hai giá trị này đều được gắn với các nguyên tử: true và: false tương ứng.

Dây

Các chuỗi trong Elixir được chèn vào giữa dấu ngoặc kép và chúng được mã hóa bằng UTF-8. Chúng có thể kéo dài nhiều dòng và chứa các phép nội suy. Để xác định một chuỗi, chỉ cần nhập chuỗi đó trong dấu ngoặc kép:

"Hello world"

Để xác định chuỗi nhiều dòng, chúng tôi sử dụng cú pháp tương tự như python với ba dấu ngoặc kép -

"""
Hello
World!
"""

Chúng ta sẽ tìm hiểu sâu hơn về chuỗi, mã nhị phân và danh sách char (tương tự như chuỗi) trong chương chuỗi.

Binaries

Dấu phân cách là chuỗi các byte nằm trong dấu << >> được phân tách bằng dấu phẩy. Ví dụ,

<< 65, 68, 75>>

Binaries chủ yếu được sử dụng để xử lý dữ liệu liên quan đến bit và byte, nếu bạn có. Theo mặc định, chúng có thể lưu trữ 0 đến 255 trong mỗi giá trị. Giới hạn kích thước này có thể được tăng lên bằng cách sử dụng hàm kích thước cho biết cần có bao nhiêu bit để lưu trữ giá trị đó. Ví dụ,

<<65, 255, 289::size(15)>>

Danh sách

Elixir sử dụng dấu ngoặc vuông để chỉ định danh sách các giá trị. Giá trị có thể thuộc bất kỳ loại nào. Ví dụ,

[1, "Hello", :an_atom, true]

Danh sách đi kèm với các hàm có sẵn cho phần đầu và phần đuôi của danh sách có tên hd và tl, các hàm này trả về phần đầu và phần đuôi của danh sách tương ứng. Đôi khi khi bạn tạo một danh sách, nó sẽ trả về một danh sách char. Điều này là do khi elixir nhìn thấy danh sách các ký tự ASCII có thể in được, nó sẽ in nó dưới dạng danh sách char. Xin lưu ý rằng chuỗi và danh sách char không bằng nhau. Chúng ta sẽ thảo luận thêm về danh sách trong các chương sau.

Tuples

Elixir sử dụng dấu ngoặc nhọn để xác định các bộ giá trị. Giống như danh sách, bộ giá trị có thể chứa bất kỳ giá trị nào.

{ 1, "Hello", :an_atom, true

Một câu hỏi đặt ra ở đây, - tại sao lại cung cấp cả hai liststupleskhi cả hai đều hoạt động theo cùng một cách? Họ có các cách triển khai khác nhau.

  • Danh sách thực sự được lưu trữ dưới dạng danh sách liên kết, vì vậy việc chèn, xóa rất nhanh trong danh sách.

  • Mặt khác, các bộ dữ liệu được lưu trữ trong khối bộ nhớ liền kề, giúp truy cập chúng nhanh hơn nhưng làm tăng thêm chi phí cho việc chèn và xóa.

Một biến cung cấp cho chúng ta bộ nhớ được đặt tên mà chương trình của chúng ta có thể thao tác. Mỗi biến trong Elixir có một kiểu cụ thể, xác định kích thước và cách bố trí bộ nhớ của biến; phạm vi giá trị có thể được lưu trữ trong bộ nhớ đó; và tập hợp các thao tác có thể áp dụng cho biến.

Các loại biến

Elixir hỗ trợ các loại biến cơ bản sau.

Số nguyên

Chúng được sử dụng cho Số nguyên. Chúng có kích thước 32 bit trên kiến ​​trúc 32 bit và 64 bit trên kiến ​​trúc 64 bit. Các số nguyên luôn được đăng nhập trong elixir. Nếu một số nguyên bắt đầu mở rộng kích thước vượt quá giới hạn của nó, elixir chuyển nó thành Số nguyên lớn chiếm bộ nhớ trong phạm vi 3 đến n từ, tùy theo bất kỳ từ nào có thể vừa với nó trong bộ nhớ.

Phao nổi

Pha nổi có độ chính xác 64-bit trong thuốc tiên. Chúng cũng giống như số nguyên về bộ nhớ. Khi xác định float, có thể sử dụng ký hiệu hàm mũ.

Boolean

Chúng có thể nhận 2 giá trị là đúng hoặc sai.

Dây

Các chuỗi được mã hóa utf-8 trong elixir. Chúng có một mô-đun chuỗi cung cấp rất nhiều chức năng cho người lập trình để thao tác với chuỗi.

Chức năng ẩn danh / Lambdas

Đây là những hàm có thể được định nghĩa và gán cho một biến, sau đó có thể được sử dụng để gọi hàm này.

Bộ sưu tập

Có rất nhiều loại bộ sưu tập có sẵn trong Elixir. Một số trong số đó là Danh sách, Tuples, Bản đồ, Binaries, v.v. Chúng sẽ được thảo luận trong các chương tiếp theo.

Sự định nghĩa biến

Một khai báo biến cho trình thông dịch biết vị trí và dung lượng để tạo bộ lưu trữ cho biến. Elixir không cho phép chúng ta chỉ khai báo một biến. Một biến phải được khai báo và gán giá trị cùng một lúc. Ví dụ, để tạo một biến có tên là life và gán cho nó một giá trị 42, chúng ta làm như sau:

life = 42

Điều này sẽ ràng buộc tuổi thọ của biến với giá trị 42. Nếu chúng ta muốn gán lại biến này một giá trị mới, chúng ta có thể thực hiện việc này bằng cách sử dụng cú pháp tương tự như trên, tức là,

life = "Hello world"

Đặt tên biến

Các biến đặt tên tuân theo một snake_casequy ước trong Elixir, tức là tất cả các biến phải bắt đầu bằng một chữ cái thường, theo sau là 0 hoặc nhiều chữ cái (cả chữ hoa và chữ thường), theo sau là dấu '?' tùy chọn HOẶC LÀ '!'.

Tên biến cũng có thể được bắt đầu bằng dấu gạch dưới ở đầu nhưng chỉ được sử dụng khi bỏ qua biến, tức là biến đó sẽ không được sử dụng nữa mà cần được gán cho một thứ gì đó.

In các biến

Trong shell tương tác, các biến sẽ in ra nếu bạn chỉ cần nhập tên biến. Ví dụ: nếu bạn tạo một biến -

life = 42

Và nhập 'life' vào shell của bạn, bạn sẽ nhận được đầu ra là -

42

Nhưng nếu bạn muốn xuất một biến ra bảng điều khiển (Khi chạy tập lệnh bên ngoài từ tệp), bạn cần cung cấp biến làm đầu vào cho IO.puts chức năng -

life = 42  
IO.puts life

hoặc là

life = 42 
IO.puts(life)

Điều này sẽ cung cấp cho bạn kết quả sau:

42

Một toán tử là một ký hiệu yêu cầu trình biên dịch thực hiện các thao tác toán học hoặc logic cụ thể. Có rất nhiều nhà khai thác được cung cấp bởi elixir. Chúng được chia thành các loại sau:

  • Toán tử số học
  • Toán tử so sánh
  • Toán tử boolean
  • Toán tử khác

Toán tử số học

Bảng sau đây cho thấy tất cả các toán tử số học được hỗ trợ bởi ngôn ngữ Elixir. Giả sử biếnA giữ 10 và biến B giữ 20, sau đó -

Hiển thị các ví dụ

Nhà điều hành Sự miêu tả Thí dụ
+ Thêm 2 số. A + B sẽ cho 30
- Trừ số thứ hai cho số thứ nhất. AB sẽ cho -10
* Nhân hai số. A * B sẽ cho 200
/ Chia số đầu tiên cho số thứ hai. Điều này truyền các số trong float và cho kết quả float A / B sẽ cho 0,5.
div Hàm này được sử dụng để lấy thương số trên phép chia. div (10,20) sẽ cho 0
rem Hàm này được sử dụng để lấy phần dư khi chia. rem (A, B) sẽ cho 10

Toán tử so sánh

Các toán tử so sánh trong Elixir hầu hết phổ biến với các toán tử được cung cấp trong hầu hết các ngôn ngữ khác. Bảng sau đây tổng hợp các toán tử so sánh trong Elixir. Giả sử biếnA giữ 10 và biến B giữ 20, sau đó -

Hiển thị các ví dụ

Nhà điều hành Sự miêu tả Thí dụ
== Kiểm tra xem giá trị bên trái có bằng giá trị bên phải hay không (Nhập giá trị nếu chúng không cùng loại). A == B sẽ cho sai
! = Kiểm tra nếu giá trị bên trái không bằng giá trị bên phải. A! = B sẽ cho đúng
=== Kiểm tra xem loại giá trị ở bên trái bằng loại giá trị ở bên phải, nếu có thì kiểm tra giá trị tương tự. A === B sẽ cho sai
! == Tương tự như trên nhưng kiểm tra bất bình đẳng thay vì bình đẳng. A! == B sẽ cho đúng
> Kiểm tra xem giá trị của toán hạng bên trái có lớn hơn giá trị của toán hạng bên phải hay không; nếu có, thì điều kiện trở thành đúng. A> B sẽ cho sai
< Kiểm tra nếu giá trị của toán hạng bên trái nhỏ hơn giá trị của toán hạng bên phải; nếu có, thì điều kiện trở thành đúng. A <B sẽ cho đúng
> = Kiểm tra nếu giá trị của toán hạng bên trái lớn hơn hoặc bằng giá trị của toán hạng bên phải; nếu có, thì điều kiện trở thành đúng. A> = B sẽ cho sai
<= Kiểm tra nếu giá trị của toán hạng bên trái nhỏ hơn hoặc bằng giá trị của toán hạng bên phải; nếu có, thì điều kiện trở thành đúng. A <= B sẽ cho đúng

Toán tử logic

Elixir cung cấp 6 toán tử logic: và, hoặc, không, &&, || và!. Ba đầu tiên,and or notlà các toán tử Boolean nghiêm ngặt, có nghĩa là họ mong đợi đối số đầu tiên của họ là một Boolean. Đối số không Boolean sẽ gây ra lỗi. Trong khi ba tiếp theo,&&, || and !không nghiêm ngặt, không yêu cầu chúng tôi phải có giá trị đầu tiên nghiêm ngặt như một boolean. Họ làm việc theo cách giống như các đối tác nghiêm ngặt của họ. Giả sử biếnA giữ đúng và biến B giữ 20, sau đó -

Hiển thị các ví dụ

Nhà điều hành Sự miêu tả Thí dụ
Kiểm tra xem cả hai giá trị được cung cấp có trung thực không, nếu có thì trả về giá trị của biến thứ hai. (Lôgic và). A và B sẽ cho 20
hoặc là Kiểm tra nếu một trong hai giá trị được cung cấp là trung thực. Trả về bất kỳ giá trị nào là trung thực. Khác trả về false. (Lôgic hoặc). A hoặc B sẽ cho đúng
không phải Toán tử đơn nguyên đảo ngược giá trị của đầu vào đã cho. không A sẽ cho sai
&& Không nghiêm ngặt and. Hoạt động giống nhưand nhưng không mong đợi đối số đầu tiên là một Boolean. B && A sẽ cho 20
|| Không nghiêm ngặt or. Hoạt động giống nhưor nhưng không mong đợi đối số đầu tiên là một Boolean. B || A sẽ cho đúng
! Không nghiêm ngặt not. Hoạt động giống nhưnot nhưng không mong đợi đối số là Boolean. ! A sẽ cho sai

NOTE − , hoặc , &&|| || là các nhà khai thác ngắn mạch. Điều này có nghĩa là nếu đối số đầu tiên củaandlà sai, sau đó nó sẽ không kiểm tra thêm cho cái thứ hai. Và nếu đối số đầu tiên củaorlà đúng, thì nó sẽ không kiểm tra cái thứ hai. Ví dụ,

false and raise("An error")  
#This won't raise an error as raise function wont get executed because of short
#circuiting nature of and operator

Toán tử Bitwise

Toán tử bitwise làm việc trên các bit và thực hiện thao tác từng bit. Elixir cung cấp các mô-đun bitwise như một phần của góiBitwise, vì vậy để sử dụng chúng, bạn cần sử dụng mô-đun bitwise. Để sử dụng nó, hãy nhập lệnh sau vào trình bao của bạn:

use Bitwise

Giả sử A là 5 và B là 6 cho các ví dụ sau:

Hiển thị các ví dụ

Nhà điều hành Sự miêu tả Thí dụ
&&& Bitwise và toán tử sao chép một bit để kết quả nếu nó tồn tại trong cả hai toán hạng. A &&& B sẽ cho 4
||| Bitwise hoặc toán tử sao chép một bit để kết quả nếu nó tồn tại trong một trong hai toán hạng. A ||| B sẽ cho 7
>>> Toán tử dịch sang phải theo bit dịch chuyển các bit toán hạng đầu tiên sang phải theo số được chỉ định trong toán hạng thứ hai. A >>> B sẽ cho 0
<<< Toán tử dịch chuyển trái theo bit dịch chuyển các bit toán hạng đầu tiên sang trái bởi số được chỉ định trong toán hạng thứ hai. A <<< B sẽ cho 320
^^^ Toán tử Bitwise XOR sao chép một bit để chỉ kết quả nếu nó khác nhau trên cả hai toán hạng. A ^^^ B sẽ cho 3
~~~ Bitwise đơn lẻ không đảo ngược các bit trên một số nhất định. ~~~ A sẽ cho -6

Nhà điều hành khác

Ngoài các toán tử ở trên, Elixir cũng cung cấp một loạt các toán tử khác như Concatenation Operator, Match Operator, Pin Operator, Pipe Operator, String Match Operator, Code Point Operator, Capture Operator, Ternary Operator khiến nó trở thành một ngôn ngữ khá mạnh mẽ.

Hiển thị các ví dụ

Đối sánh mẫu là một kỹ thuật mà Elixir kế thừa mẫu Erlang. Đây là một kỹ thuật rất mạnh mẽ cho phép chúng ta trích xuất các cấu trúc con đơn giản hơn từ các cấu trúc dữ liệu phức tạp như danh sách, bộ dữ liệu, bản đồ, v.v.

Một trận đấu có 2 phần chính, left và một rightbên. Phía bên phải là cấu trúc dữ liệu thuộc bất kỳ loại nào. Phía bên trái cố gắng khớp cấu trúc dữ liệu ở phía bên phải và liên kết bất kỳ biến nào ở bên trái với cấu trúc con tương ứng ở bên phải. Nếu không tìm thấy kết quả phù hợp, nhà điều hành sẽ đưa ra lỗi.

Đối sánh đơn giản nhất là một biến duy nhất ở bên trái và bất kỳ cấu trúc dữ liệu nào ở bên phải. This variable will match anything. Ví dụ,

x = 12
x = "Hello"
IO.puts(x)

Bạn có thể đặt các biến bên trong một cấu trúc để bạn có thể nắm bắt một cấu trúc con. Ví dụ,

[var_1, _unused_var, var_2] = [{"First variable"}, 25, "Second variable" ]
IO.puts(var_1)
IO.puts(var_2)

Điều này sẽ lưu trữ các giá trị, {"First variable"}trong var_1"Second variable"trong var_2 . Ngoài ra còn có một_ biến (hoặc các biến có tiền tố là '_') hoạt động chính xác như các biến khác nhưng nói với elixir, "Make sure something is here, but I don't care exactly what it is.". Trong ví dụ trước, _unused_var là một trong những biến như vậy.

Chúng ta có thể ghép các mẫu phức tạp hơn bằng kỹ thuật này. Đối vớiexample nếu bạn muốn mở và lấy một số trong một bộ giá trị nằm trong danh sách mà bản thân nó nằm trong danh sách, bạn có thể sử dụng lệnh sau:

[_, [_, {a}]] = ["Random string", [:an_atom, {24}]]
IO.puts(a)

Chương trình trên tạo ra kết quả sau:

24

Điều này sẽ ràng buộc a đến 24. Các giá trị khác bị bỏ qua khi chúng ta đang sử dụng '_'.

Trong đối sánh mẫu, nếu chúng ta sử dụng một biến trên right, giá trị của nó được sử dụng. Nếu bạn muốn sử dụng giá trị của một biến ở bên trái, bạn sẽ cần sử dụng toán tử pin.

Ví dụ: nếu bạn có một biến "a" có giá trị 25 và bạn muốn so khớp nó với một biến khác "b" có giá trị 25, thì bạn cần nhập:

a = 25
b = 25
^a = b

Dòng cuối cùng khớp với giá trị hiện tại của a, thay vì gán nó, cho giá trị của b. Nếu chúng ta có tập hợp bên trái và bên phải không khớp nhau, người điều khiển đối sánh sẽ mắc lỗi. Ví dụ: nếu chúng tôi cố gắng khớp một bộ tuple với một danh sách hoặc một danh sách có kích thước 2 với danh sách có kích thước 3, một lỗi sẽ được hiển thị.

Cấu trúc ra quyết định yêu cầu người lập trình chỉ định một hoặc nhiều điều kiện để chương trình đánh giá hoặc kiểm tra, cùng với một câu lệnh hoặc các câu lệnh sẽ được thực thi nếu điều kiện đó được xác định là truevà tùy chọn, các câu lệnh khác sẽ được thực thi nếu điều kiện được xác định là false.

Sau đây là khái quát về cấu trúc ra quyết định điển hình được tìm thấy trong hầu hết các ngôn ngữ lập trình:

Elixir cung cấp các cấu trúc có điều kiện if / else giống như nhiều ngôn ngữ lập trình khác. Nó cũng có mộtcondcâu lệnh gọi giá trị đúng đầu tiên mà nó tìm thấy. Trường hợp là một câu lệnh luồng điều khiển khác sử dụng khớp mẫu để điều khiển luồng chương trình. Hãy có một cái nhìn sâu sắc về chúng.

Elixir cung cấp các loại câu lệnh ra quyết định sau. Nhấp vào các liên kết sau để kiểm tra chi tiết của chúng.

Sr.No. Tuyên bố & Mô tả
1 câu lệnh if

Một câu lệnh if bao gồm một biểu thức Boolean theo sau là do, một hoặc nhiều câu lệnh thực thi và cuối cùng là một endtừ khóa. Mã trong câu lệnh if chỉ thực thi nếu điều kiện Boolean đánh giá là true.

2 if..else statement

Một câu lệnh if có thể được theo sau bởi một câu lệnh else tùy chọn (trong khối do..end), thực thi khi biểu thức Boolean sai.

3 trừ khi tuyên bố

Câu lệnh if có cùng phần thân với câu lệnh if. Mã bên trong câu lệnh if chỉ thực thi khi điều kiện được chỉ định là sai.

4 trừ khi..các tuyên bố sau

Câu lệnh if..else có cùng phần thân với câu lệnh if..else. Mã bên trong câu lệnh if chỉ thực thi khi điều kiện được chỉ định là sai.

5 chung cư

Câu lệnh cond được sử dụng khi chúng ta muốn thực thi mã trên cơ sở một số điều kiện. Nó hoạt động giống như một cấu trúc if… else if… .else trong một số ngôn ngữ lập trình khác.

6 trường hợp

Câu lệnh case có thể được coi là sự thay thế cho câu lệnh switch trong ngôn ngữ mệnh lệnh. Trường hợp nhận một biến / chữ và áp dụng đối sánh mẫu với nó với các trường hợp khác nhau. Nếu bất kỳ trường hợp nào khớp, Elixir thực thi mã được liên kết với trường hợp đó và thoát khỏi câu lệnh trường hợp.

Các chuỗi trong Elixir được chèn vào giữa dấu ngoặc kép và chúng được mã hóa bằng UTF-8. Không giống như C và C ++ trong đó các chuỗi mặc định được mã hóa ASCII và chỉ có thể có 256 ký tự khác nhau, UTF-8 bao gồm 1.112.064 điểm mã. Điều này có nghĩa là mã hóa UTF-8 bao gồm nhiều ký tự có thể có khác nhau. Vì các chuỗi sử dụng utf-8, chúng ta cũng có thể sử dụng các ký hiệu như: ö, ł, v.v.

Tạo một chuỗi

Để tạo một biến chuỗi, chỉ cần gán một chuỗi cho một biến -

str = "Hello world"

Để in cái này ra bảng điều khiển của bạn, chỉ cần gọi IO.puts và chuyển cho nó biến str -

str = str = "Hello world" 
IO.puts(str)

Chương trình trên tạo ra kết quả sau:

Hello World

Chuỗi trống

Bạn có thể tạo một chuỗi rỗng bằng cách sử dụng chuỗi ký tự, "". Ví dụ,

a = ""
if String.length(a) === 0 do
   IO.puts("a is an empty string")
end

Chương trình trên tạo ra kết quả sau.

a is an empty string

Nội suy chuỗi

Nội suy chuỗi là một cách để xây dựng một giá trị Chuỗi mới từ hỗn hợp các hằng số, biến, ký tự và biểu thức bằng cách bao gồm các giá trị của chúng bên trong một ký tự chuỗi. Elixir hỗ trợ nội suy chuỗi, để sử dụng một biến trong một chuỗi, khi viết nó, hãy bọc nó bằng dấu ngoặc nhọn và thêm vào trước dấu ngoặc nhọn bằng một'#' ký tên.

Ví dụ,

x = "Apocalypse" 
y = "X-men #{x}"
IO.puts(y)

Điều này sẽ lấy giá trị của x và thay thế nó bằng y. Đoạn mã trên sẽ tạo ra kết quả sau:

X-men Apocalypse

Kết nối chuỗi

Chúng ta đã thấy việc sử dụng nối chuỗi trong các chương trước. Toán tử '<>' được sử dụng để nối các chuỗi trong Elixir. Để nối 2 chuỗi,

x = "Dark"
y = "Knight"
z = x <> " " <> y
IO.puts(z)

Đoạn mã trên tạo ra kết quả sau:

Dark Knight

Chiều dài chuỗi

Để có được độ dài của chuỗi, chúng tôi sử dụng String.lengthchức năng. Chuyển chuỗi làm tham số và nó sẽ hiển thị cho bạn kích thước của nó. Ví dụ,

IO.puts(String.length("Hello"))

Khi chạy chương trình trên, nó tạo ra kết quả sau:

5

Đảo ngược một chuỗi

Để đảo ngược một chuỗi, hãy chuyển nó vào hàm String.reverse. Ví dụ,

IO.puts(String.reverse("Elixir"))

Chương trình trên tạo ra kết quả sau:

rixilE

So sánh chuỗi

Để so sánh 2 chuỗi, chúng ta có thể sử dụng toán tử == hoặc ===. Ví dụ,

var_1 = "Hello world"
var_2 = "Hello Elixir"
if var_1 === var_2 do
   IO.puts("#{var_1} and #{var_2} are the same")
else
   IO.puts("#{var_1} and #{var_2} are not the same")
end

Chương trình trên tạo ra kết quả sau:

Hello world and Hello elixir are not the same.

Khớp chuỗi

Chúng ta đã thấy việc sử dụng toán tử đối sánh chuỗi = ~. Để kiểm tra xem một chuỗi có khớp với một regex hay không, chúng ta cũng có thể sử dụng toán tử đối sánh chuỗi hoặc String.match? chức năng. Ví dụ,

IO.puts(String.match?("foo", ~r/foo/))
IO.puts(String.match?("bar", ~r/foo/))

Chương trình trên tạo ra kết quả sau:

true 
false

Điều này cũng có thể đạt được bằng cách sử dụng toán tử = ~. Ví dụ,

IO.puts("foo" =~ ~r/foo/)

Chương trình trên tạo ra kết quả sau:

true

Hàm chuỗi

Elixir hỗ trợ một số lượng lớn các hàm liên quan đến chuỗi, một số hàm được sử dụng nhiều nhất được liệt kê trong bảng sau.

Sr.No. Chức năng và Mục đích của nó
1

at(string, position)

Trả về grapheme tại vị trí của chuỗi utf8 đã cho. Nếu vị trí lớn hơn độ dài chuỗi, thì nó trả về nil

2

capitalize(string)

Chuyển ký tự đầu tiên trong chuỗi đã cho thành chữ hoa và phần còn lại thành chữ thường

3

contains?(string, contents)

Kiểm tra xem chuỗi có chứa bất kỳ nội dung đã cho nào không

4

downcase(string)

Chuyển đổi tất cả các ký tự trong chuỗi đã cho thành chữ thường

5

ends_with?(string, suffixes)

Trả về true nếu chuỗi kết thúc bằng bất kỳ hậu tố nào đã cho

6

first(string)

Trả về grapheme đầu tiên từ chuỗi utf8, nil nếu chuỗi trống

7

last(string)

Trả về grapheme cuối cùng từ chuỗi utf8, nil nếu chuỗi trống

số 8

replace(subject, pattern, replacement, options \\ [])

Trả về một chuỗi mới được tạo bằng cách thay thế các lần xuất hiện của mẫu trong chủ đề bằng thay thế

9

slice(string, start, len)

Trả về một chuỗi con bắt đầu từ điểm bắt đầu offset và có độ dài len

10

split(string)

Chia một chuỗi thành các chuỗi con tại mỗi lần xuất hiện khoảng trắng Unicode với khoảng trắng đầu và cuối bị bỏ qua. Các nhóm khoảng trắng được coi là một lần xuất hiện duy nhất. Sự phân chia không xảy ra trên khoảng trắng không đứt quãng

11

upcase(string)

Chuyển đổi tất cả các ký tự trong chuỗi đã cho thành chữ hoa

Binaries

Một nhị phân chỉ là một chuỗi các byte. Binaries được xác định bằng cách sử dụng<< >>. Ví dụ:

<< 0, 1, 2, 3 >>

Tất nhiên, những byte đó có thể được tổ chức theo bất kỳ cách nào, ngay cả trong một chuỗi không làm cho chúng trở thành một chuỗi hợp lệ. Ví dụ,

<< 239, 191, 191 >>

Các chuỗi cũng là các mã nhị phân. Và toán tử nối chuỗi<> thực sự là một toán tử nối nhị phân:

IO.puts(<< 0, 1 >> <> << 2, 3 >>)

Đoạn mã trên tạo ra kết quả sau:

<< 0, 1, 2, 3 >>

Lưu ý ký tự ł. Vì điều này được mã hóa utf-8, biểu diễn ký tự này chiếm 2 byte.

Vì mỗi số được biểu diễn trong một hệ nhị phân có nghĩa là một byte, khi giá trị này tăng lên từ 255, nó sẽ bị cắt bớt. Để ngăn chặn điều này, chúng tôi sử dụng công cụ sửa đổi kích thước để chỉ định số lượng bit mà chúng tôi muốn lấy. Ví dụ -

IO.puts(<< 256 >>) # truncated, it'll print << 0 >>
IO.puts(<< 256 :: size(16) >>) #Takes 16 bits/2 bytes, will print << 1, 0 >>

Chương trình trên sẽ tạo ra kết quả sau:

<< 0 >>
<< 1, 0 >>

Chúng ta cũng có thể sử dụng công cụ sửa đổi utf8, nếu một ký tự là điểm mã thì nó sẽ được tạo trong đầu ra; khác các byte -

IO.puts(<< 256 :: utf8 >>)

Chương trình trên tạo ra kết quả sau:

Ā

Chúng tôi cũng có một chức năng được gọi là is_binarykiểm tra xem một biến đã cho có phải là một nhị phân hay không. Lưu ý rằng chỉ các biến được lưu trữ dưới dạng bội số của 8bit mới là mã nhị phân.

Chuỗi bit

Nếu chúng ta xác định một hệ nhị phân bằng cách sử dụng công cụ sửa đổi kích thước và chuyển cho nó một giá trị không phải là bội số của 8, chúng ta kết thúc bằng một chuỗi bit thay vì một nhị phân. Ví dụ,

bs = << 1 :: size(1) >>
IO.puts(bs)
IO.puts(is_binary(bs))
IO.puts(is_bitstring(bs))

Chương trình trên tạo ra kết quả sau:

<< 1::size(1) >>
false
true

Điều này có nghĩa là biến bskhông phải là một nhị phân mà là một chuỗi bit. Chúng ta cũng có thể nói rằng nhị phân là một chuỗi bit trong đó số bit chia hết cho 8. So khớp mẫu hoạt động trên các mã nhị phân cũng như chuỗi bit theo cùng một cách.

Danh sách char không gì khác hơn là một danh sách các ký tự. Hãy xem xét chương trình sau để hiểu tương tự.

IO.puts('Hello')
IO.puts(is_list('Hello'))

Chương trình trên tạo ra kết quả sau:

Hello
true

Thay vì chứa các byte, một danh sách char chứa các điểm mã của các ký tự giữa các dấu nháy đơn. So while the double-quotes represent a string (i.e. a binary), singlequotes represent a char list (i.e. a list). Lưu ý rằng IEx sẽ chỉ tạo ra các điểm mã dưới dạng đầu ra nếu bất kỳ ký tự nào nằm ngoài phạm vi ASCII.

Danh sách Char được sử dụng hầu hết khi giao tiếp với Erlang, đặc biệt là các thư viện cũ không chấp nhận các tệp nhị phân làm đối số. Bạn có thể chuyển đổi một danh sách char thành một chuỗi và quay lại bằng cách sử dụng các hàm to_string (char_list) và to_char_list (string) -

IO.puts(is_list(to_char_list("hełło")))
IO.puts(is_binary(to_string ('hełło')))

Chương trình trên tạo ra kết quả sau:

true
true

NOTE - Các chức năng to_stringto_char_list là đa hình, tức là, chúng có thể nhận nhiều loại đầu vào như nguyên tử, số nguyên và chuyển đổi chúng thành chuỗi và danh sách char tương ứng.

(Liên kết) Danh sách

Danh sách liên kết là một danh sách không đồng nhất của các phần tử được lưu trữ tại các vị trí khác nhau trong bộ nhớ và được theo dõi bằng cách sử dụng các tham chiếu. Danh sách liên kết là cấu trúc dữ liệu đặc biệt được sử dụng trong lập trình chức năng.

Elixir sử dụng dấu ngoặc vuông để chỉ định danh sách các giá trị. Giá trị có thể thuộc bất kỳ loại nào -

[1, 2, true, 3]

Khi Elixir nhìn thấy danh sách các số ASCII có thể in được, Elixir sẽ in đó dưới dạng danh sách char (nghĩa đen là danh sách các ký tự). Bất cứ khi nào bạn thấy một giá trị trong IEx và bạn không chắc nó là gì, bạn có thể sử dụngi chức năng để lấy thông tin về nó.

IO.puts([104, 101, 108, 108, 111])

Các ký tự trên trong danh sách đều có thể in được. Khi chương trình trên được chạy, nó tạo ra kết quả sau:

hello

Bạn cũng có thể xác định danh sách theo cách khác, bằng cách sử dụng các dấu ngoặc kép -

IO.puts(is_list('Hello'))

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

true

Hãy nhớ rằng các đại diện được trích dẫn đơn và được trích dẫn kép không tương đương trong Elixir vì chúng được thể hiện bằng các kiểu khác nhau.

Độ dài của một danh sách

Để tìm độ dài của một danh sách, chúng ta sử dụng hàm độ dài như trong chương trình sau:

IO.puts(length([1, 2, :true, "str"]))

Chương trình trên tạo ra kết quả sau:

4

Phép cộng và phép trừ

Hai danh sách có thể được nối và trừ bằng cách sử dụng ++--các toán tử. Hãy xem xét ví dụ sau để hiểu các chức năng.

IO.puts([1, 2, 3] ++ [4, 5, 6])
IO.puts([1, true, 2, false, 3, true] -- [true, false])

Điều này sẽ cung cấp cho bạn một chuỗi được nối trong trường hợp đầu tiên và một chuỗi bị trừ trong trường hợp thứ hai. Chương trình trên tạo ra kết quả sau:

[1, 2, 3, 4, 5, 6]
[1, 2, 3, true]

Đầu và đuôi của một danh sách

Phần đầu là phần tử đầu tiên của danh sách và phần đuôi là phần còn lại của danh sách. Chúng có thể được truy xuất với các chức nănghdtl. Chúng ta hãy gán một danh sách cho một biến và truy xuất phần đầu và phần đuôi của nó.

list = [1, 2, 3]
IO.puts(hd(list))
IO.puts(tl(list))

Điều này sẽ cho chúng ta phần đầu và phần đuôi của danh sách dưới dạng đầu ra. Chương trình trên tạo ra kết quả sau:

1
[2, 3]

Note - Bắt đầu hoặc đuôi của một danh sách trống là một lỗi.

Các chức năng Danh sách khác

Thư viện tiêu chuẩn Elixir cung cấp rất nhiều chức năng để xử lý danh sách. Chúng tôi sẽ xem xét một số trong số đó ở đây. Bạn có thể kiểm tra phần còn lại ở đây Danh sách .

S.no. Tên và mô tả chức năng
1

delete(list, item)

Xóa mục đã cho khỏi danh sách. Trả về danh sách không có mục. Nếu mục xuất hiện nhiều lần trong danh sách, chỉ mục xuất hiện đầu tiên sẽ bị loại bỏ.

2

delete_at(list, index)

Tạo một danh sách mới bằng cách xóa giá trị tại chỉ mục được chỉ định. Các chỉ số âm cho biết một khoảng chênh lệch so với phần cuối của danh sách. Nếu chỉ mục nằm ngoài giới hạn, danh sách ban đầu được trả về.

3

first(list)

Trả về phần tử đầu tiên trong danh sách hoặc nil nếu danh sách trống.

4

flatten(list)

Làm phẳng danh sách đã cho của danh sách lồng nhau.

5

insert_at(list, index, value)

Trả về một danh sách với giá trị được chèn tại chỉ mục đã chỉ định. Lưu ý rằng chỉ mục được giới hạn ở độ dài danh sách. Các chỉ số âm cho biết một khoảng chênh lệch so với phần cuối của danh sách.

6

last(list)

Trả về phần tử cuối cùng trong danh sách hoặc nil nếu danh sách trống.

Tuples

Tuples cũng là cấu trúc dữ liệu lưu trữ một số cấu trúc khác bên trong chúng. Không giống như danh sách, chúng lưu trữ các phần tử trong một khối bộ nhớ liền kề. Điều này có nghĩa là việc truy cập một phần tử tuple trên mỗi chỉ mục hoặc lấy kích thước tuple là một thao tác nhanh. Chỉ mục bắt đầu từ số không.

Elixir sử dụng dấu ngoặc nhọn để xác định các bộ giá trị. Giống như danh sách, các bộ giá trị có thể chứa bất kỳ giá trị nào -

{:ok, "hello"}

Chiều dài của một Tuple

Để có được chiều dài của một tuple, hãy sử dụng tuple_size hoạt động như trong chương trình sau:

IO.puts(tuple_size({:ok, "hello"}))

Chương trình trên tạo ra kết quả sau:

2

Bổ sung một giá trị

Để thêm giá trị vào tuple, hãy sử dụng hàm Tuple.append -

tuple = {:ok, "Hello"}
Tuple.append(tuple, :world)

Thao tác này sẽ tạo và trả về một tuple mới: {: ok, "Hello",: world}

Chèn giá trị

Để chèn một giá trị tại một vị trí nhất định, chúng ta có thể sử dụng Tuple.insert_at chức năng hoặc put_elemchức năng. Hãy xem xét ví dụ sau để hiểu tương tự -

tuple = {:bar, :baz}
new_tuple_1 = Tuple.insert_at(tuple, 0, :foo)
new_tuple_2 = put_elem(tuple, 1, :foobar)

Thông báo rằng put_eleminsert_atđã trả lại các bộ giá trị mới. Tuple ban đầu được lưu trữ trong biến tuple không được sửa đổi vì kiểu dữ liệu Elixir là bất biến. Bằng cách không thay đổi, mã Elixir dễ lý giải hơn vì bạn không bao giờ cần phải lo lắng nếu một mã cụ thể đang thay đổi cấu trúc dữ liệu của bạn.

Tuples vs. Lists

Sự khác biệt giữa danh sách và bộ giá trị là gì?

Danh sách được lưu trữ trong bộ nhớ dưới dạng danh sách liên kết, nghĩa là mỗi phần tử trong danh sách giữ giá trị của nó và trỏ đến phần tử sau cho đến khi đạt đến cuối danh sách. Chúng tôi gọi mỗi cặp giá trị và con trỏ là một ô khuyết điểm. Điều này có nghĩa là truy cập độ dài của một danh sách là một phép toán tuyến tính: chúng ta cần xem qua toàn bộ danh sách để tìm ra kích thước của nó. Việc cập nhật danh sách diễn ra nhanh chóng miễn là chúng tôi đã bổ sung trước các phần tử.

Mặt khác, các bộ lưu trữ được lưu trữ liên tục trong bộ nhớ. Điều này có nghĩa là nhanh chóng nhận được kích thước tuple hoặc truy cập một phần tử theo chỉ mục. Tuy nhiên, việc cập nhật hoặc thêm các phần tử vào các bộ giá trị rất tốn kém vì nó yêu cầu sao chép toàn bộ bộ mã trong bộ nhớ.

Cho đến nay, chúng ta chưa thảo luận về bất kỳ cấu trúc dữ liệu liên kết nào, tức là cấu trúc dữ liệu có thể liên kết một giá trị nhất định (hoặc nhiều giá trị) với một khóa. Các ngôn ngữ khác nhau gọi các tính năng này bằng các tên khác nhau như từ điển, mã băm, mảng kết hợp, v.v.

Trong Elixir, chúng ta có hai cấu trúc dữ liệu kết hợp chính: danh sách từ khóa và bản đồ. Trong chương này, chúng ta sẽ tập trung vào danh sách Từ khoá.

Trong nhiều ngôn ngữ lập trình chức năng, người ta thường sử dụng danh sách các bộ giá trị 2 mục làm đại diện cho cấu trúc dữ liệu kết hợp. Trong Elixir, khi chúng ta có một danh sách các bộ giá trị và mục đầu tiên của bộ mã (tức là khóa) là một nguyên tử, chúng ta gọi nó là danh sách từ khóa. Hãy xem xét ví dụ sau để hiểu tương tự -

list = [{:a, 1}, {:b, 2}]

Elixir hỗ trợ một cú pháp đặc biệt để xác định các danh sách như vậy. Chúng ta có thể đặt dấu hai chấm ở cuối mỗi nguyên tử và loại bỏ hoàn toàn các bộ giá trị. Ví dụ,

list_1 = [{:a, 1}, {:b, 2}]
list_2 = [a: 1, b: 2]
IO.puts(list_1 == list_2)

Chương trình trên sẽ tạo ra kết quả sau:

true

Cả hai đều đại diện cho một danh sách từ khóa. Vì danh sách từ khóa cũng là danh sách, chúng tôi có thể sử dụng tất cả các thao tác mà chúng tôi đã sử dụng trên các danh sách trên chúng.

Để truy xuất giá trị được liên kết với một nguyên tử trong danh sách từ khóa, hãy chuyển nguyên tử thành [] sau tên của danh sách -

list = [a: 1, b: 2]
IO.puts(list[:a])

Chương trình trên tạo ra kết quả sau:

1

Danh sách từ khóa có ba đặc điểm đặc biệt -

  • Chìa khóa phải là nguyên tử.
  • Các phím được đặt hàng, theo chỉ định của nhà phát triển.
  • Chìa khóa có thể được trao nhiều lần.

Để thao tác danh sách từ khóa, Elixir cung cấp mô-đun Từ khóa . Tuy nhiên, hãy nhớ rằng danh sách từ khóa chỉ đơn giản là danh sách và do đó chúng cung cấp các đặc điểm hiệu suất tuyến tính giống như danh sách. Danh sách càng dài thì càng mất nhiều thời gian để tìm khóa, đếm số lượng mục, v.v. Vì lý do này, danh sách từ khóa được sử dụng chủ yếu trong Elixir dưới dạng tùy chọn. Nếu bạn cần lưu trữ nhiều mục hoặc đảm bảo các liên kết một khóa có giá trị tối đa, bạn nên sử dụng bản đồ để thay thế.

Truy cập một khóa

Để truy cập các giá trị được liên kết với một khóa nhất định, chúng tôi sử dụng Keyword.getchức năng. Nó trả về giá trị đầu tiên được liên kết với khóa đã cho. Để nhận tất cả các giá trị, chúng tôi sử dụng hàm Keyword.get_values. Ví dụ -

kl = [a: 1, a: 2, b: 3] 
IO.puts(Keyword.get(kl, :a)) 
IO.puts(Keyword.get_values(kl))

Chương trình trên sẽ tạo ra kết quả sau:

1
[1, 2]

Chèn khóa

Để thêm một giá trị mới, hãy sử dụng Keyword.put_new. Nếu khóa đã tồn tại, giá trị của nó vẫn không thay đổi -

kl = [a: 1, a: 2, b: 3]
kl_new = Keyword.put_new(kl, :c, 5)
IO.puts(Keyword.get(kl_new, :c))

Khi chương trình trên được chạy, nó tạo ra một danh sách Từ khoá mới với khoá bổ sung, c và tạo ra kết quả sau:

5

Xóa khóa

Nếu bạn muốn xóa tất cả các mục nhập cho một khóa, hãy sử dụng Keyword.delete; để chỉ xóa mục nhập đầu tiên cho một khóa, hãy sử dụng Keyword.delete_first.

kl = [a: 1, a: 2, b: 3, c: 0]
kl = Keyword.delete_first(kl, :b)
kl = Keyword.delete(kl, :a)

IO.puts(Keyword.get(kl, :a))
IO.puts(Keyword.get(kl, :b))
IO.puts(Keyword.get(kl, :c))

Điều này sẽ xóa đầu tiên b trong Danh sách và tất cả atrong danh sách. Khi chương trình trên được chạy, nó sẽ tạo ra kết quả sau:

0

Danh sách từ khóa là một cách thuận tiện để giải quyết nội dung được lưu trữ trong danh sách theo khóa, nhưng bên dưới, Elixir vẫn đang xem qua danh sách. Điều đó có thể phù hợp nếu bạn có các kế hoạch khác cho danh sách đó yêu cầu xem qua tất cả danh sách đó, nhưng nó có thể là một chi phí không cần thiết nếu bạn định sử dụng các khóa làm phương pháp tiếp cận dữ liệu duy nhất.

Đây là nơi bản đồ đến để giải cứu bạn. Bất cứ khi nào bạn cần lưu trữ khóa-giá trị, bản đồ là cấu trúc dữ liệu “đi đến” trong Elixir.

Tạo bản đồ

Bản đồ được tạo bằng cú pháp% {} -

map = %{:a => 1, 2 => :b}

So với danh sách từ khóa, chúng ta có thể thấy hai điểm khác biệt -

  • Bản đồ cho phép bất kỳ giá trị nào làm khóa.
  • Các phím của bản đồ không tuân theo bất kỳ thứ tự nào.

Truy cập một khóa

Để tích lũy giá trị được liên kết với một khóa, Maps sử dụng cú pháp tương tự như danh sách Từ khóa -

map = %{:a => 1, 2 => :b}
IO.puts(map[:a])
IO.puts(map[2])

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

1
b

Chèn khóa

Để chèn khóa vào bản đồ, chúng tôi sử dụng Dict.put_new hàm lấy bản đồ, khóa mới và giá trị mới làm đối số -

map = %{:a => 1, 2 => :b}
new_map = Dict.put_new(map, :new_val, "value") 
IO.puts(new_map[:new_val])

Thao tác này sẽ chèn cặp khóa-giá trị :new_val - "value"trong một bản đồ mới. Khi chương trình trên được chạy, nó tạo ra kết quả sau:

"value"

Cập nhật giá trị

Để cập nhật một giá trị đã có trong bản đồ, bạn có thể sử dụng cú pháp sau:

map = %{:a => 1, 2 => :b}
new_map = %{ map | a: 25}
IO.puts(new_map[:a])

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

25

Khớp mẫu

Ngược lại với danh sách từ khóa, bản đồ rất hữu ích với đối sánh mẫu. Khi một bản đồ được sử dụng trong một mẫu, nó sẽ luôn khớp trên một tập con của giá trị đã cho -

%{:a => a} = %{:a => 1, 2 => :b}
IO.puts(a)

Chương trình trên tạo ra kết quả sau:

1

Điều này sẽ phù hợp a với 1. Và do đó, nó sẽ tạo ra đầu ra là1.

Như được hiển thị ở trên, một bản đồ phù hợp với điều kiện là các phím trong mẫu tồn tại trong bản đồ nhất định. Do đó, một bản đồ trống phù hợp với tất cả các bản đồ.

Các biến có thể được sử dụng khi truy cập, đối sánh và thêm các khóa bản đồ -

n = 1
map = %{n => :one}
%{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}

Mô-đun Bản đồ cung cấp một API rất giống với mô-đun Từ khoá với các chức năng thuận tiện để thao tác với bản đồ. Bạn có thể sử dụng các chức năng nhưMap.get, Map.delete, để thao tác trên bản đồ.

Bản đồ có khóa Atom

Bản đồ đi kèm với một vài thuộc tính thú vị. Khi tất cả các khóa trong bản đồ là nguyên tử, bạn có thể sử dụng cú pháp từ khóa để thuận tiện -

map = %{:a => 1, 2 => :b} 
IO.puts(map.a)

Một tính chất thú vị khác của bản đồ là chúng cung cấp cú pháp riêng để cập nhật và truy cập các khóa nguyên tử -

map = %{:a => 1, 2 => :b}
IO.puts(map.a)

Chương trình trên tạo ra kết quả sau:

1

Lưu ý rằng để truy cập các khóa nguyên tử theo cách này, nó phải tồn tại hoặc chương trình sẽ không hoạt động.

Trong Elixir, chúng tôi nhóm một số chức năng thành các mô-đun. Chúng tôi đã sử dụng các mô-đun khác nhau trong các chương trước như mô-đun Chuỗi, mô-đun Bitwise, mô-đun Tuple, v.v.

Để tạo mô-đun của riêng chúng tôi trong Elixir, chúng tôi sử dụng defmodulevĩ mô. Chúng tôi sử dụngdef macro để xác định các chức năng trong mô-đun đó -

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

Trong các phần tiếp theo, các ví dụ của chúng ta sẽ có kích thước dài hơn và có thể khó gõ tất cả chúng vào shell. Chúng ta cần học cách biên dịch mã Elixir và cả cách chạy các tập lệnh Elixir.

Tổng hợp

Luôn luôn thuận tiện khi ghi các mô-đun vào tệp để chúng có thể được biên dịch và sử dụng lại. Giả sử chúng ta có một tệp tên là math.ex với nội dung sau:

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

Chúng ta có thể biên dịch các tệp bằng lệnh -elixirc :

$ elixirc math.ex

Điều này sẽ tạo ra một tệp có tên Elixir.Math.beamchứa mã bytecode cho mô-đun đã xác định. Nếu chúng ta bắt đầuiexmột lần nữa, định nghĩa mô-đun của chúng tôi sẽ có sẵn (với điều kiện là iex được khởi động trong cùng thư mục chứa tệp bytecode). Ví dụ,

IO.puts(Math.sum(1, 2))

Chương trình trên sẽ tạo ra kết quả sau:

3

Chế độ tập lệnh

Ngoài phần mở rộng tệp Elixir .ex, Elixir cũng hỗ trợ .exscác tệp cho kịch bản. Elixir xử lý cả hai tệp theo cùng một cách, sự khác biệt duy nhất là ở vật kính..ex tệp có nghĩa là được biên dịch trong khi tệp .exs được sử dụng cho scripting. Khi được thực thi, cả hai phần mở rộng đều biên dịch và tải các mô-đun của chúng vào bộ nhớ, mặc dù chỉ.ex tệp ghi mã bytecode của chúng vào đĩa ở định dạng tệp .beam.

Ví dụ: nếu chúng tôi muốn chạy Math.sum trong cùng một tệp, chúng ta có thể sử dụng .exs theo cách sau:

Math.exs

defmodule Math do
   def sum(a, b) do
      a + b
   end
end
IO.puts(Math.sum(1, 2))

Chúng ta có thể chạy nó bằng lệnh Elixir -

$ elixir math.exs

Chương trình trên sẽ tạo ra kết quả sau:

3

Tệp sẽ được biên dịch trong bộ nhớ và thực thi, kết quả là in “3”. Không có tệp bytecode nào sẽ được tạo.

Mô-đun làm tổ

Các mô-đun có thể được lồng trong Elixir. Tính năng này của ngôn ngữ giúp chúng tôi tổ chức mã của mình theo cách tốt hơn. Để tạo các mô-đun lồng nhau, chúng tôi sử dụng cú pháp sau:

defmodule Foo do
   #Foo module code here
   defmodule Bar do
      #Bar module code here
   end
end

Ví dụ ở trên sẽ xác định hai mô-đun: FooFoo.Bar. Thứ hai có thể được truy cập nhưBar phía trong Foomiễn là chúng nằm trong cùng phạm vi từ vựng. Nếu sau này,Bar mô-đun được di chuyển ra ngoài định nghĩa mô-đun Foo, nó phải được tham chiếu bằng tên đầy đủ của nó (Foo.Bar) hoặc một bí danh phải được đặt bằng cách sử dụng chỉ thị bí danh được thảo luận trong chương bí danh.

Note- Trong Elixir, không cần xác định mô-đun Foo để xác định mô-đun Foo.Bar, vì ngôn ngữ này dịch tất cả các tên mô-đun sang nguyên tử. Bạn có thể xác định các mô-đun được thiết lập tùy ý mà không cần xác định bất kỳ mô-đun nào trong chuỗi. Ví dụ, bạn có thể xác địnhFoo.Bar.Baz không xác định Foo hoặc là Foo.Bar.

Để tạo điều kiện cho việc sử dụng lại phần mềm, Elixir cung cấp ba chỉ thị: alias, requireimport. Nó cũng cung cấp một macro được gọi là sử dụng được tóm tắt bên dưới:

# Alias the module so it can be called as Bar instead of Foo.Bar
alias Foo.Bar, as: Bar

# Ensure the module is compiled and available (usually for macros)
require Foo

# Import functions from Foo so they can be called without the `Foo.` prefix
import Foo

# Invokes the custom code defined in Foo as an extension point
use Foo

Bây giờ chúng ta hãy hiểu chi tiết về từng chỉ thị.

bí danh

Chỉ thị bí danh cho phép bạn thiết lập bí danh cho bất kỳ tên mô-đun nhất định nào. Ví dụ: nếu bạn muốn đặt một bí danh'Str' vào mô-đun Chuỗi, bạn có thể chỉ cần viết -

alias String, as: Str
IO.puts(Str.length("Hello"))

Chương trình trên tạo ra kết quả sau:

5

Một bí danh được cấp cho String mô-đun như Str. Bây giờ khi chúng ta gọi bất kỳ hàm nào bằng cách sử dụng ký tự Str, nó thực sự tham chiếu đếnStringmô-đun. Điều này rất hữu ích khi chúng tôi sử dụng tên mô-đun rất dài và muốn thay thế những tên mô-đun bằng tên ngắn hơn trong phạm vi hiện tại.

NOTE - Bí danh MUST bắt đầu bằng một chữ cái viết hoa.

Bí danh chỉ có giá trị trong lexical scope chúng được gọi vào. Ví dụ: nếu bạn có 2 mô-đun trong một tệp và tạo bí danh trong một trong các mô-đun, bí danh đó sẽ không thể truy cập được trong mô-đun thứ hai.

Nếu bạn đặt tên của một mô-đun dựng sẵn, như String hoặc Tuple, làm bí danh cho một số mô-đun khác, để truy cập vào mô-đun dựng sẵn, bạn sẽ cần thêm "Elixir.". Ví dụ,

alias List, as: String
#Now when we use String we are actually using List.
#To use the string module: 
IO.puts(Elixir.String.length("Hello"))

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

5

yêu cầu

Elixir cung cấp các macro như một cơ chế để lập trình meta (viết mã tạo ra mã).

Macro là các đoạn mã được thực thi và mở rộng tại thời điểm biên dịch. Điều này có nghĩa là, để sử dụng macro, chúng ta cần đảm bảo rằng mô-đun và việc triển khai của nó có sẵn trong quá trình biên dịch. Điều này được thực hiện vớirequire chỉ thị.

Integer.is_odd(3)

Khi chương trình trên được chạy, nó sẽ tạo ra kết quả sau:

** (CompileError) iex:1: you must require Integer before invoking the macro Integer.is_odd/1

Trong Elixir, Integer.is_odd được định nghĩa là một macro. Macro này có thể được sử dụng như một người bảo vệ. Điều này có nghĩa là, để gọiInteger.is_odd, chúng ta sẽ cần mô-đun Integer.

Sử dụng require Integer chức năng và chạy chương trình như hình dưới đây.

require Integer
Integer.is_odd(3)

Lúc này chương trình sẽ chạy và tạo ra kết quả là: true.

Nói chung, không cần có mô-đun trước khi sử dụng, ngoại trừ trường hợp chúng ta muốn sử dụng các macro có sẵn trong mô-đun đó. Cố gắng gọi một macro không được tải sẽ gây ra lỗi. Lưu ý rằng giống như chỉ thị bí danh, request cũng có phạm vi từ vựng . Chúng ta sẽ nói nhiều hơn về macro trong chương sau.

nhập khẩu

Chúng tôi sử dụng importchỉ thị để dễ dàng truy cập các chức năng hoặc macro từ các mô-đun khác mà không cần sử dụng tên đủ điều kiện. Ví dụ: nếu chúng ta muốn sử dụngduplicate từ mô-đun Danh sách nhiều lần, chúng ta có thể nhập nó một cách đơn giản.

import List, only: [duplicate: 2]

Trong trường hợp này, chúng tôi chỉ nhập hàm trùng lặp (với độ dài danh sách đối số là 2) từ Danh sách. Mặc du:only là tùy chọn, nên sử dụng nó để tránh nhập tất cả các chức năng của một mô-đun nhất định bên trong không gian tên. :except cũng có thể được cung cấp như một tùy chọn để nhập mọi thứ trong một mô-đun ngoại trừ danh sách các chức năng.

Các import chỉ thị cũng hỗ trợ :macros:functions được trao cho :only. Ví dụ: để nhập tất cả các macro, người dùng có thể viết:

import Integer, only: :macros

Lưu ý rằng nhập khẩu quá Lexically scopedgiống như chỉ thị yêu cầu và bí danh. Cũng lưu ý rằng'import'ing a module also 'require's it.

sử dụng

Mặc dù không phải là một chỉ thị, use là một macro có liên quan chặt chẽ đến requirecho phép bạn sử dụng một mô-đun trong ngữ cảnh hiện tại. Macro sử dụng thường được các nhà phát triển sử dụng để đưa chức năng bên ngoài vào phạm vi từ vựng hiện tại, thường là các mô-đun. Hãy để chúng tôi hiểu chỉ thị sử dụng thông qua một ví dụ -

defmodule Example do 
   use Feature, option: :value 
end

Sử dụng là một macro chuyển đổi ở trên thành -

defmodule Example do
   require Feature
   Feature.__using__(option: :value)
end

Các use Module đầu tiên yêu cầu mô-đun và sau đó gọi __using__macro trên Mô-đun. Elixir có khả năng lập trình siêu ứng tuyệt vời và nó có các macro để tạo mã tại thời điểm biên dịch. Macro _ _using__ được gọi trong trường hợp trên và mã được đưa vào ngữ cảnh cục bộ của chúng ta. Bối cảnh cục bộ là nơi sử dụng macro được gọi tại thời điểm biên dịch.

Hàm là một tập hợp các câu lệnh được tổ chức lại với nhau để thực hiện một nhiệm vụ cụ thể. Các hàm trong lập trình hoạt động hầu hết giống như hàm trong Toán học. Bạn cung cấp cho các hàm một số đầu vào, chúng tạo ra đầu ra dựa trên đầu vào được cung cấp.

Có 2 loại chức năng trong Elixir -

Chức năng ẩn danh

Các hàm được xác định bằng cách sử dụng fn..end constructlà các chức năng ẩn danh. Các hàm này đôi khi còn được gọi là lambdas. Chúng được sử dụng bằng cách gán chúng cho các tên biến.

Chức năng được đặt tên

Các hàm được xác định bằng cách sử dụng def keywordlà các hàm được đặt tên. Đây là các hàm gốc được cung cấp trong Elixir.

Chức năng ẩn danh

Đúng như tên của nó, một hàm ẩn danh không có tên. Chúng thường được chuyển cho các chức năng khác. Để xác định một hàm ẩn danh trong Elixir, chúng ta cầnfnendtừ khóa. Trong đó, chúng ta có thể xác định bất kỳ số lượng tham số và thân hàm nào được phân tách bằng->. Ví dụ,

sum = fn (a, b) -> a + b end
IO.puts(sum.(1, 5))

Khi chạy chương trình trên, được chạy, nó tạo ra kết quả sau:

6

Lưu ý rằng các hàm này không được gọi giống như các hàm đã đặt tên. Chúng ta có một '.'giữa tên hàm và các đối số của nó.

Sử dụng Toán tử Chụp

Chúng tôi cũng có thể xác định các chức năng này bằng cách sử dụng toán tử chụp. Đây là một phương pháp dễ dàng hơn để tạo các hàm. Bây giờ chúng ta sẽ xác định hàm tổng ở trên bằng cách sử dụng toán tử bắt,

sum = &(&1 + &2) 
IO.puts(sum.(1, 2))

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

3

Trong phiên bản viết tắt, các tham số của chúng tôi không được đặt tên nhưng có sẵn cho chúng tôi dưới dạng & 1, & 2, & 3, v.v.

Chức năng đối sánh mẫu

Đối sánh mẫu không chỉ giới hạn ở các biến và cấu trúc dữ liệu. Chúng ta có thể sử dụng đối sánh mẫu để làm cho các hàm của chúng ta đa hình. Ví dụ: chúng tôi sẽ khai báo một hàm có thể nhận 1 hoặc 2 đầu vào (trong một bộ tuple) và in chúng ra bảng điều khiển,

handle_result = fn
   {var1} -> IO.puts("#{var1} found in a tuple!")
   {var_2, var_3} -> IO.puts("#{var_2} and #{var_3} found!")
end
handle_result.({"Hey people"})
handle_result.({"Hello", "World"})

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

Hey people found in a tuple!
Hello and World found!

Các hàm được đặt tên

Chúng ta có thể định nghĩa các hàm bằng tên để có thể dễ dàng tham khảo sau này. Các hàm được đặt tên được định nghĩa trong một mô-đun bằng cách sử dụng từ khóa def. Các chức năng được đặt tên luôn được xác định trong một mô-đun. Để gọi các hàm được đặt tên, chúng ta cần tham chiếu chúng bằng cách sử dụng tên mô-đun của chúng.

Sau đây là cú pháp cho các hàm được đặt tên:

def function_name(argument_1, argument_2) do
   #code to be executed when function is called
end

Bây giờ chúng ta hãy xác định hàm tổng được đặt tên của chúng ta trong mô-đun Toán học.

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

IO.puts(Math.sum(5, 6))

Khi chạy chương trình trên, nó tạo ra kết quả sau:

11

Đối với các hàm 1-liner, có một ký hiệu viết tắt để xác định các hàm này, sử dụng do:. Ví dụ -

defmodule Math do
   def sum(a, b), do: a + b
end
IO.puts(Math.sum(5, 6))

Khi chạy chương trình trên, nó tạo ra kết quả sau:

11

Chức năng riêng tư

Elixir cung cấp cho chúng ta khả năng xác định các chức năng riêng tư có thể được truy cập từ bên trong mô-đun mà chúng được xác định. Để xác định một chức năng riêng tư, hãy sử dụngdefp thay vì def. Ví dụ,

defmodule Greeter do
   def hello(name), do: phrase <> name
   defp phrase, do: "Hello "
end

Greeter.hello("world")

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

Hello world

Nhưng nếu chúng ta chỉ cố gắng gọi hàm cụm từ một cách rõ ràng, bằng cách sử dụng Greeter.phrase() chức năng, nó sẽ gây ra lỗi.

Đối số mặc định

Nếu chúng ta muốn một giá trị mặc định cho một đối số, chúng ta sử dụng argument \\ value cú pháp -

defmodule Greeter do
   def hello(name, country \\ "en") do
      phrase(country) <> name
   end

   defp phrase("en"), do: "Hello, "
   defp phrase("es"), do: "Hola, "
end

Greeter.hello("Ayush", "en")
Greeter.hello("Ayush")
Greeter.hello("Ayush", "es")

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

Hello, Ayush
Hello, Ayush
Hola, Ayush

Đệ quy là một phương pháp mà giải pháp cho một vấn đề phụ thuộc vào các giải pháp cho các trường hợp nhỏ hơn của cùng một vấn đề. Hầu hết các ngôn ngữ lập trình máy tính đều hỗ trợ đệ quy bằng cách cho phép một hàm gọi chính nó trong văn bản chương trình.

Lý tưởng nhất là các hàm đệ quy có điều kiện kết thúc. Điều kiện kết thúc này, còn được gọi là trường hợp cơ sở ngừng nhập lại hàm và thêm các lệnh gọi hàm vào ngăn xếp. Đây là nơi mà lệnh gọi hàm đệ quy dừng lại. Chúng ta hãy xem xét ví dụ sau để hiểu thêm về hàm đệ quy.

defmodule Math do
   def fact(res, num) do
   if num === 1 do
      res
   else
      new_res = res * num
      fact(new_res, num-1)
      end
   end
end

IO.puts(Math.fact(1,5))

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

120

Vì vậy, trong hàm trên, Math.fact, chúng tôi đang tính giai thừa của một số. Lưu ý rằng chúng tôi đang gọi hàm trong chính nó. Bây giờ chúng ta hãy hiểu cách hoạt động của nó.

Chúng tôi đã cung cấp cho nó 1 và số có giai thừa mà chúng tôi muốn tính. Hàm kiểm tra xem số có phải là 1 hay không và trả về res nếu là 1(Ending condition). Nếu không, nó sẽ tạo một biến new_res và gán cho nó giá trị của res * num hiện tại trước đó. Nó trả về giá trị được trả về bởi thực tế cuộc gọi hàm của chúng ta (new_res, num-1) . Điều này lặp lại cho đến khi chúng ta nhận được num là 1. Khi điều đó xảy ra, chúng ta nhận được kết quả.

Chúng ta hãy xem xét một ví dụ khác, in từng phần tử của danh sách một. Để làm điều này, chúng tôi sẽ sử dụnghdtl chức năng của danh sách và so khớp mẫu trong các chức năng -

a = ["Hey", 100, 452, :true, "People"]
defmodule ListPrint do
   def print([]) do
   end
   def print([head | tail]) do 
      IO.puts(head)
      print(tail)
   end
end

ListPrint.print(a)

Hàm in đầu tiên được gọi khi chúng ta có một danh sách trống(ending condition). Nếu không, thì hàm in thứ hai sẽ được gọi sẽ chia danh sách làm 2 và gán phần tử đầu tiên của danh sách là đầu và phần tử còn lại của danh sách là đuôi. Sau đó phần đầu được in và chúng ta gọi lại hàm in với phần còn lại của danh sách, tức là phần đuôi. Khi chương trình trên được chạy, nó tạo ra kết quả sau:

Hey
100
452
true
People

Do tính bất biến, các vòng lặp trong Elixir (như trong bất kỳ ngôn ngữ lập trình chức năng nào) được viết khác với các ngôn ngữ mệnh lệnh. Ví dụ, trong một ngôn ngữ mệnh lệnh như C, bạn sẽ viết -

for(i = 0; i < 10; i++) {
   printf("%d", array[i]);
}

Trong ví dụ được đưa ra ở trên, chúng ta đang thay đổi cả mảng và biến i. Không thể gây đột biến trong Elixir. Thay vào đó, các ngôn ngữ chức năng dựa vào đệ quy: một hàm được gọi đệ quy cho đến khi đạt được điều kiện ngăn hành động đệ quy tiếp tục. Không có dữ liệu nào bị thay đổi trong quá trình này.

Bây giờ chúng ta hãy viết một vòng lặp đơn giản bằng cách sử dụng đệ quy in ra hello n lần.

defmodule Loop do
   def print_multiple_times(msg, n) when n <= 1 do
      IO.puts msg
   end

   def print_multiple_times(msg, n) do
      IO.puts msg
      print_multiple_times(msg, n - 1)
   end
end

Loop.print_multiple_times("Hello", 10)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello

Chúng tôi đã sử dụng các kỹ thuật đối sánh mẫu và đệ quy của hàm để thực hiện thành công một vòng lặp. Định nghĩa đệ quy rất khó hiểu nhưng việc chuyển đổi vòng lặp thành đệ quy thì rất dễ dàng.

Elixir cung cấp cho chúng tôi Enum module. Mô-đun này được sử dụng cho các cuộc gọi lặp đi lặp lại nhiều nhất vì việc sử dụng chúng dễ dàng hơn nhiều so với việc cố gắng tìm ra các định nghĩa đệ quy cho giống nhau. Chúng ta sẽ thảo luận về những điều đó trong chương tiếp theo. Các định nghĩa đệ quy của riêng bạn chỉ nên được sử dụng khi bạn không tìm thấy giải pháp bằng cách sử dụng mô-đun đó. Các chức năng đó được tối ưu hóa cuộc gọi đuôi và khá nhanh.

Enumerable là một đối tượng có thể được liệt kê. "Enumerated" có nghĩa là đếm từng thành viên của một tập hợp / tập hợp / danh mục (thường theo thứ tự, thường là theo tên).

Elixir cung cấp khái niệm về liệt kê và mô-đun Enum để làm việc với chúng. Các chức năng trong mô-đun Enum được giới hạn, như tên đã nói, liệt kê các giá trị trong cấu trúc dữ liệu. Ví dụ về cấu trúc dữ liệu có thể liệt kê là danh sách, tuple, bản đồ, v.v. Mô-đun Enum cung cấp cho chúng ta hơn 100 hàm để xử lý các enum. Chúng ta sẽ thảo luận một vài chức năng quan trọng trong chương này.

Tất cả các hàm này nhận một enumerable làm phần tử đầu tiên và một hàm làm phần tử thứ hai và hoạt động trên chúng. Các chức năng được mô tả dưới đây.

tất cả?

Khi chúng tôi sử dụng all? chức năng, toàn bộ tập hợp phải đánh giá là true nếu không sẽ trả về false. Ví dụ, để kiểm tra xem tất cả các phần tử trong danh sách có phải là số lẻ hay không.

res = Enum.all?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end) 
IO.puts(res)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

false

Điều này là do không phải tất cả các phần tử của danh sách này đều kỳ lạ.

bất kì?

Như tên cho thấy, hàm này trả về true nếu bất kỳ phần tử nào của tập hợp đánh giá là true. Ví dụ -

res = Enum.any?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end)
IO.puts(res)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

true

khúc gỗ

Hàm này chia bộ sưu tập của chúng ta thành các phần nhỏ có kích thước được cung cấp như đối số thứ hai. Ví dụ -

res = Enum.chunk([1, 2, 3, 4, 5, 6], 2)
IO.puts(res)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

[[1, 2], [3, 4], [5, 6]]

mỗi

Có thể cần phải lặp lại một bộ sưu tập mà không tạo ra giá trị mới, đối với trường hợp này, chúng tôi sử dụng each chức năng -

Enum.each(["Hello", "Every", "one"], fn(s) -> IO.puts(s) end)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

Hello
Every
one

bản đồ

Để áp dụng chức năng của chúng tôi cho từng mục và tạo ra một bộ sưu tập mới, chúng tôi sử dụng chức năng bản đồ. Nó là một trong những cấu trúc hữu ích nhất trong lập trình hàm vì nó khá diễn đạt và ngắn gọn. Chúng ta hãy xem xét một ví dụ để hiểu điều này. Chúng tôi sẽ nhân đôi các giá trị được lưu trữ trong một danh sách và lưu trữ nó trong một danh sách mớires -

res = Enum.map([2, 5, 3, 6], fn(a) -> a*2 end)
IO.puts(res)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

[4, 10, 6, 12]

giảm

Các reducehàm giúp chúng ta giảm số lượng của chúng ta xuống một giá trị duy nhất. Để làm điều này, chúng tôi cung cấp một bộ tích lũy tùy chọn (5 trong ví dụ này) để được chuyển vào hàm của chúng tôi; nếu không có bộ tích lũy nào được cung cấp, giá trị đầu tiên được sử dụng -

res = Enum.reduce([1, 2, 3, 4], 5, fn(x, accum) -> x + accum end)
IO.puts(res)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

15

Bộ tích lũy là giá trị ban đầu được chuyển đến fn. Từ cuộc gọi thứ hai trở đi, giá trị trả về từ cuộc gọi trước được chuyển dưới dạng tích lũy. Chúng tôi cũng có thể sử dụng giảm mà không cần tích lũy -

res = Enum.reduce([1, 2, 3, 4], fn(x, accum) -> x + accum end)
IO.puts(res)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

10

uniq

Hàm uniq loại bỏ các bản sao khỏi bộ sưu tập của chúng tôi và chỉ trả về tập hợp các phần tử trong bộ sưu tập. Ví dụ -

res = Enum.uniq([1, 2, 2, 3, 3, 3, 4, 4, 4, 4])
IO.puts(res)

Khi chạy chương trình trên, nó tạo ra kết quả sau:

[1, 2, 3, 4]

Đánh giá háo hức

Tất cả các chức năng trong mô-đun Enum đều háo hức. Nhiều hàm mong đợi có thể liệt kê và trả về một danh sách. Điều này có nghĩa là khi thực hiện nhiều thao tác với Enum, mỗi thao tác sẽ tạo ra một danh sách trung gian cho đến khi chúng ta đạt được kết quả. Chúng ta hãy xem xét ví dụ sau để hiểu điều này -

odd? = &(odd? = &(rem(&1, 2) != 0) 
res = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum 
IO.puts(res)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

7500000000

Ví dụ trên có một đường dẫn hoạt động. Chúng tôi bắt đầu với một phạm vi và sau đó nhân từng phần tử trong phạm vi với 3. Thao tác đầu tiên này bây giờ sẽ tạo và trả về một danh sách với 100_000 mục. Sau đó, chúng tôi giữ tất cả các phần tử lẻ khỏi danh sách, tạo một danh sách mới, bây giờ với 50_000 mục, và sau đó chúng tôi tổng hợp tất cả các mục nhập.

Các |> biểu tượng được sử dụng trong đoạn mã trên là pipe operator: nó chỉ đơn giản là lấy đầu ra từ biểu thức ở phía bên trái của nó và chuyển nó làm đối số đầu tiên cho lời gọi hàm ở phía bên phải của nó. Nó tương tự như Unix | nhà điều hành. Mục đích của nó là làm nổi bật luồng dữ liệu được chuyển đổi bởi một loạt các chức năng.

Không có pipe toán tử, mã trông phức tạp -

Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))

Chúng tôi có nhiều chức năng khác, tuy nhiên, chỉ có một số chức năng quan trọng được mô tả ở đây.

Nhiều hàm mong đợi một giá trị liệt kê và trả về listtrở lại. Nó có nghĩa là, trong khi thực hiện nhiều thao tác với Enum, mỗi thao tác sẽ tạo ra một danh sách trung gian cho đến khi chúng ta đạt được kết quả.

Các luồng hỗ trợ các hoạt động lười biếng thay vì các hoạt động háo hức bởi enums. Nói ngắn gọn,streams are lazy, composable enumerables. Điều này có nghĩa là Luồng không thực hiện một hoạt động trừ khi nó thực sự cần thiết. Chúng ta hãy xem xét một ví dụ để hiểu điều này -

odd? = &(rem(&1, 2) != 0)
res = 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
IO.puts(res)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

7500000000

Trong ví dụ ở trên, 1..100_000 |> Stream.map(&(&1 * 3))trả về kiểu dữ liệu, một luồng thực, đại diện cho tính toán bản đồ trên phạm vi 1..100_000. Nó vẫn chưa đánh giá đại diện này. Thay vì tạo danh sách trung gian, các luồng xây dựng một loạt các phép tính chỉ được gọi khi chúng ta chuyển luồng cơ bản đến mô-đun Enum. Luồng rất hữu ích khi làm việc với các tập hợp lớn, có thể là vô hạn.

Luồng và enum có nhiều chức năng chung. Các luồng chủ yếu cung cấp các chức năng tương tự được cung cấp bởi mô-đun Enum đã tạo Danh sách dưới dạng giá trị trả về của chúng sau khi thực hiện tính toán trên các liệt kê đầu vào. Một số trong số chúng được liệt kê trong bảng sau:

Sr.No. Chức năng và mô tả của nó
1

chunk(enum, n, step, leftover \\ nil)

Truyền trực tuyến kiểu liệt kê theo từng phần, mỗi phần chứa n mục, trong đó mỗi phần mới bắt đầu các phần tử bước vào kiểu liệt kê.

2

concat(enumerables)

Tạo một luồng liệt kê từng có thể liệt kê trong một có thể liệt kê.

3

each(enum, fun)

Thực hiện chức năng đã cho cho từng mục.

4

filter(enum, fun)

Tạo luồng lọc các phần tử theo chức năng đã cho trên phép liệt kê.

5

map(enum, fun)

Tạo một luồng sẽ áp dụng chức năng đã cho khi liệt kê.

6

drop(enum, n)

Lười biếng làm rơi n mục tiếp theo từ bảng liệt kê.

Cấu trúc là phần mở rộng được xây dựng trên bản đồ cung cấp kiểm tra thời gian biên dịch và các giá trị mặc định.

Xác định cấu trúc

Để xác định một cấu trúc, cấu trúc defstruct được sử dụng:

defmodule User do
   defstruct name: "John", age: 27
end

Danh sách từ khóa được sử dụng với defstruct xác định các trường mà struct sẽ có cùng với các giá trị mặc định của chúng. Các cấu trúc lấy tên của mô-đun mà chúng được định nghĩa. Trong ví dụ được đưa ra ở trên, chúng tôi đã xác định một cấu trúc có tên Người dùng. Bây giờ chúng tôi có thể tạo cấu trúc Người dùng bằng cách sử dụng cú pháp tương tự như cú pháp được sử dụng để tạo bản đồ -

new_john = %User{})
ayush = %User{name: "Ayush", age: 20}
megan = %User{name: "Megan"})

Đoạn mã trên sẽ tạo ra ba cấu trúc khác nhau với các giá trị:

%User{age: 27, name: "John"}
%User{age: 20, name: "Ayush"}
%User{age: 27, name: "Megan"}

Các cấu trúc cung cấp đảm bảo thời gian biên dịch rằng chỉ các trường (và tất cả chúng) được xác định thông qua defstruct mới được phép tồn tại trong một cấu trúc. Vì vậy, bạn không thể xác định các trường của riêng mình khi bạn đã tạo cấu trúc trong mô-đun.

Truy cập và cập nhật cấu trúc

Khi thảo luận về bản đồ, chúng tôi đã chỉ ra cách chúng tôi có thể truy cập và cập nhật các trường của bản đồ. Các kỹ thuật tương tự (và cùng một cú pháp) cũng áp dụng cho các cấu trúc. Ví dụ: nếu chúng ta muốn cập nhật người dùng mà chúng ta đã tạo trong ví dụ trước đó, thì -

defmodule User do
   defstruct name: "John", age: 27
end
john = %User{}
#john right now is: %User{age: 27, name: "John"}

#To access name and age of John, 
IO.puts(john.name)
IO.puts(john.age)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

John
27

Để cập nhật một giá trị trong một cấu trúc, chúng tôi sẽ sử dụng lại quy trình tương tự mà chúng tôi đã sử dụng trong chương bản đồ,

meg = %{john | name: "Meg"}

Các cấu trúc cũng có thể được sử dụng trong đối sánh mẫu, cả để đối sánh giá trị của các khóa cụ thể cũng như để đảm bảo rằng giá trị đối sánh là cấu trúc cùng loại với giá trị đã khớp.

Các giao thức là một cơ chế để đạt được tính đa hình trong Elixir. Điều phối trên một giao thức có sẵn cho bất kỳ loại dữ liệu nào miễn là nó thực hiện giao thức.

Chúng ta hãy xem xét một ví dụ về việc sử dụng các giao thức. Chúng tôi đã sử dụng một hàm có tên làto_stringtrong các chương trước để chuyển đổi từ kiểu khác sang kiểu chuỗi. Đây thực sự là một giao thức. Nó hoạt động theo đầu vào được đưa ra mà không tạo ra lỗi. Điều này có vẻ giống như chúng ta đang thảo luận về các chức năng đối sánh mẫu, nhưng khi chúng ta tiếp tục tiến xa hơn, nó hóa ra khác.

Hãy xem xét ví dụ sau để hiểu thêm về cơ chế giao thức.

Hãy để chúng tôi tạo một giao thức sẽ hiển thị nếu đầu vào đã cho trống hoặc không. Chúng tôi sẽ gọi đây là giao thứcblank?.

Xác định một giao thức

Chúng ta có thể xác định một giao thức trong Elixir theo cách sau:

defprotocol Blank do
   def blank?(data)
end

Như bạn thấy, chúng ta không cần xác định một phần thân cho hàm. Nếu bạn đã quen thuộc với các giao diện trong các ngôn ngữ lập trình khác, bạn có thể nghĩ về một Giao thức về cơ bản giống như một thứ.

Vì vậy, Nghị định thư này nói rằng bất kỳ thứ gì triển khai nó phải có empty?hàm, mặc dù tùy thuộc vào người triển khai về cách hàm phản hồi. Với giao thức được xác định, chúng ta hãy hiểu cách thêm một số triển khai.

Thực hiện một giao thức

Vì chúng ta đã xác định một giao thức, nên bây giờ chúng ta cần cho nó biết cách xử lý các đầu vào khác nhau mà nó có thể nhận được. Hãy để chúng tôi xây dựng trên ví dụ chúng tôi đã thực hiện trước đó. Chúng tôi sẽ triển khai giao thức trống cho danh sách, bản đồ và chuỗi. Điều này sẽ hiển thị nếu thứ chúng tôi đã thông qua là trống hay không.

#Defining the protocol
defprotocol Blank do
   def blank?(data)
end

#Implementing the protocol for lists
defimpl Blank, for: List do
   def blank?([]), do: true
   def blank?(_), do: false
end

#Implementing the protocol for strings
defimpl Blank, for: BitString do
   def blank?(""), do: true
   def blank?(_), do: false
end

#Implementing the protocol for maps
defimpl Blank, for: Map do
   def blank?(map), do: map_size(map) == 0
end

IO.puts(Blank.blank? [])
IO.puts(Blank.blank? [:true, "Hello"])
IO.puts(Blank.blank? "")
IO.puts(Blank.blank? "Hi")

Bạn có thể triển khai Giao thức của mình cho nhiều hoặc ít loại tùy thích, bất cứ điều gì có ý nghĩa đối với việc sử dụng Giao thức của bạn. Đây là một trường hợp sử dụng khá cơ bản của các giao thức. Khi chương trình trên được chạy, nó tạo ra kết quả sau:

true
false
true
false

Note - Nếu bạn sử dụng điều này cho bất kỳ kiểu nào khác với những kiểu bạn đã xác định giao thức, nó sẽ tạo ra lỗi.

Tệp IO là một phần không thể thiếu của bất kỳ ngôn ngữ lập trình nào vì nó cho phép ngôn ngữ tương tác với các tệp trên hệ thống tệp. Trong chương này, chúng ta sẽ thảo luận về hai mô-đun - Path và File.

Mô-đun đường dẫn

Các pathmô-đun là một mô-đun rất nhỏ có thể được coi là một mô-đun trợ giúp cho các hoạt động của hệ thống tệp. Phần lớn các hàm trong mô-đun Tệp mong đợi các đường dẫn là đối số. Thông thường nhất, những đường dẫn đó sẽ là các mã nhị phân thông thường. Mô-đun Path cung cấp các phương tiện để làm việc với các đường dẫn như vậy. Việc sử dụng các chức năng từ mô-đun Đường dẫn thay vì chỉ thao tác với các tệp nhị phân được ưu tiên vì mô-đun Đường dẫn xử lý các hệ điều hành khác nhau một cách minh bạch. Có thể thấy rằng Elixir sẽ tự động chuyển đổi dấu gạch chéo (/) thành dấu gạch chéo ngược (\) trên Windows khi thực hiện các thao tác với tệp.

Chúng ta hãy xem xét ví dụ sau để hiểu thêm về mô-đun Đường dẫn -

IO.puts(Path.join("foo", "bar"))

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

foo/bar

Có rất nhiều phương thức mà mô-đun đường dẫn cung cấp. Bạn có thể xem các phương pháp khác nhau tại đây . Các phương pháp này thường được sử dụng nếu bạn đang thực hiện nhiều thao tác thao tác với tệp.

Mô-đun tệp

Mô-đun tệp chứa các chức năng cho phép chúng tôi mở tệp dưới dạng thiết bị IO. Theo mặc định, các tệp được mở ở chế độ nhị phân, điều này yêu cầu các nhà phát triển sử dụngIO.binreadIO.binwritecác chức năng từ mô-đun IO. Hãy để chúng tôi tạo một tệp có tênnewfile và ghi một số dữ liệu vào nó.

{:ok, file} = File.read("newfile", [:write]) 
# Pattern matching to store returned stream
IO.binwrite(file, "This will be written to the file")

Nếu bạn mở tệp chúng tôi vừa viết vào, nội dung sẽ được hiển thị theo cách sau:

This will be written to the file

Bây giờ chúng ta hãy hiểu cách sử dụng mô-đun tệp.

Mở một tệp

Để mở một tệp, chúng ta có thể sử dụng bất kỳ một trong 2 chức năng sau:

{:ok, file} = File.open("newfile")
file = File.open!("newfile")

Bây giờ chúng ta hãy hiểu sự khác biệt giữa File.open chức năng và File.open!() chức năng.

  • Các File.openhàm luôn trả về một bộ giá trị. Nếu tệp được mở thành công, nó trả về giá trị đầu tiên trong bộ giá trị là:okvà giá trị thứ hai là nghĩa đen của kiểu io_device. Nếu lỗi được gây ra, nó sẽ trả về một bộ giá trị đầu tiên là:error và giá trị thứ hai là lý do.

  • Các File.open!() mặt khác, hàm sẽ trả về io_devicenếu tệp được mở thành công khác, nó sẽ phát sinh lỗi. LƯU Ý: Đây là mẫu theo sau trong tất cả các chức năng của mô-đun tệp mà chúng ta sẽ thảo luận.

Chúng tôi cũng có thể chỉ định các chế độ mà chúng tôi muốn mở tệp này. Để mở tệp ở dạng chỉ đọc và ở chế độ mã hóa utf-8, chúng tôi sử dụng mã sau:

file = File.open!("newfile", [:read, :utf8])

Ghi vào tệp

Chúng tôi có hai cách để ghi vào tệp. Hãy để chúng tôi xem cái đầu tiên sử dụng chức năng ghi từ mô-đun Tệp.

File.write("newfile", "Hello")

Nhưng điều này không nên được sử dụng nếu bạn đang ghi nhiều lần vào cùng một tệp. Mỗi khi hàm này được gọi, một bộ mô tả tệp sẽ được mở và một quá trình mới được tạo ra để ghi vào tệp. Nếu bạn đang ghi nhiều lần trong một vòng lặp, hãy mở tệp quaFile.openvà ghi vào nó bằng các phương thức trong mô-đun IO. Chúng ta hãy xem xét một ví dụ để hiểu giống nhau -

#Open the file in read, write and utf8 modes. 
file = File.open!("newfile_2", [:read, :utf8, :write])

#Write to this "io_device" using standard IO functions
IO.puts(file, "Random text")

Bạn có thể sử dụng các phương pháp mô-đun IO khác như IO.writeIO.binwrite để ghi vào các tệp được mở dưới dạng io_device.

Đọc từ một tệp

Chúng tôi có hai cách để đọc từ tệp. Hãy để chúng tôi xem cái đầu tiên sử dụng chức năng đọc từ mô-đun Tệp.

IO.puts(File.read("newfile"))

Khi chạy mã này, bạn sẽ nhận được một bộ giá trị với phần tử đầu tiên là :ok và cái thứ hai là nội dung của newfile

Chúng tôi cũng có thể sử dụng File.read! chức năng chỉ lấy nội dung của các tệp trả lại cho chúng tôi.

Đóng một tệp đang mở

Bất cứ khi nào bạn mở tệp bằng chức năng File.open, sau khi sử dụng xong, bạn nên đóng tệp bằng File.close chức năng -

File.close(file)

Trong Elixir, tất cả mã chạy bên trong các quy trình. Các quy trình được cách ly với nhau, chạy đồng thời với nhau và giao tiếp thông qua truyền tin nhắn. Không nên nhầm lẫn các quy trình của Elixir với các quy trình của hệ điều hành. Các quy trình trong Elixir cực kỳ nhẹ về bộ nhớ và CPU (không giống như các luồng trong nhiều ngôn ngữ lập trình khác). Do đó, không có gì lạ khi có hàng chục hoặc thậm chí hàng trăm nghìn tiến trình chạy đồng thời.

Trong chương này, chúng ta sẽ tìm hiểu về các cấu trúc cơ bản để tạo ra các quy trình mới, cũng như gửi và nhận thông điệp giữa các quy trình khác nhau.

Chức năng đẻ trứng

Cách dễ nhất để tạo một quy trình mới là sử dụng spawnchức năng. Cácspawnchấp nhận một chức năng sẽ được chạy trong quy trình mới. Ví dụ -

pid = spawn(fn -> 2 * 2 end)
Process.alive?(pid)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

false

Giá trị trả về của hàm sinh sản là một PID. Đây là số nhận dạng duy nhất cho quy trình và vì vậy nếu bạn chạy mã phía trên PID của mình, mã sẽ khác. Như bạn có thể thấy trong ví dụ này, quá trình đã chết khi chúng tôi kiểm tra xem nó còn sống hay không. Điều này là do quá trình sẽ thoát ngay sau khi nó chạy xong chức năng đã cho.

Như đã đề cập, tất cả các mã Elixir đều chạy bên trong các quy trình. Nếu bạn chạy chức năng tự, bạn sẽ thấy PID cho phiên hiện tại của bạn -

pid = self
 
Process.alive?(pid)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

true

Thông qua

Chúng tôi có thể gửi tin nhắn đến một quy trình với send và nhận chúng với receive. Hãy để chúng tôi chuyển một tin nhắn đến quy trình hiện tại và nhận nó trên cùng một quy trình.

send(self(), {:hello, "Hi people"})

receive do
   {:hello, msg} -> IO.puts(msg)
   {:another_case, msg} -> IO.puts("This one won't match!")
end

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

Hi people

Chúng tôi đã gửi một thông báo đến quy trình hiện tại bằng cách sử dụng chức năng gửi và chuyển nó đến PID của chính mình. Sau đó, chúng tôi xử lý tin nhắn đến bằng cách sử dụngreceive chức năng.

Khi một tin nhắn được gửi đến một quy trình, tin nhắn đó sẽ được lưu trữ trong process mailbox. Khối nhận đi qua hộp thư của quy trình hiện tại để tìm kiếm một thư phù hợp với bất kỳ mẫu nào đã cho. Khối nhận hỗ trợ bảo vệ và nhiều mệnh đề, chẳng hạn như trường hợp.

Nếu không có thư nào trong hộp thư khớp với bất kỳ mẫu nào, quy trình hiện tại sẽ đợi cho đến khi có thư phù hợp. Thời gian chờ cũng có thể được chỉ định. Ví dụ,

receive do
   {:hello, msg}  -> msg
after
   1_000 -> "nothing after 1s"
end

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

nothing after 1s

NOTE - Thời gian chờ bằng 0 có thể được đưa ra khi bạn đã mong đợi thư ở trong hộp thư.

Liên kết

Hình thức sinh sản phổ biến nhất trong Elixir thực sự là thông qua spawn_linkchức năng. Trước khi xem một ví dụ với spawn_link, hãy để chúng tôi hiểu điều gì sẽ xảy ra khi một quy trình không thành công.

spawn fn -> raise "oops" end

Khi chương trình trên được chạy, nó tạo ra lỗi sau:

[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
   :erlang.apply/2

Nó ghi lại lỗi nhưng quá trình sinh sản vẫn đang chạy. Điều này là do các quy trình bị cô lập. Nếu chúng ta muốn sự thất bại trong một quá trình truyền sang một quá trình khác, chúng ta cần liên kết chúng. Điều này có thể được thực hiện vớispawn_linkchức năng. Chúng ta hãy xem xét một ví dụ để hiểu giống nhau -

spawn_link fn -> raise "oops" end

Khi chương trình trên được chạy, nó tạo ra lỗi sau:

** (EXIT from #PID<0.41.0>) an exception was raised:
   ** (RuntimeError) oops
      :erlang.apply/2

Nếu bạn đang chạy cái này trong iexshell thì shell xử lý lỗi này không thoát. Nhưng nếu bạn chạy trước bằng cách tạo một tệp script và sau đó sử dụngelixir <file-name>.exs, quy trình gốc cũng sẽ bị hạ xuống do lỗi này.

Các quy trình và liên kết đóng vai trò quan trọng khi xây dựng hệ thống chịu lỗi. Trong các ứng dụng Elixir, chúng tôi thường liên kết các quy trình của mình với người giám sát, người này sẽ phát hiện khi nào một quy trình chết và bắt đầu một quy trình mới ở vị trí của nó. Điều này chỉ có thể thực hiện được vì các quy trình được tách biệt và không chia sẻ bất kỳ thứ gì theo mặc định. Và vì các quá trình được tách biệt, không có cách nào mà một quá trình bị lỗi sẽ làm hỏng hoặc làm hỏng trạng thái của một quá trình khác. Trong khi các ngôn ngữ khác sẽ yêu cầu chúng tôi bắt / xử lý các ngoại lệ; trong Elixir, chúng tôi thực sự ổn khi để các quy trình không thành công vì chúng tôi mong đợi người giám sát khởi động lại hệ thống của chúng tôi đúng cách.

Tiểu bang

Ví dụ: nếu bạn đang xây dựng một ứng dụng yêu cầu trạng thái để giữ cấu hình ứng dụng của bạn hoặc bạn cần phân tích cú pháp một tệp và giữ nó trong bộ nhớ, bạn sẽ lưu trữ nó ở đâu? Chức năng quy trình của Elixir có thể hữu ích khi thực hiện những việc như vậy.

Chúng ta có thể viết các quy trình lặp lại vô hạn, duy trì trạng thái và gửi và nhận tin nhắn. Ví dụ: chúng ta hãy viết một mô-đun khởi động các quy trình mới hoạt động như một kho lưu trữ khóa-giá trị trong một tệp có tênkv.exs.

defmodule KV do
   def start_link do
      Task.start_link(fn -> loop(%{}) end)
   end

   defp loop(map) do
      receive do
         {:get, key, caller} ->
         send caller, Map.get(map, key)
         loop(map)
         {:put, key, value} ->
         loop(Map.put(map, key, value))
      end
   end
end

Lưu ý rằng start_link hàm bắt đầu một quá trình mới chạy loop, bắt đầu với một bản đồ trống. Cácloopsau đó chức năng chờ tin nhắn và thực hiện hành động thích hợp cho từng tin nhắn. Trong trường hợp của một:gettin nhắn, nó sẽ gửi một tin nhắn trở lại người gọi và các cuộc gọi lặp lại, để chờ một tin nhắn mới. Trong khi:put thông điệp thực sự gọi loop với một phiên bản mới của bản đồ, với khóa và giá trị đã cho được lưu trữ.

Bây giờ chúng ta hãy chạy phần sau -

iex kv.exs

Bây giờ bạn nên ở trong iexvỏ sò. Để kiểm tra mô-đun của chúng tôi, hãy thử cách sau:

{:ok, pid} = KV.start_link

# pid now has the pid of our new process that is being 
# used to get and store key value pairs 

# Send a KV pair :hello, "Hello" to the process
send pid, {:put, :hello, "Hello"}

# Ask for the key :hello
send pid, {:get, :hello, self()}

# Print all the received messages on the current process.
flush()

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

"Hello"

Trong chương này, chúng ta sẽ khám phá các dấu hiệu, các cơ chế được cung cấp bởi ngôn ngữ để làm việc với các biểu diễn dạng văn bản. Các dấu hiệu bắt đầu bằng ký tự dấu ngã (~), theo sau là một chữ cái (xác định dấu hiệu) và sau đó là dấu phân cách; tùy chọn, các bổ ngữ có thể được thêm vào sau dấu phân cách cuối cùng.

Regex

Regexes trong Elixir là dấu hiệu. Chúng ta đã thấy việc sử dụng chúng trong chương Chuỗi. Một lần nữa chúng ta hãy lấy một ví dụ để xem chúng ta có thể sử dụng regex trong Elixir như thế nào.

# A regular expression that matches strings which contain "foo" or
# "bar":
regex = ~r/foo|bar/
IO.puts("foo" =~ regex)
IO.puts("baz" =~ regex)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

true
false

Các dấu hiệu hỗ trợ 8 dấu phân cách khác nhau -

~r/hello/
~r|hello|
~r"hello"
~r'hello'
~r(hello)
~r[hello]
~r{hello}
~r<hello>

Lý do đằng sau việc hỗ trợ các dấu phân cách khác nhau là các dấu phân cách khác nhau có thể phù hợp hơn cho các dấu hiệu khác nhau. Ví dụ: sử dụng dấu ngoặc đơn cho biểu thức chính quy có thể là một lựa chọn khó hiểu vì chúng có thể bị lẫn với dấu ngoặc đơn bên trong regex. Tuy nhiên, dấu ngoặc đơn có thể hữu ích cho các dấu hiệu khác, như chúng ta sẽ thấy trong phần tiếp theo.

Elixir hỗ trợ các regex tương thích Perl và cũng hỗ trợ các công cụ sửa đổi. Bạn có thể đọc thêm về việc sử dụng regexes tại đây .

Chuỗi, danh sách Char và danh sách Word

Ngoài regexes, Elixir có thêm 3 dấu hiệu sẵn có. Hãy để chúng tôi xem xét các dấu hiệu.

Dây

Dấu hiệu ~ s được sử dụng để tạo chuỗi, giống như dấu ngoặc kép. Dấu hiệu ~ s rất hữu ích, ví dụ, khi một chuỗi chứa cả dấu ngoặc kép và dấu nháy đơn -

new_string = ~s(this is a string with "double" quotes, not 'single' ones)
IO.puts(new_string)

Dấu hiệu này tạo ra các chuỗi. Khi chương trình trên được chạy, nó tạo ra kết quả sau:

"this is a string with \"double\" quotes, not 'single' ones"

Danh sách Char

Dấu hiệu ~ c được sử dụng để tạo danh sách char -

new_char_list = ~c(this is a char list containing 'single quotes')
IO.puts(new_char_list)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

this is a char list containing 'single quotes'

Danh sách các từ

Dấu hiệu ~ w được sử dụng để tạo danh sách các từ (các từ chỉ là các chuỗi thông thường). Bên trong dấu hiệu ~ w, các từ được phân tách bằng khoảng trắng.

new_word_list = ~w(foo bar bat)
IO.puts(new_word_list)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

foobarbat

Dấu hiệu ~ w cũng chấp nhận c, sa bổ ngữ (cho danh sách char, chuỗi và nguyên tử, tương ứng), chỉ định kiểu dữ liệu của các phần tử của danh sách kết quả -

new_atom_list = ~w(foo bar bat)a
IO.puts(new_atom_list)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

[:foo, :bar, :bat]

Nội suy và thoát khỏi dấu hiệu

Bên cạnh các ký hiệu viết thường, Elixir hỗ trợ các ký hiệu viết hoa để xử lý các ký tự thoát và nội suy. Trong khi cả hai chuỗi ~ s và ~ S sẽ trả về chuỗi, chuỗi thứ nhất cho phép mã thoát và nội suy trong khi chuỗi thứ hai thì không. Chúng ta hãy xem xét một ví dụ để hiểu điều này -

~s(String with escape codes \x26 #{"inter" <> "polation"})
# "String with escape codes & interpolation"
~S(String without escape codes \x26 without #{interpolation})
# "String without escape codes \\x26 without \#{interpolation}"

Dấu hiệu tùy chỉnh

Chúng tôi có thể dễ dàng tạo các dấu hiệu tùy chỉnh của riêng mình. Trong ví dụ này, chúng ta sẽ tạo một dấu hiệu để chuyển một chuỗi thành chữ hoa.

defmodule CustomSigil do
   def sigil_u(string, []), do: String.upcase(string)
end

import CustomSigil

IO.puts(~u/tutorials point/)

Khi chúng tôi chạy đoạn mã trên, nó tạo ra kết quả sau:

TUTORIALS POINT

Đầu tiên, chúng tôi xác định một mô-đun được gọi là CustomSigil và trong mô-đun đó, chúng tôi đã tạo một hàm gọi là sigil_u. Vì không có dấu hiệu ~ u hiện có trong không gian dấu hiệu hiện có, chúng tôi sẽ sử dụng nó. _U chỉ ra rằng chúng ta muốn sử dụng u làm ký tự sau dấu ngã. Định nghĩa hàm phải có hai đối số, một đầu vào và một danh sách.

Danh sách hiểu là đường cú pháp để lặp qua các bảng liệt kê trong Elixir. Trong chương này, chúng ta sẽ sử dụng các cách hiểu để lặp lại và tạo.

Khái niệm cơ bản

Khi chúng ta xem xét mô-đun Enum trong chương liệt kê, chúng ta đã bắt gặp hàm bản đồ.

Enum.map(1..3, &(&1 * 2))

Trong ví dụ này, chúng ta sẽ truyền một hàm làm đối số thứ hai. Mỗi mục trong phạm vi sẽ được chuyển vào hàm và sau đó một danh sách mới sẽ được trả về chứa các giá trị mới.

Ánh xạ, lọc và chuyển đổi là những hành động rất phổ biến trong Elixir và do đó, có một cách hơi khác để đạt được kết quả giống như ví dụ trước -

for n <- 1..3, do: n * 2

Khi chúng tôi chạy đoạn mã trên, nó tạo ra kết quả sau:

[2, 4, 6]

Ví dụ thứ hai là khả năng hiểu và như bạn có thể thấy, nó chỉ đơn giản là đường cú pháp cho những gì bạn cũng có thể đạt được nếu bạn sử dụng Enum.mapchức năng. Tuy nhiên, không có lợi ích thực sự nào khi sử dụng khả năng hiểu một hàm từ mô-đun Enum về mặt hiệu suất.

Tổng hợp không giới hạn ở danh sách nhưng có thể được sử dụng với tất cả các kiểu liệt kê.

Bộ lọc

Bạn có thể coi bộ lọc như một loại bảo vệ để hiểu rõ. Khi một giá trị được lọc trả vềfalse hoặc là nilnó bị loại khỏi danh sách cuối cùng. Hãy để chúng tôi lặp qua một phạm vi và chỉ lo lắng về số chẵn. Chúng tôi sẽ sử dụngis_even từ mô-đun Integer để kiểm tra xem một giá trị có chẵn hay không.

import Integer
IO.puts(for x <- 1..10, is_even(x), do: x)

Khi đoạn mã trên được chạy, nó tạo ra kết quả sau:

[2, 4, 6, 8, 10]

Chúng ta cũng có thể sử dụng nhiều bộ lọc trong cùng một cách hiểu. Thêm bộ lọc khác mà bạn muốn sauis_even bộ lọc được phân tách bằng dấu phẩy.

: vào Option

Trong các ví dụ trên, tất cả các phần hiểu được đều trả về danh sách là kết quả của chúng. Tuy nhiên, kết quả của việc hiểu có thể được chèn vào các cấu trúc dữ liệu khác nhau bằng cách chuyển:into tùy chọn để hiểu.

Ví dụ, một bitstring Trình tạo có thể được sử dụng với tùy chọn: into để dễ dàng loại bỏ tất cả các khoảng trắng trong một chuỗi -

IO.puts(for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>)

Khi đoạn mã trên được chạy, nó tạo ra kết quả sau:

helloworld

Đoạn mã trên xóa tất cả các khoảng trắng khỏi chuỗi bằng cách sử dụng c != ?\s lọc và sau đó sử dụng tùy chọn: into, nó đặt tất cả các ký tự trả về trong một chuỗi.

Elixir là một ngôn ngữ được nhập động, vì vậy tất cả các kiểu trong Elixir đều được suy ra bởi thời gian chạy. Tuy nhiên, Elixir đi kèm với các tệp kiểu chữ, là một ký hiệu được sử dụng chodeclaring custom data types and declaring typed function signatures (specifications).

Thông số kỹ thuật chức năng (thông số kỹ thuật)

Theo mặc định, Elixir cung cấp một số kiểu cơ bản, chẳng hạn như số nguyên hoặc pid, và cả các kiểu phức tạp: ví dụ: roundhàm, làm tròn một số thực đến số nguyên gần nhất của nó, nhận một số làm đối số (số nguyên hoặc số thực) và trả về một số nguyên. Trong tài liệu liên quan , chữ ký đánh máy tròn được viết là -

round(number) :: integer

Mô tả ở trên ngụ ý rằng hàm bên trái nhận làm đối số là những gì được chỉ định trong dấu ngoặc và trả về những gì ở bên phải của ::, tức là Số nguyên. Thông số chức năng được viết với@specchỉ thị, được đặt ngay trước định nghĩa hàm. Hàm vòng có thể được viết là -

@spec round(number) :: integer
def round(number), do: # Function implementation
...

Các stylespec cũng hỗ trợ các kiểu phức tạp, ví dụ: nếu bạn muốn trả về một danh sách các số nguyên, thì bạn có thể sử dụng [Integer]

Các loại tùy chỉnh

Trong khi Elixir cung cấp rất nhiều kiểu sẵn có hữu ích, rất tiện lợi để xác định các kiểu tùy chỉnh khi thích hợp. Điều này có thể được thực hiện khi xác định các mô-đun thông qua chỉ thị @type. Chúng ta hãy xem xét một ví dụ để hiểu giống nhau -

defmodule FunnyCalculator do
   @type number_with_joke :: {number, String.t}

   @spec add(number, number) :: number_with_joke
   def add(x, y), do: {x + y, "You need a calculator to do that?"}

   @spec multiply(number, number) :: number_with_joke
   def multiply(x, y), do: {x * y, "It is like addition on steroids."}
end

{result, comment} = FunnyCalculator.add(10, 20)
IO.puts(result)
IO.puts(comment)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

30
You need a calculator to do that?

NOTE - Các loại tùy chỉnh được xác định thông qua @type được xuất và có sẵn bên ngoài mô-đun mà chúng được xác định. Nếu bạn muốn giữ một loại tùy chỉnh ở chế độ riêng tư, bạn có thể sử dụng @typep chỉ thị thay vì @type.

Behaviors trong Elixir (và Erlang) là một cách để tách biệt và trừu tượng hóa phần chung của một thành phần (trở thành mô-đun hành vi) khỏi phần cụ thể (trở thành mô-đun gọi lại). Hành vi cung cấp một cách để -

  • Xác định một tập hợp các chức năng phải được thực hiện bởi một mô-đun.
  • Đảm bảo rằng một mô-đun thực hiện tất cả các chức năng trong tập hợp đó.

Nếu phải làm như vậy, bạn có thể nghĩ đến các hành vi như giao diện trong các ngôn ngữ hướng đối tượng như Java: một tập hợp các chữ ký hàm mà một mô-đun phải triển khai.

Xác định hành vi

Chúng ta hãy xem xét một ví dụ để tạo hành vi của riêng mình và sau đó sử dụng hành vi chung này để tạo mô-đun. Chúng tôi sẽ xác định một hành vi chào đón mọi người xin chào và tạm biệt bằng các ngôn ngữ khác nhau.

defmodule GreetBehaviour do
   @callback say_hello(name :: string) :: nil
   @callback say_bye(name :: string) :: nil
end

Các @callbackChỉ thị được sử dụng để liệt kê các chức năng mà các mô-đun áp dụng sẽ cần xác định. Nó cũng chỉ định không. đối số, kiểu của chúng và giá trị trả về của chúng.

Chấp nhận một Hành vi

Chúng tôi đã xác định thành công một hành vi. Bây giờ chúng tôi sẽ áp dụng và triển khai nó trong nhiều mô-đun. Hãy để chúng tôi tạo hai mô-đun thực hiện hành vi này bằng tiếng Anh và tiếng Tây Ban Nha.

defmodule GreetBehaviour do
   @callback say_hello(name :: string) :: nil
   @callback say_bye(name :: string) :: nil
end

defmodule EnglishGreet do
   @behaviour GreetBehaviour
   def say_hello(name), do: IO.puts("Hello " <> name)
   def say_bye(name), do: IO.puts("Goodbye, " <> name)
end

defmodule SpanishGreet do
   @behaviour GreetBehaviour
   def say_hello(name), do: IO.puts("Hola " <> name)
   def say_bye(name), do: IO.puts("Adios " <> name)
end

EnglishGreet.say_hello("Ayush")
EnglishGreet.say_bye("Ayush")
SpanishGreet.say_hello("Ayush")
SpanishGreet.say_bye("Ayush")

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

Hello Ayush
Goodbye, Ayush
Hola Ayush
Adios Ayush

Như bạn đã thấy, chúng tôi áp dụng một hành vi sử dụng @behaviourchỉ thị trong mô-đun. Chúng ta phải xác định tất cả các chức năng được thực hiện trong hành vi cho tất cả các mô-đun con . Điều này có thể được coi là tương đương với giao diện trong các ngôn ngữ OOP.

Elixir có ba cơ chế lỗi: lỗi, ném và thoát. Hãy cùng chúng tôi khám phá chi tiết từng cơ chế.

lỗi

Lỗi (hoặc ngoại lệ) được sử dụng khi những điều đặc biệt xảy ra trong mã. Một lỗi mẫu có thể được truy xuất bằng cách cố gắng thêm một số vào một chuỗi -

IO.puts(1 + "Hello")

Khi chương trình trên được chạy, nó tạo ra lỗi sau:

** (ArithmeticError) bad argument in arithmetic expression
   :erlang.+(1, "Hello")

Đây là một lỗi có sẵn mẫu.

Tăng lỗi

Chúng ta có thể raiselỗi sử dụng các chức năng tăng. Chúng ta hãy xem xét một ví dụ để hiểu giống nhau -

#Runtime Error with just a message
raise "oops"  # ** (RuntimeError) oops

Các lỗi khác có thể được đưa ra với raise / 2 chuyển tên lỗi và danh sách các đối số từ khóa

#Other error type with a message
raise ArgumentError, message: "invalid argument foo"

Bạn cũng có thể xác định lỗi của riêng mình và nêu ra những lỗi đó. Hãy xem xét ví dụ sau:

defmodule MyError do
   defexception message: "default message"
end

raise MyError  # Raises error with default message
raise MyError, message: "custom message"  # Raises error with custom message

Khắc phục lỗi

Chúng tôi không muốn các chương trình của mình bị thoát đột ngột nhưng các lỗi cần được xử lý cẩn thận. Đối với điều này, chúng tôi sử dụng xử lý lỗi. Chúng tôirescue lỗi sử dụng try/rescuexây dựng. Chúng ta hãy xem xét ví dụ sau để hiểu giống nhau -

err = try do
   raise "oops"
rescue
   e in RuntimeError -> e
end

IO.puts(err.message)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

oops

Chúng tôi đã xử lý các lỗi trong câu lệnh cứu bằng cách sử dụng đối sánh mẫu. Nếu chúng tôi không sử dụng lỗi nào và chỉ muốn sử dụng nó cho mục đích nhận dạng, chúng tôi cũng có thể sử dụng biểu mẫu -

err = try do
   1 + "Hello"
rescue
   RuntimeError -> "You've got a runtime error!"
   ArithmeticError -> "You've got a Argument error!"
end

IO.puts(err)

Khi chạy chương trình trên, nó tạo ra kết quả sau:

You've got a Argument error!

NOTE- Hầu hết các chức năng trong thư viện tiêu chuẩn Elixir được thực hiện hai lần, một lần trả về các bộ giá trị và lần khác làm tăng lỗi. Ví dụ,File.readFile.read!chức năng. Đầu tiên trả về một tuple nếu tệp được đọc thành công và nếu gặp lỗi, tuple này được sử dụng để đưa ra lý do gây ra lỗi. Cái thứ hai nêu ra lỗi nếu gặp lỗi.

Nếu chúng ta sử dụng phương pháp tiếp cận hàm đầu tiên, thì chúng ta cần sử dụng trường hợp cho mẫu phù hợp với lỗi và thực hiện hành động theo đó. Trong trường hợp thứ hai, chúng tôi sử dụng phương pháp giải cứu thử đối với mã dễ bị lỗi và xử lý lỗi cho phù hợp.

Ném

Trong Elixir, một giá trị có thể được ném và sau đó sẽ được bắt lại. Ném và Bắt được dành riêng cho các trường hợp không thể lấy giá trị trừ khi sử dụng ném và bắt.

Các trường hợp này khá phổ biến trong thực tế ngoại trừ khi giao tiếp với các thư viện. Ví dụ: bây giờ chúng ta hãy giả sử rằng mô-đun Enum không cung cấp bất kỳ API nào để tìm giá trị và chúng ta cần tìm bội số đầu tiên của 13 trong danh sách các số -

val = try do
   Enum.each 20..100, fn(x) ->
      if rem(x, 13) == 0, do: throw(x)
   end
   "Got nothing"
catch
   x -> "Got #{x}"
end

IO.puts(val)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

Got 26

Lối ra

Khi một quy trình chết vì "nguyên nhân tự nhiên" (ví dụ: ngoại lệ không được xử lý), nó sẽ gửi một tín hiệu thoát. Một quá trình cũng có thể chết bằng cách gửi một tín hiệu thoát rõ ràng. Chúng ta hãy xem xét ví dụ sau:

spawn_link fn -> exit(1) end

Trong ví dụ trên, quá trình được liên kết đã chết bằng cách gửi một tín hiệu thoát có giá trị là 1. Lưu ý rằng lối ra cũng có thể được “bắt” bằng cách sử dụng try / catch. Ví dụ -

val = try do
   exit "I am exiting"
catch
   :exit, _ -> "not really"
end

IO.puts(val)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

not really

Sau

Đôi khi cần đảm bảo rằng tài nguyên được dọn dẹp sau một số hành động có thể gây ra lỗi. Cấu trúc try / after cho phép bạn làm điều đó. Ví dụ, chúng ta có thể mở một tệp và sử dụng mệnh đề sau để đóng nó - ngay cả khi có sự cố.

{:ok, file} = File.open "sample", [:utf8, :write]
try do
   IO.write file, "olá"
   raise "oops, something went wrong"
after
   File.close(file)
end

Khi chúng tôi chạy chương trình này, nó sẽ báo lỗi cho chúng tôi. Nhưngafter câu lệnh sẽ đảm bảo rằng trình mô tả tệp được đóng khi bất kỳ sự kiện nào như vậy.

Macro là một trong những tính năng tiên tiến và mạnh mẽ nhất của Elixir. Như với tất cả các tính năng nâng cao của bất kỳ ngôn ngữ nào, macro nên được sử dụng một cách tiết kiệm. Chúng làm cho nó có thể thực hiện các chuyển đổi mã mạnh mẽ trong thời gian biên dịch. Bây giờ chúng ta sẽ hiểu sơ lược về macro là gì và cách sử dụng chúng.

Trích dẫn

Trước khi bắt đầu nói về macro, trước tiên chúng ta hãy xem xét nội bộ của Elixir. Một chương trình Elixir có thể được biểu diễn bằng cấu trúc dữ liệu của chính nó. Khối xây dựng của một chương trình Elixir là một bộ gồm ba phần tử. Ví dụ, lệnh gọi hàm sum (1, 2, 3) được biểu diễn bên trong dưới dạng:

{:sum, [], [1, 2, 3]}

Phần tử đầu tiên là tên hàm, phần tử thứ hai là danh sách từ khóa chứa siêu dữ liệu và phần tử thứ ba là danh sách đối số. Bạn có thể lấy nó làm đầu ra trong iex shell nếu bạn viết như sau:

quote do: sum(1, 2, 3)

Các toán tử cũng được biểu diễn dưới dạng các bộ giá trị như vậy. Các biến cũng được biểu diễn bằng cách sử dụng các bộ ba như vậy, ngoại trừ phần tử cuối cùng là một nguyên tử, thay vì một danh sách. Khi trích dẫn các biểu thức phức tạp hơn, chúng ta có thể thấy rằng mã được biểu diễn trong các bộ như vậy, chúng thường được lồng vào nhau trong một cấu trúc giống như một cái cây. Nhiều ngôn ngữ sẽ gọi các biểu diễn như vậy làAbstract Syntax Tree (AST). Elixir gọi những biểu thức được trích dẫn này.

Bỏ trích dẫn

Bây giờ chúng ta có thể truy xuất cấu trúc bên trong của mã của mình, làm cách nào để sửa đổi nó? Để nhập mã hoặc giá trị mới, chúng tôi sử dụngunquote. Khi chúng tôi hủy trích dẫn một biểu thức, nó sẽ được đánh giá và đưa vào AST. Chúng ta hãy xem xét một ví dụ (trong iex shell) để hiểu khái niệm -

num = 25

quote do: sum(15, num)

quote do: sum(15, unquote(num))

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

{:sum, [], [15, {:num, [], Elixir}]}
{:sum, [], [15, 25]}

Trong ví dụ cho biểu thức trích dẫn, nó không tự động thay thế num bằng 25. Chúng ta cần hủy trích dẫn biến này nếu chúng ta muốn sửa đổi AST.

Macro

Vì vậy, bây giờ chúng ta đã quen thuộc với trích dẫn và hủy trích dẫn, chúng ta có thể khám phá lập trình ẩn trong Elixir bằng cách sử dụng macro.

Nói một cách đơn giản nhất, macro là các hàm đặc biệt được thiết kế để trả về một biểu thức được trích dẫn sẽ được chèn vào mã ứng dụng của chúng tôi. Hãy tưởng tượng macro được thay thế bằng biểu thức được trích dẫn thay vì được gọi giống như một hàm. Với macro, chúng tôi có mọi thứ cần thiết để mở rộng Elixir và thêm mã động vào các ứng dụng của mình

Hãy để chúng tôi thực hiện trừ khi là một macro. Chúng ta sẽ bắt đầu bằng cách xác định macro bằng cách sử dụngdefmacrovĩ mô. Hãy nhớ rằng macro của chúng ta cần trả về một biểu thức được trích dẫn.

defmodule OurMacro do
   defmacro unless(expr, do: block) do
      quote do
         if !unquote(expr), do: unquote(block)
      end
   end
end

require OurMacro

OurMacro.unless true, do: IO.puts "True Expression"

OurMacro.unless false, do: IO.puts "False expression"

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

False expression

Điều đang xảy ra ở đây là mã của chúng tôi đang được thay thế bằng mã được trích dẫn được trả về bởi macro trừ khi . Chúng tôi đã bỏ trích dẫn biểu thức để đánh giá nó trong ngữ cảnh hiện tại và cũng bỏ trích dẫn khối do để thực thi nó trong ngữ cảnh của nó. Ví dụ này cho chúng ta thấy lập trình siêu ứng dụng bằng macro trong elixir.

Macro có thể được sử dụng trong các tác vụ phức tạp hơn nhiều nhưng nên sử dụng một cách tiết kiệm. Điều này là do lập trình ẩn nói chung được coi là một thực hành không tốt và chỉ nên được sử dụng khi cần thiết.

Elixir cung cấp khả năng tương tác tuyệt vời với các thư viện Erlang. Hãy để chúng tôi thảo luận ngắn gọn về một vài thư viện.

Mô-đun nhị phân

Mô-đun Chuỗi Elixir tích hợp xử lý các tệp nhị phân được mã hóa UTF-8. Mô-đun nhị phân hữu ích khi bạn xử lý dữ liệu nhị phân không nhất thiết phải được mã hóa UTF-8. Chúng ta hãy xem xét một ví dụ để hiểu thêm về mô-đun Binary -

# UTF-8
IO.puts(String.to_char_list("Ø"))

# binary
IO.puts(:binary.bin_to_list "Ø")

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

[216]
[195, 152]

Ví dụ trên cho thấy sự khác biệt; mô-đun Chuỗi trả về điểm mã UTF-8, trong khi: nhị phân xử lý các byte dữ liệu thô.

Mô-đun tiền điện tử

Mô-đun tiền điện tử chứa các hàm băm, chữ ký số, mã hóa và hơn thế nữa. Mô-đun này không phải là một phần của thư viện chuẩn Erlang, nhưng được bao gồm trong bản phân phối Erlang. Điều này có nghĩa là bạn phải liệt kê: tiền điện tử trong danh sách ứng dụng của dự án bất cứ khi nào bạn sử dụng. Hãy để chúng tôi xem một ví dụ sử dụng mô-đun tiền điện tử -

IO.puts(Base.encode16(:crypto.hash(:sha256, "Elixir")))

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

3315715A7A3AD57428298676C5AE465DADA38D951BDFAC9348A8A31E9C7401CB

Mô-đun Digraph

Mô-đun đồ thị chứa các hàm để xử lý các đồ thị có hướng được xây dựng bằng các đỉnh và cạnh. Sau khi xây dựng đồ thị, các thuật toán trong đó sẽ giúp tìm, ví dụ, đường đi ngắn nhất giữa hai đỉnh hoặc các vòng lặp trong đồ thị. Lưu ý rằng các chức năngin :digraph thay đổi cấu trúc đồ thị một cách gián tiếp như một hiệu ứng phụ, trong khi trả về các đỉnh hoặc cạnh đã thêm.

digraph = :digraph.new()
coords = [{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}]
[v0, v1, v2] = (for c <- coords, do: :digraph.add_vertex(digraph, c))
:digraph.add_edge(digraph, v0, v1)
:digraph.add_edge(digraph, v1, v2)
for point <- :digraph.get_short_path(digraph, v0, v2) do 
   {x, y} = point
   IO.puts("#{x}, #{y}")
end

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

0.0, 0.0
1.0, 0.0
1.0, 1.0

Mô-đun Toán học

Mô-đun toán học chứa các phép toán phổ biến bao gồm các hàm lượng giác, hàm mũ và logarit. Chúng ta hãy xem xét ví dụ sau để hiểu cách hoạt động của mô-đun Toán học -

# Value of pi
IO.puts(:math.pi())

# Logarithm
IO.puts(:math.log(7.694785265142018e23))

# Exponentiation
IO.puts(:math.exp(55.0))

#...

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

3.141592653589793
55.0
7.694785265142018e23

Mô-đun hàng đợi

Hàng đợi là một cấu trúc dữ liệu thực hiện các hàng đợi FIFO (kết thúc kép) (nhập trước xuất trước) một cách hiệu quả. Ví dụ sau đây cho thấy cách hoạt động của mô-đun Hàng đợi:

q = :queue.new
q = :queue.in("A", q)
q = :queue.in("B", q)
{{:value, val}, q} = :queue.out(q)
IO.puts(val)
{{:value, val}, q} = :queue.out(q)
IO.puts(val)

Khi chương trình trên được chạy, nó tạo ra kết quả sau:

A
B