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

  • Người tạo chủ đề Người tạo chủ đề scoubidoubidou
  • Ngày bắt đầu Ngày bắt đầu

scoubidoubidou

Senior 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.
 
Sửa lần cuối:
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:
 
Sửa lần cuối:
Nên đưa declarative vào để so sánh chứ nhỉ, một ví dụ nổi tiếng nhất là SQL: declarative.
 
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:
 
Mình có tìm hiểu tương đối về lịch sử các ngôn ngữ lập trình trên Wikipedia thì chủ yếu thấy các ngôn ngữ OOP đều bắt nguồn từ Simula. Điểm mới của Simula so với các ngôn ngữ lúc bấy giờ là đưa ra thêm các tính năng như class, inheritance, polymorphism, virtual procedues và coroutines. Các ngôn ngữ OOP sau này đều ăn theo Simula ở những điểm này.

Các ngôn ngữ lập trình FP thì bắt nguồn từ FP, ML, Miranda và sau mainstream hơn là Haskell. Nếu trong các ngôn ngữ FP thì mình thấy Haskell là tổng hợp các tính năng mạnh nhất của FP cho tới thời điểm hiện tại.

Điểm chung của các ngôn ngữ FP là chỉ dùng data structure và function (không dùng class) hoặc một số ngôn ngữ như Rust thì sẽ giới thiệu tính năng trait cũng gần tương tự class để việc implement quen thuộc hơn với những người từ OOP sang. Nhiều bạn sẽ hỏi là ô nếu trong FP chỉ dùng function như vậy thì liệu FP có hạn chế gì so với OOP hay không. Mình có thể khá chắc chắn khi trả lời là không.

Cơ mà nếu muốn chỉ ra tại sao FP lại ưu điểm hơn OOP thì chắc mình nghĩ sẽ cần mọi người đưa ra vấn đề trước là những hạn chế gì mà mọi người nghĩ sẽ gặp phải khi dùng FP thay vì dùng OOP. Mình có thể chỉ ra những tính năng FP có mà OOP không có, làm sao FP có thể cover được các tính năng của OOP chỉ với function (không có class) nhưng sẽ phải rất clear với mọi người về những tính năng mà mọi người nghĩ FP sẽ bị hạn chế, và liệu thực tế có thực sự xảy ra như vậy hay không.
 
Em nghĩ bác nên đối FP với OOP, declarative programming với imperative programming thì chuẩn hơn :D. Về phân loại FP, OOP, declarative programming, imperative programming, em nghĩ nói chính xác ra thì tất cả đều thuộc "programming paradigm", tức là

  • Cách viết code (ví dụ với FP là dùng pure functions, hạn chế side effects)
  • Một số tính năng của ngôn ngữ (ví dụ với FP là first class function, với OOP là inheritance)

Với em thấy code theo FP thích, nhưng đến lúc bắt buộc có side effects thì xử lý hơi nhọc (cần ném vào Result/Option) :stick:. Đến giờ thú thực là em chưa thấy OOP code gì ngon lắm ngoài việc làm UI tree layout: mỗi component đều kế thừa 1 base class render xong chạy render 1 component gốc là tất cả đều xuất hiện theo
 
Em nghĩ bác nên đối FP với OOP, declarative programming với imperative programming thì chuẩn hơn :D. Về phân loại FP, OOP, declarative programming, imperative programming, em nghĩ nói chính xác ra thì tất cả đều thuộc "programming paradigm", tức là

  • Cách viết code (ví dụ với FP là dùng pure functions, hạn chế side effects)
  • Một số tính năng của ngôn ngữ (ví dụ với FP là first class function, với OOP là inheritance)

Với em thấy code theo FP thích, nhưng đến lúc bắt buộc có side effects thì xử lý hơi nhọc (cần ném vào Result/Option) :stick:. Đến giờ thú thực là em chưa thấy OOP code gì ngon lắm ngoài việc làm UI tree layout: mỗi component đều kế thừa 1 base class render xong chạy render 1 component gốc là tất cả đều xuất hiện theo

Em chưa có cơ hội làm việc nhiều với cả hai nhưng mà em nghĩ FP mạnh trong việc tránh side effect aps dụng cho công việc đòi hỏi độ chính xác và an toàn dữ liệu cao như là làm pipeline, công thức toán... OOP lại mạnh trong khả năng mở rộng như là làm Game (thêm char mới, item mới,...)
 
Em chưa có cơ hội làm việc nhiều với cả hai nhưng mà em nghĩ FP mạnh trong việc tránh side effect aps dụng cho công việc đòi hỏi độ chính xác và an toàn dữ liệu cao như là làm pipeline, công thức toán... OOP lại mạnh trong khả năng mở rộng như là làm Game (thêm char mới, item mới,...)
Em chưa code game bao giờ mà bác khai sáng cho em cụ thể hơn các đoạn OOP dùng "hay" trong game được không :stick:? Liên quan là em đọc thường thấy làm game có cái Data-Oriented Design (nếu "láo" hơn tí thì gọi là Data-Oriented Programming): cố gắng cho dữ liệu dưới dạng bảng gần nhau => CPU lấy nhanh, render nhanh hơn => phù hợp cho game nghe hợp lý.

Với Data-Oriented Programming thực ra cũng là tên của 1 quyển sách không nổi lắm về... Clojure, không liên quan nhiều đến game. Em thấy nó là một cách triển khai khác của FP cũng gấc hay :beauty:
 
Em chưa code game bao giờ mà bác khai sáng cho em cụ thể hơn các đoạn OOP dùng "hay" trong game được không :stick:? Liên quan là em đọc thường thấy làm game có cái Data-Oriented Design (nếu "láo" hơn tí thì gọi là Data-Oriented Programming): cố gắng cho dữ liệu dưới dạng bảng gần nhau => CPU lấy nhanh, render nhanh hơn => phù hợp cho game nghe hợp lý.

Với Data-Oriented Programming thực ra cũng là tên của 1 quyển sách không nổi lắm về... Clojure, không liên quan nhiều đến game. Em thấy nó là một cách triển khai khác của FP cũng gấc hay :beauty:
design patterns chẳng hạn. Visitor có thể dùng để mở họp hành vi nhân vật mà k ảnh hưởng tới nhân vật khác :D
 
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ễ
Fact #1: Haskell không hẳn là ngôn ngữ declarative programming

Trong Haskell có do notation, bằng cách dùng do notation thì code Haskell có thể viết theo lối imperative thay vì declarative

Thường lối viết code mà sử dụng các hàm compose lại với nhau thành một chương trình thường bắt nguồn từ ngôn ngữ lập trình FP (tên gọi của ngôn ngữ lập trình) viết bởi John Backus. Ngôn ngữ lập trình này là function-level programming (khác với functional programming). Functional Programming cũng phát triển nhờ một phần vào FP nhưng chủ yếu style viết code compose các hàm còn đến từ Lambda Calculus.


Fact #2: Haskell không hẳn là pure functional programming language

Mọi người thường nhầm lẫn rằng functional programming language là phải pure. Tuy nhiên trước khi Haskell ra đời thì các ngôn ngữ functional như Miranda và ML không hề pure và vẫn chứa side effect. Đến thời Haskell thì term purely functional programming language mới thường được gắn chủ yếu với Haskell.
Haskell is a purely functional programming language that features referential transparency, immutability and lazy evaluation

Tuy nhiên, việc trở thành purely functional programming language cũng gây ra nhiều hạn chế đối với Haskell. Đến tháng 5 năm 1996 thì Monadic I/O với do notation được phát minh để giải quyết vấn đề trên (tức là vấn đề này đã được giải quyết từ 30 năm trước).

Trong "A History of Haskell" cũng có đoạn:

Purity is a big bet, with pervasive consequences. Unrestricted side
effects are undoubtedly very convenient. Lacking side effects,
Haskell’s input/output was initially painfully clumsy, which was a
source of considerable embarrassment. Necessity being the mother
of invention, this embarrassment ultimately led to the invention of
monadic I/O, which we now regard as one of Haskell’s main con-
tributions to the world, as we discuss in more detail in Section 7.

Điểm khác nhau giữa FP và OOP khi nhắc đến purity là:

  • Trong OOP, mặc định biến là mutable. Muốn đảm bảo việc cập nhật biến an toàn trong môi trường multi-thread thì phải sử dụng các synchronization primitive thủ công.
  • Trong FP, mặc định biến là immutable. Muốn biến cập nhật được và cập nhật biến an toàn thì sẽ có các kiểu dữ liệu Monadic (trong một số ngôn ngữ thì sẽ gọi thuật ngữ này là effectful-type system) cho các biến đó cho từng trường hợp khác nhau (tùy thuộc vào mức độ sử dụng và hiệu năng). Do đó mức độ đóng gói (encapsulation) sẽ cao hơn so với OOP.
Trong Haskell, để cập nhật biến an toàn trong môi trường multi-threading, mọi người có thể sử dụng thư viện STM. MVar sẽ phù hợp cho các trường hợp đơn giản hơn.


Fact #3: Functional programming language hoàn toàn có thể polymorphism mà chỉ sử dụng function (thay vì class)

Điểm nổi bật của OOP so với các ngôn ngữ khác lúc bấy giờ là có polymorphism (sub-typing polymorphism). Để thực hiện được polymorphism thì trong Simula 67 họ giới thiệu thêm một thuật ngữ mới là class. Các biến nếu là sub class thuộc cùng một class có thể thay thế được cho nhau.

Polymorphism khá là quan trọng, các ngôn ngữ hiện đại hiện nay (như Java hoặc Golang) muốn unit test được đều phải sử dụng polymorphism (interface).

Để tăng tính linh hoạt, các ngôn ngữ lập trình thường có hai hướng:

  • Xây dựng ngôn ngữ lập trình Weak Type. Nhược điểm là không type safe, chương trình dễ bị crash
  • Sử dụng polymorphism. Hiện tại các ngôn ngữ lập trình chủ yếu sử dụng OOP (Sub-Typing)

Các ngôn ngữ lập trình Functional Programming thì đi theo hướng khác:

  • Năm 1973, ngôn ngữ ML ra đời. Điểm nổi bật của ngôn ngữ lập trình ML là ML là ngôn ngữ lập trình đầu tiên giới thiệu về generic trong type system và có khả năng tự động infer type của biến theo signature của hàm thay vì phải annotate type cho từng biến một như Java (một tính năng khá advance thời bấy giờ).
  • Năm 1999, type class trong Haskell ra đời, cho phép implementation của hàm có thể thay đổi theo kiểu dữ liệu truyền vào. Do đó một hàm có thể hoàn toàn đa hình mà không cần phải sử dụng đến class.

Do vậy, trong Haskell có thể hoàn toàn đa hình mà không cần sử dụng đến class như OOP. Đó là điểm khác nhau giữa Haskell và các ngôn ngữ FP so với các ngôn ngữ OOP.

Javascript không giống Haskell vì trong Javascript function không thể thay đổi implementation theo type của biến truyền vào. Do vậy các bạn nào implement theo kiểu thay hết class trong chương trình bằng function thì đều sẽ sập ngay khi cần đến polymorphism. Một trong những điểm mà khiến cho cách này vẫn work là vì Javascript là Weak Type nên một biến truyền vào có thể nhận nhiều kiểu giá trị hoặc biến nhận vào là các hàm khác nhau có cùng chung type annotation.

Fact #4: Nhiều ngôn ngữ functional programming sử dụng fully preemptive scheduling

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

Điểm đặc biệt của các ngôn ngữ lập trình functional programming như Elixir hay Haskell là sử dụng M:N threading model (M green thread chạy trên N thread của OS). Go cũng sử dụng M:N threading model, tuy nhiên đến version 1.14 Go cũng mới hỗ trợ pre-emptive scheduling. Điểm hạn chế của Go là không có các primitive để có thể chủ động kiểm soát goroutine (signal hoặc terminate goroutine). Trong Elixir bạn hoàn toàn có thể spawn, communicate giữa các process, set timeout cho process nếu process chạy quá lâu và terminate nếu cần.


 
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.
Fen dùng elixir làm gì á? Phoenix, liveview hay ash, có dùng OTP chưa
 

Thống kê chủ đề

Ngày tạo
scoubidoubidou,
Người trả lời cuối
Konstante,
Trả lời
18
Lượt xem
2.583
Quay lại
Lên đầu trang