Tìm hiểu về Power Query / M Language (Part 12): Mô hình
Để nâng cao hiểu biết của chúng ta về cách hoạt động của Power Query, chúng ta hãy xem xét mô hình – phương pháp luận mà M được xây dựng. Về mặt khái niệm, M hoạt động như thế nào? Nó nghĩ như thế nào? Các chi tiết, như biến, biểu thức let, hàm và định danh, đều quan trọng, nhưng nếu chúng ta chỉ tập trung vào chi tiết, chúng ta có thể bỏ lỡ bức tranh lớn.
Nếu không có nhận thức về phương pháp luận của M, chúng ta có thể dành cả ngày để viết mã M bối rối bởi một số hành vi nhất định, bối rối không hiểu tại sao M dường như không cho chúng ta làm những việc nhất định mà chúng ta có thể muốn làm và đổ mồ hôi khi chúng ta tốn công cố gắng bắt chước các mẫu mã từ các ngôn ngữ khác không cần thiết trong M.
Nó là gì ?
Power Query (M) là một ngôn ngữ truy vấn. Nó được thiết kế để xây dựng các truy vấn kết hợp dữ liệu. Giữa “truy vấn” và “trộn”, M có thể được sử dụng để xuất dữ liệu đã được làm sạch, lọc, chuyển đổi, tổng hợp và kết hợp từ nhiều nguồn và định dạng.
Tuy nhiên, M không cung cấp khả năng sửa đổi dữ liệu trong các nguồn dữ liệu. Nó lấy dữ liệu từ các nguồn nhưng không ghi các thay đổi hoặc chèn dữ liệu mới vào các nguồn đó. Ngoài ra, nó không phải là ngôn ngữ lập trình hoặc tập lệnh có mục đích chung. M không có ý định làm những việc như vẽ hộp thoại trên màn hình, sao chép tệp từ ổ đĩa này sang ổ đĩa khác hoặc in. M là để truy vấn và trộn dữ liệu, không phải để giải quyết tất cả các vấn đề về máy tính của thế giới! Trọng tâm phù hợp này có nghĩa là M có thể cố gắng làm một việc đặc biệt xuất sắc thay vì cố gắng làm rất nhiều loại việc khác nhau mà chỉ có thể làm được như vậy.
Làm thế nào nó hoạt động ?
Nói tóm lại, về mặt kỹ thuật, M là một ngôn ngữ chức năng, chia sẻ những điểm tương đồng với các ngôn ngữ như F#. Cụ thể, nó chủ yếu là thuần túy, bậc cao hơn và một phần lười biếng.
Thứ tự đánh giá
M không nhất thiết phải thực thi các biểu thức theo thứ tự chúng xuất hiện trong các tập lệnh. M tuân theo thứ tự phụ thuộc, không phải thứ tự vật lý được thể hiện trong mã nguồn.
Hãy nghĩ về cách các công thức trong bảng tính (excel) được đánh giá. Ví dụ dưới đây, để tạo ra kết quả được yêu cầu, trước tiên công thức trong ô A3 phải được đánh giá. Khi giá trị của nó đã được tính toán, A1 và A2 sẽ được xử lý. Điều nào được thực thi trước không quan trọng, do đó, công cụ bảng tính có thể chọn thực thi A1 trước A2 hay ngược lại (hoặc có thể thực thi cả hai song song).
Trình thông dịch của M hoạt động tương tự. Nó xem xét biểu thức tạo ra giá trị và xác định thứ tự cần thiết để thực hiện giá trị đó. Nếu có các bước mà thứ tự đó không bị quy định bởi các yếu tố phụ thuộc, M có thể chọn.
Điều này ngụ ý rằng bạn có thể viết mã M theo bất kỳ thứ tự nào bạn muốn, ngay cả khi thứ tự đó không theo thứ tự, mặc dù việc viết mã theo thứ tự có thể sẽ dễ dàng hơn cho những người khác đọc mã của bạn. Dưới đây, hai biểu thức này đều hợp lệ và tương đương trong đầu ra của chúng mặc dù các biểu thức con của chúng được đưa ra theo thứ tự ngược lại. Trong cả hai trường hợp, M tìm ra thứ tự thực thi chính xác để sử dụng khi nó xử lý chúng.
let
Data = { 1, 2, 3},
Result = List.Transform(Data, each _ * 10)
in
Result
let
Result = List.Transform(Data, each _ * 10),
Data = { 1, 2, 3}
in
Result
Một phần lười biếng (Partially Lazy)
Khi trình thông dịch của M xác định thứ tự đánh giá, điều gì sẽ xảy ra nếu nó gặp một biểu thức không cần thiết?
let
Source = { "Data", "Maker", "VN" },
Filtered = List.Select(Source, each Text.StartsWith(_, "M"))
in
Source
Ở ví dụ trên, trình thông dịch làm gì với Filtered, không cần đánh giá để tạo ra kết quả đầu ra theo yêu cầu của mệnh đề in? Trình thông dịch bỏ qua Filtered. Vì không cần thiết nên nó không được đánh giá. Nhưng tại sao lại viết mã không cần thiết?
Danh sách các bước đã áp dụng hiển thị bước đầu tiên đã chọn Đối với một, còn thử nghiệm thì sao? Trong trình chỉnh sửa truy vấn GUI, bạn có thể chọn một bước trước bước cuối cùng để bạn có thể kiểm tra đầu ra của nó. Khi bạn làm điều này, mệnh đề let’s in được đặt để trỏ đến bước đã chọn. Tuy nhiên, các bước đã bỏ qua vẫn được giữ lại trong mã nguồn. Thông dịch viên của M nhận ra rằng việc đánh giá chúng là không cần thiết nên bỏ qua chúng. Điều này là tốt – nếu không, thời gian có thể bị lãng phí khi đánh giá các biểu thức ngoại lai (có thể phức tạp)!
Trong trình chỉnh sửa truy vấn GUI, bạn có thể chọn một bước trước bước cuối cùng để bạn có thể kiểm tra đầu ra của nó. Khi bạn làm điều này, mệnh đề let in được đặt để trỏ đến bước đã chọn. Tuy nhiên, các bước đã bỏ qua vẫn được giữ lại trong mã nguồn. Thông dịch viên của M nhận ra rằng việc đánh giá chúng là không cần thiết nên bỏ qua chúng. Điều này là tốt – nếu không, thời gian có thể bị lãng phí khi đánh giá các biểu thức ngoại lai (có thể phức tạp)
Một ví dụ khác:
(even as logical) =>
let
Odd = { 3, 5, 7 },
Even = { 2, 4, 6 }
in
if even then Even else Odd
Mỗi khi phương thức trên được gọi, giá trị Odd hoặc Even là cần thiết – nhưng không phải cả hai giá trị. M chỉ đánh giá biến sẽ được sử dụng để tạo ra đầu ra. Ngược lại, nếu M là một ngôn ngữ thủ tục hoặc hướng đối tượng, có khả năng cả hai biến (cả Odd và Even) sẽ được đánh giá trước khi mệnh đề in được thực thi. Công việc không cần thiết sẽ được thực hiện. Rất may, M không hoạt động theo cách này.
Tôi thấy rằng các phép gán biến trong biểu thức let được đánh giá một cách lười biếng. Hành vi đánh giá lười biếng này cũng đúng đối với danh sách (list), bản ghi (record) và bảng (table).
List.Count({ Compute(), ComputeAnother() })
Để đếm số lượng mục trong danh sách, trình thông dịch M không cần đánh giá nội dung của các mục đó. Ở ví dụ trên, Compute() và ComputeAnother() không bao giờ được gọi vì đánh giá lười biếng.
Trong trường hợp của bản ghi bên dưới, Salary không được đánh giá vì giá trị của nó không được sử dụng khi xuất đầu ra được yêu cầu.
let
ComputeSalary = (wage) => ...,
PayDetails =
[
Wage = 25.10,
Salary = ComputeSalary(Wage)
]
in
PayDetails[Wage]
Hành vi lười biếng tương tự cũng đúng với các bảng. Tuy nhiên, trong M, chỉ biểu thức let, danh sách (list), bản ghi (record) và bảng (table) được đánh giá lười biếng. Đánh giá háo hức được sử dụng cho mọi thứ khác. Đây là lý do tại sao chúng ta nói rằng M chỉ lười biếng một phần.
Ví dụ, các đối số cho một lời gọi hàm được đánh giá một cách háo hức.
let
Numbers = { 1, 2, 3 },
Letters = { "A", "B", "C" },
Choose = (chooseFirst as logical, first, second) =>
if chooseFirst then first else second
in
Choose(true, Numbers, Letters)
Ở ví dụ trên, vì cả Numbers và Letters đều được sử dụng làm tham số khi Choose được gọi và các tham số không được đánh giá lười biếng, nên các biểu thức cho cả hai biến đều được đánh giá mặc dù chỉ một trong hai biến sẽ được hàm sử dụng.
Ngôn ngữ lập trình bậc cao (high-level programming language)
Ở ví dụ trên, cả hai danh sách đều được đánh giá vì chúng đã được chuyển dưới dạng đối số cho một hàm. Điều gì sẽ xảy ra nếu chúng ta thực sự muốn tránh việc cả hai danh sách chỉ được hiện thực hóa khi một danh sách bị vứt bỏ?
Hãy nhớ rằng – chúng ta có thể chuyển các chức năng xung quanh. Thay vì truyền Choose hai danh sách giá trị, điều gì sẽ xảy ra nếu chúng ta điều chỉnh nó để chúng ta truyền vào nó các hàm tạo ra các giá trị khi chúng được gọi ra? (không phải là “sử dụng các hàm trong danh sách tham số để điền các đối số của hàm”, ý tôi là “truyền các hàm dưới dạng đối số vào chính hàm đó”) Choose sau đó có thể chọn chỉ thực thi một trong các hàm, hàm đó tạo ra đầu ra mà nó cần. Hàm khác không được gọi nên danh sách mà nó tạo ra sẽ không được tạo.
let
Numbers = () => { 1, 2, 3 },
Letters = () => { "A", "B", "C" },
Choose = (chooseFirst as logical, first, second) =>
if chooseFirst then first() else second()
in
Choose(true, Numbers, Letters)
Về mặt kỹ thuật, trình thông dịch M vẫn đánh giá cả Numbers và Letters. Tuy nhiên, vì những biến này hiện xác định các hàm, việc đánh giá chúng chỉ đơn giản là lấy các biểu thức hàm của chúng và biến chúng thành các giá trị hàm. Hai giá trị hàm được chuyển vào hàm Choose sau đó chỉ gọi một trong số chúng. Nói một cách ngắn gọn, tôi nói rằng tôi đã chuyển hai hàm vào Choose để chọn hàm nào trong hai hàm để gọi
Khả năng truyền các hàm vào các hàm khác dưới dạng đối số, cũng như khả năng trả về các hàm từ các hàm làm cho M trở thành một ngôn ngữ bậc cao.