thảo luận Học imperative vs functional programming qua ví dụ

scoubidoubidou

Junior Member
Hôm nay có đọc được thread các bác so sánh giữa OOP và Functional Programming (FP), mình từng làm việc trên Java OOP, Elixir (chỉ có FP) và cũng khá thích dùng FP trên Java với Javascript nên mình nghĩ FP không thể so sánh với OOP vì bản chất 2 cái đó khác nhau, FP có thể dùng trong OOP được.

Một số điểm mạnh của FP (declarative programming) so với imperative programming: dễ hiểu, dễ sửa, dễ test, tránh được side effects, multi thread dễ

Học dễ nhất là qua ví dụ nên mình đưa ra những ví dụ cơ bản của 2 trường phái hoặc các bác post ví dụ của 1 trong hai trường phái lên rồi mọi người convert qua trường phái còn lại để xem cách nào hiệu quả hơn trong mỗi trường hợp


Ví dụ #1:
Cho list các posts của 1 forum, tính trung bình số từ của bài viết theo tác giả:
JavaScript:
const articles = [
    { author: 'Alice', title: 'Article 1', content: 'Lorem ipsum dolor sit amet.' },
    { author: 'Bob', title: 'Article 2', content: 'Consectetur adipiscing elit, sed do eiusmod.' },
    { author: 'Alice', title: 'Article 3', content: 'Ut enim ad minim veniam.' },
    { author: 'Charlie', title: 'Article 4', content: 'Quis nostrud exercitation ullamco laboris.' },
];

Imperative:
JavaScript:
function calculateAverageWordCountImperative(articles, author) {
    let sum = 0;
    let count = 0;

    for (const article of articles) {
        if (article.author === author) {
            sum += article.content.split(/\s+/).length;
            count++;
        }
    }

    return count ? sum / count : 0;
}

FP:
JavaScript:
function calculateAverageWordCountFunctional(articles, author) {
    const { sum, count } = articles
        .filter(article => article.author === author)
        .map(article => article.content.split(/\s+/).length)
        .reduce((acc, nbWord) => ({ sum: acc.sum + nbWord, count: acc.count + 1 }), { sum: 0, count: 0 });
    
    return count ? sum / count : 0;
}

Ví dụ #2: Multi-threading (Java)
Tính tổng bình phương của các số chẵn, bài này để ví dụ việc dùng multi thread rất dễ trong java bằng FP

Imperative:
Java:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

long sumOfSquaresOfEvens = 0;
for (int number : numbers) {
    if (number % 2 == 0) {
        sumOfSquaresOfEvens += number*number;
    }
}

FP:
Java:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

long sumOfSquaresOfEvens = numbers.parallelStream()
    .filter(number -> number % 2 == 0)
    .mapToLong(number -> number * number)
    .sum();


Đó là những ví dụ cơ bản còn trong thực tế mình dùng Elixir không có type nên nhiều khi rất là rối, phải chia ra các pure functions dễ hiểu và viết docs cho các function phức tạp nếu không thì nhìn vô cũng không khác gì imperative.
 
FP là trường phái viết code theo kiểu công thức toán học kiểu y=f(x), thậm chí ko dùng biến tạm (local var). Tuy nhiên ngôn ngữ FP nào cũng compile/inteprete/invoke một ngôn ngữ imperative. Ví dụ Clojure compile ra Java, F# hình như compile ra .NET IL, Elixir interprete ra Erlang code, Haskell invoke libC. Và ngôn ngữ imperative đó được cung cấp hint để optimize code lúc chạy, phổ biến nhất là tail call optimization (TCO) - như Elixir cách duy nhất để loop là dùng tail call.

Đấy là những ngôn ngữ thuần FP thì mới có TCO, còn imperative/hybrid như Java, Python, Javascript hầu như chỉ mượn FP syntax về dùng chứ ko offer được performance gain so với viết imperative truyền thống, chưa kể nếu lạm dụng dễ bị over allocate memory do ko có TCO, và tệ hơn nếu ko có lazy init. Hai cái ví dụ của thớt chỉ show off được FP syntax thôi chứ đấy ko phải là selling point của FP.

P/S: Tui dùng Elixir 7 tháng, đang áp dụng một ít FP principle trong Python (no side-effect, no class instance - anti-OOP) nhưng ko thể dùng hoàn toàn do vướng vụ allocate memory (vd Pandas dataframe) với cần closure để refactor complex code.
 
Last edited:
FP là trường phái viết code theo kiểu công thức toán học kiểu y=f(x), thậm chí ko dùng biến tạm (local var). Tuy nhiên ngôn ngữ FP nào cũng compile/inteprete/invoke một ngôn ngữ imperative. Ví dụ Clojure compile ra Java, F# hình như compile ra .NET IL, Elixir interprete ra Erlang code, Haskell invoke libC. Và ngôn ngữ imperative đó được cung cấp hint để optimize code lúc chạy, phổ biến nhất là tail call optimization (TCO) - như Elixir cách duy nhất để loop là dùng tail call.

Đấy là những ngôn ngữ thuần FP thì mới có TCO, còn imperative/hybrid như Java, Python, Javascript hầu như chỉ mượn FP syntax về dùng chứ ko offer được performance gain so với viết imperative truyền thống, chưa kể nếu lạm dụng dễ bị over allocate memory do ko có TCO, và tệ hơn nếu ko có lazy init. Hai cái ví dụ của thớt chỉ show off được FP syntax thôi chứ đấy ko phải là selling point của FP.

P/S: Tui dùng Elixir 7 tháng, đang áp dụng một ít FP principle trong Python (no side-effect, no class instance) nhưng ko thể dùng hoàn toàn do vướng vụ allocate memory (vd Pandas dataframe) với cần closure để refactor complex code.
khá đồng tình với thớt về cái các lang hiện tại chỉ mượn syntax của FP
 
Theo mình thấy thì người sử dụng FP có 2 trường phái:
  • Sử dụng FP 1 cách toàn diện: dùng ngôn ngữ FP, viết syntax FP toàn diện, lúc này bạn mất flexibility trong những trường hợp mà lẽ ra sẽ xử lý đơn giản hơn bằng cách dùng các ngôn ngữ phổ thông như Java/JS.
  • Dùng FP vì FP có những ưu điểm mà các cách viết không có được: dễ đọc, dễ viết, dễ test,... Mình dùng FP theo trường phái thứ 2. Có thể có những hạn chế như memory allocation bạn đề cập nhưng đó là vì đó là bug cần sửa của ngôn ngữ đó, chứ đó không phải là tính năng nên theo thời gian các ngôn ngữ sẽ dần hoàn thiện hơn nếu nhà phát triển thấy FP quan trọng trong ngôn ngữ của họ.

Bác có thể chỉ ra selling points của Elixir trong project bác làm được không, trong project em làm thì thấy có mấy điểm như sau:
  • Fault Tolerance: supervisor có thể quản lý các service con của nó và restart trong trường hợp crashing. Đây cũng có thể xem như là điểm yếu của nó khi cloud/micro-services lên ngôi, cloud/edge function có thể làm tốt việc này.
  • Concurrency: việc này thì các ngôn ngữ khác như Go cũng làm rất tốt
 
Theo mình thấy thì người sử dụng FP có 2 trường phái:
  • Sử dụng FP 1 cách toàn diện: dùng ngôn ngữ FP, viết syntax FP toàn diện, lúc này bạn mất flexibility trong những trường hợp mà lẽ ra sẽ xử lý đơn giản hơn bằng cách dùng các ngôn ngữ phổ thông như Java/JS.
  • Dùng FP vì FP có những ưu điểm mà các cách viết không có được: dễ đọc, dễ viết, dễ test,... Mình dùng FP theo trường phái thứ 2. Có thể có những hạn chế như memory allocation bạn đề cập nhưng đó là vì đó là bug cần sửa của ngôn ngữ đó, chứ đó không phải là tính năng nên theo thời gian các ngôn ngữ sẽ dần hoàn thiện hơn nếu nhà phát triển thấy FP quan trọng trong ngôn ngữ của họ.

Bác có thể chỉ ra selling points của Elixir trong project bác làm được không, trong project em làm thì thấy có mấy điểm như sau:
  • Fault Tolerance: supervisor có thể quản lý các service con của nó và restart trong trường hợp crashing. Đây cũng có thể xem như là điểm yếu của nó khi cloud/micro-services lên ngôi, cloud/edge function có thể làm tốt việc này.
  • Concurrency: việc này thì các ngôn ngữ khác như Go cũng làm rất tốt
2 cái điểm đấy là ưu điểm của Erlang VM (OTP), Elixir chỉ wrap nó thành Task/Supervisor cho dễ dùng thôi. Selling point chính của Elixir/Phoenix là intuitive nhưng explicit hơn Ruby/Rails (ko có magic var / function), hệ thống thư viện cũng phong phú và (cá nhân thấy) performance tốt hơn Rails (vd GraphQL backend) - thích hợp cho ai từ RoR muốn đổi gió, nhưng community ko mạnh bằng Node/Go/Python được. Hay nhưng rất ngách, cộng đồng ít, job ở VN thì rất hiếm.
 
FP giống như một mô hình để viết code vậy, hay giống như kiểu trường phái, văn phạm để tổ chức code. Lý tưởng thì nó chỉ dùng những hàm số, các biến tạm immutable và không dùng đến các biến thay đổi (mutable). Với cách thiết kế code như vậy thì các hàm có thể được thực thi ở môi trường multi thread, multi process (OS layer, System layer) mà không lo lắng về xung đột của các biến mutable nên cũng ko cần đặt các lock, mutex, từ đó tối ưu được hiệu năng CPU.
Hiện tại các ngôn ngữ phổ biến chỉ tích hợp các syntax của FP để viết code dễ hơn thôi. Hoặc có một số ngôn ngữ cố gắng thiết kế để chỉ dùng các biến immutable và văn phạm của FP, dù rằng bên dưới vẫn compile ra các mã máy ko phải FP, mục đích là tạo ra một biến thể dạng nhẹ của FP để tối ưu performance trong điều kiện gần với lý tưởng hơn. Nhưng trên thực tế thì performance vẫn ko được cải thiện bao nhiêu so với thời gian debug code.
Tôi ko hiểu rõ về FP nên nói một cách chính xác thì tôi không biết nhưng nói về FP thì đại loại là vậy.

Đi loanh quanh tôi gặp rất nhiều anh cố tình viết code theo kiểu thuần FP nhưng lại dùng var bừa bãi, viết bằng một ngôn ngữ ko hỗ trợ thuần FP như java hoặc scala, chạy trên một môi trường multithread của VM vốn tồn rại rất nhiều lock ẩn của VM cũng như OS. Vậy mà vẫn cứ đi quảng cáo đang dùng FP tránh side effect, context switching,... thế này thế nọ rồi bỉ bôi người khác xong còn ra vẻ khinh khỉnh như kiểu đang làm gì đó cao siêu. :feel_good::feel_good:
Về bản thân thì vẫn dùng syntax của FP để viết code cho gọn khi cần. Và dùng cái văn FP với cách viết code theo kiểu FP để phân biệt mấy đứa xạo lol, múa mõm. Cách nhận diện này cũng khá hiệu quả trong một thời gian nên giờ tranh thủ lật bài để gáy luôn vậy. :feel_good::feel_good:
 
Last edited:
FP giống như một mô hình để viết code vậy, hay giống như kiểu trường phái, văn phạm để tổ chức code. Lý tưởng thì nó chỉ dùng những hàm số, các biến tạm immutable và không dùng đến các biến thay đổi (mutable). Với cách thiết kế code như vậy thì các hàm có thể được thực thi ở môi trường multi thread, multi process (OS layer, System layer) mà không lo lắng về xung đột của các biến mutable nên cũng ko cần đặt các lock, mutex, từ đó tối ưu được hiệu năng CPU.
Hiện tại các ngôn ngữ phổ biến chỉ tích hợp các syntax của FP để viết code dễ hơn thôi. Hoặc có một số ngôn ngữ cố gắng thiết kế để chỉ dùng các biến immutable và văn phạm của FP, dù rằng bên dưới vẫn compile ra các mã máy ko phải FP, mục đích là tạo ra một biến thể dạng nhẹ của FP để tối ưu performance trong điều kiện gần với lý tưởng hơn. Nhưng trên thực tế thì performance vẫn ko được cải thiện bao nhiêu so với thời gian debug code.
Tôi ko hiểu rõ về FP nên nói một cách chính xác thì tôi không biết nhưng nói về FP thì đại loại là vậy.

Đi loanh quanh tôi gặp rất nhiều anh cố tình viết code theo kiểu thuần FP nhưng lại dùng var bừa bãi, viết bằng một ngôn ngữ ko hỗ trợ thuần FP như java hoặc scala, chạy trên một môi trường multithread của VM vốn tồn rại rất nhiều lock ẩn của VM cũng như OS. Vậy mà vẫn cứ đi quảng cáo đang dùng FP tránh side effect, context switching,... thế này thế nọ rồi bỉ bôi người khác xong còn ra vẻ khinh khỉnh như kiểu đang làm gì đó cao siêu. :feel_good::feel_good:
Về bản thân thì vẫn dùng syntax của FP để viết code cho gọn khi cần. Và dùng cái văn FP với cách viết code theo kiểu FP để phân biệt mấy đứa xạo lol, múa mõm. Cách nhận diện này cũng khá hiệu quả trong một thời gian nên giờ tranh thủ lật bài để gáy luôn vậy. :feel_good::feel_good:
Ai làm gì thím mà thím nhảy dựng lên vậy :LOL:
 
ví dụ về multi thread của bác chưa sắc nét lắm.

với trường hợp imperative thì phải thêm mutation state để dùng cơ chế lock khi có nhiều thread access/modify state đó. Sẽ thấy code nó phức tạp thế nào, chưa nói tới bắt đầu giảm hiệu năng, và các bug khó như deadlock.

với trường hợp dùng parallelStream, thì nó gọi là embarrassingly parallel, khá là basic.

Nghiên cứu mấy pattern như pipeline, map-reduce sẽ bắt đầu thấy sức mạnh của fp.

Mình chia sẻ thêm một số sức mạnh của vài ngôn ngữ
  • clojure: structural sharing, làm sao cho dữ liệu immutable nhưng vẫn đảm bảo đọc/sửa hiệu quả
  • erlang/elixir sẽ mạnh về distribute và fault tolerant chứ ko thuần concurency khi hiệu tại có nhiều ngôn ngữ đã làm được
  • haskell nó quá mạnh để compiler sắp xếp thứ tự chạy câu lệnh nếu nó ko phụ thuộc lẫn nhau => rất dể áp dụng parallel (cái này mình quên từ khóa rồi)
  • ocaml : typed effect, nó abstract các side effect thành các đối tượng giúp bạn dể tương tác như là value, đó là sự khác biệt giữa dùng callback style(imperative) và future/promise (declarative)

Nói chung mình nghiên cứu FP rất nhiều nhưng nó rộng quá, và nhiều khi mix nhiều thứ với nhau nên cũng khó xác định cái gì là FP. Nên giờ mình đi theo hướng giải quyết vấn đề, xem mỗi ngôn ngữ có feature gì, rồi khi gặp bài toán nào phù hợp thì tìm cách áp dụng thôi, không qua xem trọng công cụ nữa.

if all you have is a hammer, everything looks like a nail
 
Theo tìm hiểu của tôi thì Java 8 có đưa vào Lambda expression để hỗ trợ Functional Programming, nhưng có vẻ chỉ là đưa vào cho có thôi.Vì bản chất là vẫn bao lại bên dưới một lớp ẩn.Nhiều ông không đọc kỹ dùng bừa bãi thì dẫn đến việc instance nhiều Object dẫn đến việc tràn bộ nhớ heap ngay.Tốt nhất là nếu yêu Functional Programming thì nên dùng ngôn ngữ thuần FP như haskell,Elixir..vv.Chứ dùng FP trong những ngôn ngữ bê đê như Java,C# thì tràn bộ nhớ là điều không tránh khỏi
 
Theo tìm hiểu của tôi thì Java 8 có đưa vào Lambda expression để hỗ trợ Functional Programming, nhưng có vẻ chỉ là đưa vào cho có thôi.Vì bản chất là vẫn bao lại bên dưới một lớp ẩn.Nhiều ông không đọc kỹ dùng bừa bãi thì dẫn đến việc instance nhiều Object dẫn đến việc tràn bộ nhớ heap ngay.Tốt nhất là nếu yêu Functional Programming thì nên dùng ngôn ngữ thuần FP như haskell,Elixir..vv.Chứ dùng FP trong những ngôn ngữ bê đê như Java,C# thì tràn bộ nhớ là điều không tránh khỏi
Reduce việc đặt tên biến thôi fen, tràn bộ nhớ nếu có cũng ko liên quan :LOL:
 
Back
Top