2016-11-06

Reactive programming, từ declarative programming đến RxJava, RxAndroid (phần 1)

Dạo sau này khi code mình xài Python, Scala nhiều hơn. Một cách tự nhiên sẽ đụng nhiều hơn về functional programming. Tất nhiên lúc trước khi code web và Node thì cũng đã không lạ gì với style này kiểu như chắc chắn phải xài lodash chẳng hạn. 1 thời gian sau đó là xem qua Swift và dạo gần đây là RxAndroid. Có vẻ functional programming đang được chú ý nhiều ngày càng nhiều hơn. Lại nhớ cái hồi F# mới ra, cũng download sách vào đọc 1 xíu xong rồi cũng quăn :D. Cũng nhiều lần tính nghiên cứu sâu hơn, định note lại những cái đã xem lâu rồi nhưng lười. Bây giờ thì "ít lười" hơn, và cũng nên note lại cho thấm.


RxAndroid

RxJava là một bản port từ Netflix của Reactive Extensions (Rx) dành cho Java. 
Ban đầu Reactive Extensions (Rx) chỉ là một bộ thư viện (a set of libraries) do một nhóm nghiên cứu tại Microsoft Open Technologies do Erik Meijer (kiến trúc sư/architect) và các thành viên (có thể kể đến Jeffery Van GoghWes DyerBart De Smet) phát triển.
Sau đó Rx được công bố mã nguồn (November, 2012) trở thành open-source project trên Codeplex (đã chuyển qua Github Reactive Extensions for .NET) với mục đích mở rộng việc sử dụng Rx để tăng khả năng tương tác cho các ứng dụng trên nhiều loại thiết bị và môi trường cloud. 
Trong một bài phỏng vấn (Rx in 15 minutes - Rx is here!!!!!), Erik Meijer (Architect) đã trình bày ý tưởng tạo ra Rx khi so sánh mô hình Pull (Interactive) với mô hình Push (Reactive) với data source IEnumerable (đặc biệt để tăng hiệu quả làm việc với LINQ). Rx được tạo ra bằng cách định nghĩa IObservables và IObservers tương ứng với IEnumerables và IEnumerators, unify các method và cách sử dụng (hay API).Theo đó định nghĩa ban đầu của Rx là Rx = Observables + LINQ + Schedulers.

RxJava được định nghĩa như sau:
RxJava is a Java VM implementation of ReactiveX (Reactive Extensions): a library for composing asynchronous and event-based programs by using observable sequences.

RxAndroid là mở rộng của RxJava sử dụng cho lập trình ứng dụng Android.
RxAndroid adds the minimum classes to RxJava that make writing reactive components in Android applications easy and hassle-free.

What is ReactiveX?

Theo giới thiệu của chính ReactiveX thì ReactiveX là
An API for asynchronous programming with observable streams.
hay
ReactiveX is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming.
ReactiveX dùng để lập trình bất đồng bộ với các dòng dữ liệu có thể "quan sát" (có nghĩa là push/emit, dạng push model). Hay ReactiveX là sự kết hợp các ý tưởng tốt nhất từ Observer patternIterator pattern và functional programming (lập trình hàm).

Có thể dùng ReactiveX với nhiều implementation cho các ngôn ngữ khác nhau: RxJava, RxJS, Rx.NET, RxScala, RxPy hay RxSwift...

Programming paradigms

Trước khi sử dụng reactive programming (RP) cần nói qua về programming paradigm. Tiếp theo cần phân biệt hai programming paradigm là imperative programming và  declarative programming.
Paradigm là gì? Paradigm là một thuật ngữ chỉ hình mẫu, lý thuyết, khái niệm, giả thuyết hoặc khung tham chiếu. Có một vài ý kiến cho rằng có thể dịch paradigm thành mô hình trong tiếng Việt, tức programming paradigm sẽ dịch là mô hình lập trình. Tuy nhiên "mô hình" không thể truyền tải hết những ý nghĩa của của paradigm. Có thể coi thuật ngữ paradigm tương ứng với ý nghĩa của từ mô thức(method + model + pattern .../模式 [MÔ THỨC], 範例 [PHẠM LỆ] hay 模范 [MÔ PHẠM]) tức là mô hình hình mẫu kết hợp với phương thức, cách thức. Cũng có thể cho rằng một programming paradigm là một trường phái lập trình (school/流派 [LƯU PHÁI]).

Không thể có sự so sánh chung cho các paradigm vì không có tiêu chuẩn để so sánh và căn bản các paradigm có các suy nghĩ, cách nhìn và phương pháp hoàn toàn khác nhau.

Ví dụ các programming paradigm hay được nhắc tới như sau:
§  Imperative programming (tạm dịch lập trình chỉ thị/mệnh lệnh)
§  Declarative programming (tạm dịch lập trình khai báo)
§  Object-oriented programming (OOP, lập trình hướng đối tượng)
§  Procedural programming (tạm dịch lập trình thủ tục)
§  Functional programming (tạm dịch lập trình hàm/lập trình hướng chức năng)
§  Logic programming

Functional programming và logic programming có thể được coi là "declarative".
Hầu hết các ngôn ngữ lập trình đều là các ngôn ngữ nhiều paradigm (multi-paradigm programming languages) có nghĩa support nhiều hơn một programming paradigm.

Imperative programming vs declarative programming

Imperative programming theo Wikipedia
Imperative programming is a programming paradigm that uses statements that change program's state.
Là paradigm sử dụng các phát biểu/lệnh/chỉ thị (statements/instructions) dùng để thay đổi trạng thái của chương trình.
Imperative programming focuses on describing how a program operates.
Imperative programming tập trung vào việc mô tả các chương trình thực hiện. Tức tập trung vào mô tả làm các nào thực hiện việc gì đó, chỉ thị cho computer/compiler thực hiện những gì mình muốn xảy ra từng bước (giống đặt bản thân vào vai của computer). Đây là các tiếp cận với programming rất tự nhiên từ những ngày đầu khi mà compiler còn rất đơn giản.

Structured programming là programming paradigm subset của imperative programming khi các chức năng (functionality) được chia thành những đơn vị (units). Cách thực hiện phân chia có thể thấy như phân chia thành blockscontrol flows, và subroutines hay cao hơn là những modulespackages. Ví dụ với control flow có thể thấy cách tiếp cận này đã không còn khuyến khích sử dụng go-to statements.

Theo định nghĩa như trên thì object oriented programming (OOP như C#, Java, Python) "tự nhiên" được coi là subset của structured programming khi các chức năng được tổ chức theo hướng chia thành những đối tượng. 
Phần còn lại không hướng đối tượng mà tổ chức  tổ chức các statements thành những procedures (hay functions) được gọi là procedural programming (như Pascal, Fortran, C) hiển nhiên cũng là một subset của structured programming.
Thật ra cũng có những ý kiến không đồng tình với kết luận trên khi cho rằng nếu đơn thuần structured programming sẽ có cách tiếp cập top-down trong khi đó OOP lại có cả cách tiếp cập bottom-up (khi bài toán đi lên từ các object). 
Tương tự có ý kiến không đồng ý quan hệ giữa OOP và procedural programming trong structured programming như đã nói ở trên khi cho rằnh OOP là subset của procedural programming khi mà việc thao tác với các objects cũng thông qua các methods đã được định sẵn. Tất nhiên là không có vấn đề gì. Tình huống này giống như việc mọi người hay so sánh giữa C++ và C nhưng phần lớn trường hợp C là subset của C++ (well written, và chính xác hơn là ANSI C, xem Is C a subset of C++?).
Ngược lại với imperative programming, declarative programming được định nghĩa như sau:
Declarative programming is a programming paradigm—a style of building the structure and elements of computer programs—that expresses the logic of a computation without describing its control flow.
Declarative programming mô tả vấn đề hay cách giải quyết mà không đi sâu vào việc mô tả tường minh trạng thái của công việc sẽ được tiến hành. Tức là mô tả cái mà mình mong muốn và phần còn lại cách thực hiện sẽ dành cho computer. 

Việc này có liên quan nhiều đến sự so sánh giữa statements và expressions.
Expression (biểu thức) thể hiện hay là cách mô tả giá trị, cần phải được evaluated (đánh giá) để trả về giá trị (tức nếu chỉ riêng một mình thì expression không thể làm gì cả). Một ví dụ hay gặp là biểu thức logic (logical expression/boolean expression). Do đó expression có thể pass như một đối số (argument) cho một function trong khi statement thông thường thì không (do không trả về giá trị). 
Vấn đề là với các ngôn ngữ như C/C++ chỉ cần thêm semicolon ; là có thể chuyển một expression thành một statement gọi là expression statements. Rõ ràng mọi developer đều nhận thấy là expression sẽ được evaluated và giá trị trả về coi như bị vứt đi (có vẻ như vô hại).
Điều đáng nói ở đây là trường hợp của gán giá trị assignment và toán tử gán assignment operator. Một khi cho phép expression chuyển thành một statement thì thông thường toán tử gán (assignment operator) cũng được cho phép xuất hiện trong expression (như C/C++), tức là cho phép việc chuyển một statement thành một expression. Với các ngôn ngữ cho phép điều này (C/C++) giá trị trả về của assignment statement sẽ là giá trị của expression sau khi evaluated và gán cho variable. Khi đó assignment statement không đơn thuần là một statement mà còn là một expression.
Việc duy trì quan hệ statements và expressions như trên được cho rằng sẽ có thể dẫn tới side-effects. Khi xem kỹ đoạn code sau, chuyện xảy ra nếu func có thể làm một điều gì đó không hay, có vẻ như không còn vô hại được nữa:

1 + 2 * func(3);

Side effects là gì? Nói chung là những hiệu ứng không mong muốn. Ví dụ khi thực hiện một statement có trả về một kết quả ẩn và kết quả trả về này có thể làm thay đổi giá trị được lưu trữ (hay nói chung là trạng thái) của các đối tượng khác. 
Giả sử một lỗi nếu mắc phải với assignment operator của C/C++ thì rất khó nhận ra (nhất là thời kỳ IDE còn cùi mía): phép gán trả về kết quả 10 và tự động casting sang kiểu boolean là true.

if(a = 10) {

}

Hay một ví dụ tương tự cũng của  assignment operator như sau

double x = 3.5;
int y;
double z = y = x;  // z = 3

Ngoài ra side-effect cũng thường xảy ra khi một function call thay đổi một giá trị bên ngoài (outside world, như global variable hay static variable hay thậm chí một method thay đổi một member variable không tường minh hoặc không theo đúng thiết kế).

Việc nhập nhằng giữa statements và expressions có thể thấy xảy ra ở tất cả các ngôn ngữ giống C (bao gồm C/C++, C# hay Java). Một số nhà thiết kế ngôn ngữ lập trình nghĩ theo hướng khác vì không thích điều này và mong muốn là tìm một giải pháp giảm thiểu side-effects. Theo đó một cách "tự nhiên" imperative programming thiên về statements thì declarative programming focus vào solution sẽ thiên về expressions. Ví dụ các functional programming sẽ tìm cách loại bỏ statements nhất là assignment statements bằng cách chỉ sử dụng expressions và một dạng gọi là khởi tạo giá trị (initialization) hay declaration style.
Để dễ hiểu hơn 1 chút, bạn cần xem qua ví dụ tính tổng một array. Với imperative programming, việc thực hiện dạng như sau:

var  numbers = [1, 2, 3, 4, 5]
var  sum = 0
for(var i = 0; i < numbers.length; i++) {
   sum = sum + numbers[i];
}

Với declarative programming code sẽ có dạng như sau

var numbers = [1, 2, 3, 4, 5]
var sum = numbers.reduce((x, y) => x + y)

Cụ thể với Scala 

val numbers = Array(1, 2, 3, 4, 5)
var sum = 0
       
for(i <- 0 to numbers.length - 1) {
   sum = sum + numbers(i)
}


val numbers = Array(1, 2, 3, 4, 5)
val sum = numbers.reduceLeft[Int](_ + _)

Hay x2 với một array cách tiếp cận imperative programming sẽ là khai báo một array mới, duyệt array cũ và push vào array mới từng giá trị của element nhân 2. Trong khi declarative programming sẽ code như sau:

val numbers = Array(1, 2, 3, 4, 5)
val doubled = numbers.map(x => 2 * x)

Ví dụ trên thể hiện hai "cái nhìn" rất khác nhau, với declarative programming thì việc quan tâm là focus vào logic như x => 2 *x. Tất nhiên nhiều người sẽ đặt câu hỏi, chuyện gì ở bên trong function map hay reduce kia. Rõ ràng có thể đặt lại vấn đề là việc thực hiện này có sự giúp sức của libraries hay hệ thống.

Thực ra có thể đưa ra ví dụ dễ dàng bắt gặp hơn (đồng nghĩa là ít thắc mắc/ý kiến hơn) khi sử dụng ngôn ngữ truy vấn như SQL:

SELECT * FROM Users WHERE country = 'USA';

Rõ ràng ít người thắc mắc câu query này hoạt động như thế nào hơn là việc xuất hiện cái method map hay reduce kia (gần như chấp nhận). Con người là như vậy, nhiều khi biết 1 chút ít sẽ là rào cản để đón nhận cái mới.
Một số ví dụ khác có thể thấy trong những năm qua như LINQ với C#

var odds = collection.Where(num => num % 2 != 0);

Thực ra các markup languages không xa lạ gì như HTML, XML, XAML là những ví dụ thiên về declarative khi được sử dụng để khai báo UI thay vì thực sự code để paint UI (được render bởi engine/libraries).
Declarative programming có lẽ gần với giấc mơ về lập trình hơn. Vì một ngày nào đó sẽ chỉ cần dùng ngôn ngữ tự nhiên coding bằng cách kêu computer tính "sum" mà không cần code một cái gì hết.
Việc sử dụng declarative programming được cho là giảm thiểu side effects (tác dụng phụ/hiệu ứng phụ/hiệu ứng bên lề). Điều này sẽ được nói rõ hơn với functional programming.

Reactive programming

Reactive programming (RP, tạm dịch lập trình phản ứng) không phải là khái niệm mới mẻ vì thực chất đã được giới thiệu từ những năm 70s. Tuy nhiên có những hạn chế về mặt implementation (compiler còn chưa phát triển) và quan trọng nhất là nhu cầu hay yêu cầu để nó có thể phát triển còn chưa lớn, chưa có một enterprise lớn dạng "cây đa cây đề" nào phát động. Sự phát triển của reactive programming không phải là ngẫu nhiên khi đi chung với sự phát triển của microservices (kiến trúc nhiều dịch vụ nhỏ), multi-core CPU, distributed processing hay parallel distributed processing tức việc xử lý data thường có tính chất concurrent, parallel (xem thêm concurrent vs parallel), asynchronous (bất đồng bộ), từ nhiều nguồn (hay là nhiều flow). Xem thêm Notes on Reactive Programming tại Spring.io hay Why Reactive Programming Is Not a Fad tại DZone.

Theo Wikipedia:
Reactive programming is a programming paradigm oriented around data flows and the propagation of change.

Reactive programming là programming paradigm hướng tới việc xử lý dòng dữ liệu và sự lan truyền thay đổi.
Đơn giản có thể coi reactive programming là cách lập trình với những toolbox nhằm nâng mức trừu tượng (the level of abstraction) của source code lên một mức cao hơn khi focus (tập trung) vào việc xử lý các  sự kiện và quan hệ giữa các sự kiện (the interdependence of events) mô tả business logic thay vì phải tốn nhiều thời gian cho việc tổ chức cài đặt (implementation) các chi tiết nhằm giải quyết các business logic này.

Phần sau sẽ trình bày tiếp functional programming, functional reactive programming trước khi đi qua RxJava.

No comments:

Post a Comment