thảo luận OOP hay FP

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

Các Voz Dev thuộc trường phái nào ?


  • Tổng số người bình chọn
    684
Đề Bài (phần 1):
thảo luận - OOP hay FP (https://voz.vn/t/oop-hay-fp.137397/page-10#post-4525643)

Đề Bài (phần 2):

Giả sử phần custom print (business logic để custom cái step 2 cho hàm Print) đã được làm và chạy ổn định, việc sửa code trong module này cực kỳ nguy hiểm, đòi hỏi nhiều chi phí để test lại. Trong đề bài phần này xem như không được thay đổi. Như vậy hiện tại có 2 modules fixed là Visualizer (phần core) và CustomerInfoManager (chứa loại khách hàng và custom print).

Với điều kiện trên Implement thêm 2 yêu cầu sau:
4. Đối với khách hàng có Age <= 30, lúc in Name không được in tên thật mà phải mask thành các dấu "******".

5. Cần ghi log trước và sau khi gọi custom print cho từng khách hàng. Format như sau, xem phần output cho rõ:
[Log]: Begin CustomPrint for customer name = <Name>
// In phần custom như lúc đợt 1
[Log]: End CustomPrint for customer name = <Name>
Just another implementation in F#

Mã:
module Visualizer =
    type CustomPrint = unit -> unit

    let print (customPrint: CustomPrint) =
        "\n#######################################################"
        |> System.Console.WriteLine

        customPrint ()

module CustomerInfoManager =
    open Visualizer

    type StandardCustomer = { Name: string; Age: int }
    type VipCustomer = { Name: string; Age: int; VipPoints: int }
    type InternalCustomer = { Name: string; Age: int; Department: string }

    type Customer =
        | Standard of StandardCustomer
        | VIP of VipCustomer
        | Internal of InternalCustomer

    module Customer =
        let name c =
            match c with
            | Standard s -> s.Name
            | VIP v -> v.Name
            | Internal i -> i.Name

        let age c =
            match c with
            | Standard s -> s.Age
            | VIP v -> v.Age
            | Internal i -> i.Age

        let printOtherInfo c =
            match c with
            | Standard _ -> "$$$$$$$$$$$$$$$$$$$$$$$"
            | VIP v -> sprintf "VipPoints = %i\n@@@@@@@@@@@@@@@@@@@@@@@" v.VipPoints
            | Internal i -> sprintf "Department = %s\n&&&&&&&&&&&&&&&&&&&&&&&" i.Department

        let customPrint c : CustomPrint =
            fun () ->
                sprintf "Name = %s\nAge = %i\n%s" (c |> name) (c |> age) (c |> printOtherInfo)
                |> System.Console.WriteLine

    module CustomerRepo =
        let getCustomers () =
            [ Standard { Name = "John Smith"; Age = 20 }
              VIP { Name = "Tim Batte"; Age = 30; VipPoints = 100 }
              Internal { Name = "Jenny Lars"; Age = 40; Department = "IT" } ]

module CrossCuttingConcerns =
    open CustomerInfoManager
    open Visualizer

    let maskNameIfSmallAge customPrint c : CustomPrint =
        let mask text c =
            match c with
            | Standard s -> { s with Name = text } |> Standard
            | VIP v -> { v with Name = text } |> VIP
            | Internal i -> { i with Name = text } |> Internal

        if c |> Customer.age <= 30 then c |> mask "******" else c
        |> customPrint

    let log customPrint c : CustomPrint =
        fun () ->
            sprintf "[Log]: Begin CustomPrint for customer name = %s" (c |> Customer.name)
            |> System.Console.WriteLine

            customPrint c ()

            sprintf "[Log]: End CustomPrint for customer name = %s" (c |> Customer.name)
            |> System.Console.WriteLine

module CustomerManagement =
    open CrossCuttingConcerns
    open CustomerInfoManager

    let main () =
        CustomerRepo.getCustomers ()
        |> List.map (Customer.customPrint |> maskNameIfSmallAge |> log)
        |> List.map Visualizer.print
        |> ignore

CustomerManagement.main ()

Mã:
module Visualizer =
    type CustomPrint = unit -> unit

    let print (customPrint: CustomPrint) =
        "\n#######################################################"
        |> System.Console.WriteLine

        customPrint ()

module CustomerInfoManager =
    open Visualizer

    type Customer(name, age) =
        member val Name = name with get, set
        member val Age = age with get, set
        abstract PrintOtherInfo: string
        default this.PrintOtherInfo = "$$$$$$$$$$$$$$$$$$$$$$$"

    type VipCustomer(name, age, vipPoints: int) =
        inherit Customer(name, age)
        member val VipPoints = vipPoints
        override this.PrintOtherInfo = sprintf "VipPoints = %i\n@@@@@@@@@@@@@@@@@@@@@@@" this.VipPoints

    type InternalCustomer(name, age, department) =
        inherit Customer(name, age)
        member val Department = department
        override this.PrintOtherInfo = sprintf "Department = %s\n&&&&&&&&&&&&&&&&&&&&&&&" this.Department

    module Customer =
        let customPrint (c: Customer) : CustomPrint =
            fun () ->
                sprintf "Name = %s\nAge = %i\n%s" c.Name c.Age c.PrintOtherInfo
                |> System.Console.WriteLine

    module CustomerRepo =
        let getCustomers () : Customer list =
            [ Customer("John Smith", 20)
              VipCustomer("Tim Batte", 30, 100)
              InternalCustomer("Jenny Lars", 40, "IT") ]

module CrossCuttingConcerns =
    open CustomerInfoManager
    open Visualizer

    let maskNameIfSmallAge customPrint (c: Customer) : CustomPrint =
        fun () ->
            let name = c.Name
            if c.Age <= 30 then c.Name <- "******" else ()

            customPrint c ()

            c.Name <- name

    let log customPrint (c: Customer) : CustomPrint =
        fun () ->
            sprintf "[Log]: Begin CustomPrint for customer name = %s" c.Name
            |> System.Console.WriteLine

            customPrint c ()

            sprintf "[Log]: End CustomPrint for customer name = %s" c.Name
            |> System.Console.WriteLine

module CustomerManagement =
    open CrossCuttingConcerns
    open CustomerInfoManager

    let main () =
        CustomerRepo.getCustomers ()
        |> List.map (Customer.customPrint |> maskNameIfSmallAge |> log)
        |> List.map Visualizer.print
        |> ignore

CustomerManagement.main ()

Kiểm tra tính đúng đắn của nghiệp vụ: phệt code vào đây

To be continued: Tại sao lại có hybrid approach?
 
Sửa lần cuối:
Cuối cùng thì vẫn không thấy ai đưa ra được ưu điểm vượt trội trong luận điểm của mình.4 năm rồi vẫn một cái vòng luẩn quẩn.Rốt cuộc thì OOP hơn FP ở mặt nào, Fp hơn OOP ở mặt nào.

Năm ngoái, Robert vừa cho ra mắt cuốn Functional Design: Principals, Patterns, and Practices. Trong phần preface có đoạn viết:
Many have expressed the opinion that object-oriented programming and functional programming are incompatible. These pages should prove otherwise. The programs, designs, and architectures that you see here will be an admixture of both functional and object-oriented concepts. It is my experience, and my strongly held opinion, that the two styles are entirely compatible and that good programmers can, and should, apply them together.

Sách có đưa ra các bài toán và lời giải theo lối OOP lẫn FP, phân tích ưu nhược với từng cách tiếp cận. Phía FP, Robert chọn Clojure vì cú pháp đơn giản, dễ học, mà giàu ngữ nghĩa (semantically rich).

Robert cũng dành một chương để bàn về data flow, và một chương về SOLID.
 
Sau 5 năm, mình bring lại debate với một vài tìm hiểu thêm về FP:


Một số tổng hợp của mình về FP và OOP:

1. Đối với kiểu dữ liệu, ADT trong FP an toàn hơn so với việc sử dụng nil pointer trong OOP vì nó tránh nil pointer exception bằng pattern matching và compiler check. Bạn không phải lo đang ngủ giữa đêm mà service bị crash do nil pointer exception vì một điều kiện nil không được check.

2. Đối với function, FP đưa ra hai khái niệm là pure và impure. Để giải quyết bài toán về biến global, FP sử dụng type system với các kiểu dữ liệu đặc biệt thay vì phụ thuộc vào user implementation như trong Encapsulation của OOP.

Việc sử dụng các kiểu dữ liệu đặc biệt là nhằm tạo constraint đối với việc thao tác dữ liệu và transparent thông tin ngay từ khi khởi tạo biến. Ví dụ: Tham số truyền vào chỉ có thể thay đổi được nếu kiểu dữ liệu của nó là mutable. Một biến nếu được đánh dấu là Rc và RefCell (trong Rust) thì chỉ an toàn khi mutate trong cùng một thread. Một biến nếu đánh dấu bằng kiểu dữ liệu Arc và Mutex có thể được share giữa các thread khác nhau.

FP hỗ trợ lambda function và closure, nhờ việc quản lý tốt side effect nên việc sử dụng lambda và closure trong FP sẽ an toàn hơn so với OOP.

NOTE: Các ngôn ngữ OOP copy tính năng lambda function của FP thường không nắm rõ về lambda calculus.

Nhờ việc sử dụng lambda calculus kết hợp với type system giúp các ngôn ngữ lập trình FP dễ dàng tối ưu hiệu năng hơn so với các ngôn ngữ lập trình khác (Các biến được thay đổi giá trị phải wrap trong kiểu đặc biệt RefCell hoặc Mutex, ngoài ra tất cả các biến khác đều là immutable. Không có biến nào bị thay đổi giá trị mà không có type annotation rõ ràng. Do vậy compiler có thể tự tin optimize code mà không lo giá trị của biến bị thay đổi ngoài ý muốn).


Các kiểu dữ liệu đặc biệt trong FP cũng inspire một vài cải tiến trong các thư viện và framework của frontend. Ví dụ: Elm Signal -> Redux / Redux Saga -> Preact Signal / Liquid Signal -> Svelte Runes / React Hooks.

3. Về tính đa hình, FP ưu thế hơn OOP với việc sử dụng ad-hoc polymorphism thay vì sử dụng subtyping polymorphism:
  • Function trong FP có thể polymorphism bằng cách sử dụng dynamic dispatch dựa trên kiểu dữ liệu của tham số truyền vào (typeclass). Do đó trong FP không cần phải invent ra thêm các khái niệm khác như class, method khiến cho code trong FP thống nhất hơn code trong OOP. Các khái niệm như inheritance, abstract class, interface cũng không cần thiết và được thay thế bởi typeclass.
  • Việc tách biệt giữa function, data type, typeclass cũng giúp tách biệt nhiệm vụ của từng thành phần trong ngôn ngữ lập trình thay vì gộp lại giống như class:
    • Data type: Để tái sử dụng cấu trúc dữ liệu
    • Function: Để tái sử dụng logic
    • Typeclass: Sử dụng cho tính đa hình, thay đổi logic function dựa vào kiểu dữ liệu truyền vào.
  • Ad-hoc polymorphism trong FP cho phép thay đổi implementation theo kiểu dữ liệu của nhiều tham số. Trong khi đó, sub-typing chỉ cho phép dynamic dispatching theo receiver. Ad-hoc polymorphism được thực hiện trong compile time trong khi đó dynamic dispatch dựa vào vtable và thực hiện tại runtime.
  • Ad-hoc polymorphism tránh được diamond problem như trong subtyping polymorphism
  • Tính đóng gói khi sử dụng ad-hoc polymorphism được đảm bảo
  • Người dùng có khả năng extend tính năng không chỉ cho các kiểu dữ liệu người dùng tự định nghĩa mà có thể extend cho cả các kiểu dữ liệu được cung cấp bởi thư viện mà không lo phá vỡ tính đóng gói hoặc naming collision (JS overcome việc naming collision bằng cách sử dụng Symbol. Ví dụ: Symbol.iterator).
4. Đối với type-level programming, một số ngôn ngữ lập trình của FP sử dụng cả lambda calculus trong type system (typed lambda calculus). Với việc lambda calculus thỏa mãn Turing complete thì về lý thuyết có thể encode được hầu hết các thể loại constraint vào trong type system của FP. Về thực tế việc encode sẽ phức tạp hơn vì còn phụ thuộc vào implementation.

Một vài ví dụ về type-level programming trong FP:
  • Cho phép encode các trạng thái của HTTP protocol trong type system
  • Type system của FP cover lambda cube: cho phép tham số của kiểu dữ liệu có thể là giá trị, term hoặc kiểu dữ liệu khác. Có thể encode nhiều thể loại constraint như số lượng phần tử của mảng hoặc chiều sâu của cây dữ liệu nhị phân (Haskell/Idris).
  • Rust cho phép encode một vài constraint về việc quản lý bộ nhớ bằng việc sử dụng hệ thống lifetime.
  • Haskell sử dụng Monad để đảm bảo việc các hàm có thực hiện side-effect đều phải trả về dữ liệu kiểu IO hoặc kiểu dữ liệu Monad tương ứng (Ví dụ: STM trong Haskell). Trong Elm thì sử dụng Commands và Subscriptions (Update) cho side-effect và sử dụng kiểu dữ liệu Html (View) cho hiển thị.

5. Một số ngôn ngữ lập trình FP sử dụng Preemptive Scheduling (như Elixir và Haskell) giúp việc phân chia task được fair hơn giữa các tiến trình.


Về tổng quan, các mô thức lập trình trong FP đa phần đều hướng tới việc lập trình an toàn với hiệu năng cao.
 
Sửa lần cuối:
Sau 5 năm, mình bring lại debate với một vài tìm hiểu thêm về FP:


Một số tổng hợp của mình về FP và OOP:

1. Đối với kiểu dữ liệu, ADT trong FP an toàn hơn so với việc sử dụng nil pointer trong OOP vì nó tránh nil pointer exception bằng pattern matching và compiler check. Bạn không phải lo đang ngủ giữa đêm mà service bị crash do nil pointer exception vì một điều kiện nil không được check.

2. Đối với function, FP đưa ra hai khái niệm là pure và impure. Để giải quyết bài toán về biến global, FP sử dụng type system với các kiểu dữ liệu đặc biệt thay vì phụ thuộc vào user implementation như trong Encapsulation của OOP.

Việc sử dụng các kiểu dữ liệu đặc biệt là nhằm tạo constraint đối với việc thao tác dữ liệu và transparent thông tin ngay từ khi khởi tạo biến. Ví dụ: Tham số truyền vào chỉ có thể thay đổi được nếu kiểu dữ liệu của nó là mutable. Một biến nếu được đánh dấu là Rc và RefCell (trong Rust) thì chỉ an toàn khi mutate trong cùng một thread. Một biến nếu đánh dấu bằng kiểu dữ liệu Arc và Mutex có thể được share giữa các thread khác nhau.

FP hỗ trợ lambda function và closure, nhờ việc quản lý tốt side effect nên việc sử dụng lambda và closure trong FP sẽ an toàn hơn so với OOP.

NOTE: Các ngôn ngữ OOP copy tính năng lambda function của FP thường không nắm rõ về lambda calculus.

Nhờ việc sử dụng lambda calculus kết hợp với type system giúp các ngôn ngữ lập trình FP dễ dàng tối ưu hiệu năng hơn so với các ngôn ngữ lập trình khác (Các biến được thay đổi giá trị phải wrap trong kiểu đặc biệt RefCell hoặc Mutex, ngoài ra tất cả các biến khác đều là immutable. Không có biến nào bị thay đổi giá trị mà không có type annotation rõ ràng. Do vậy compiler có thể tự tin optimize code mà không lo giá trị của biến bị thay đổi ngoài ý muốn).


Các kiểu dữ liệu đặc biệt trong FP cũng inspire một vài cải tiến trong các thư viện và framework của frontend. Ví dụ: Elm Signal -> Redux / Redux Saga -> Preact Signal / Liquid Signal -> Svelte Runes / React Hooks.

3. Về tính đa hình, FP ưu thế hơn OOP với việc sử dụng ad-hoc polymorphism thay vì sử dụng subtyping polymorphism:
  • Function trong FP có thể polymorphism bằng cách sử dụng dynamic dispatch dựa trên kiểu dữ liệu của tham số truyền vào (typeclass). Do đó trong FP không cần phải invent ra thêm các khái niệm khác như class, method khiến cho code trong FP thống nhất hơn code trong OOP. Các khái niệm như inheritance, abstract class, interface cũng không cần thiết và được thay thế bởi typeclass.
  • Việc tách biệt giữa function, data type, typeclass cũng giúp tách biệt nhiệm vụ của từng thành phần trong ngôn ngữ lập trình thay vì gộp lại giống như class:
    • Data type: Để tái sử dụng cấu trúc dữ liệu
    • Function: Để tái sử dụng logic
    • Typeclass: Sử dụng cho tính đa hình, thay đổi logic function dựa vào kiểu dữ liệu truyền vào.
  • Ad-hoc polymorphism trong FP cho phép thay đổi implementation theo kiểu dữ liệu của nhiều tham số. Trong khi đó, sub-typing chỉ cho phép dynamic dispatching theo receiver. Ad-hoc polymorphism được thực hiện trong compile time trong khi đó dynamic dispatch dựa vào vtable và thực hiện tại runtime.
  • Ad-hoc polymorphism tránh được diamond problem như trong subtyping polymorphism
  • Tính đóng gói khi sử dụng ad-hoc polymorphism được đảm bảo
  • Người dùng có khả năng extend tính năng không chỉ cho các kiểu dữ liệu người dùng tự định nghĩa mà có thể extend cho cả các kiểu dữ liệu được cung cấp bởi thư viện mà không lo phá vỡ tính đóng gói hoặc naming collision (JS overcome việc naming collision bằng cách sử dụng Symbol. Ví dụ: Symbol.iterator).
4. Đối với type-level programming, một số ngôn ngữ lập trình của FP sử dụng cả lambda calculus trong type system (typed lambda calculus). Với việc lambda calculus thỏa mãn Turing complete thì về lý thuyết có thể encode được hầu hết các thể loại constraint vào trong type system của FP. Về thực tế việc encode sẽ phức tạp hơn vì còn phụ thuộc vào implementation.

Một vài ví dụ về type-level programming trong FP:
  • Cho phép encode các trạng thái của HTTP protocol trong type system
  • Type system của FP cover lambda cube: cho phép tham số của kiểu dữ liệu có thể là giá trị, term hoặc kiểu dữ liệu khác. Có thể encode nhiều thể loại constraint như số lượng phần tử của mảng hoặc chiều sâu của cây dữ liệu nhị phân (Haskell/Idris).
  • Rust cho phép encode một vài constraint về việc quản lý bộ nhớ bằng việc sử dụng hệ thống lifetime.
  • Haskell sử dụng Monad để đảm bảo việc các hàm có thực hiện side-effect đều phải trả về dữ liệu kiểu IO hoặc kiểu dữ liệu Monad tương ứng (Ví dụ: STM trong Haskell). Trong Elm thì sử dụng Commands và Subscriptions (Update) cho side-effect và sử dụng kiểu dữ liệu Html (View) cho hiển thị.

5. Một số ngôn ngữ lập trình FP sử dụng Preemptive Scheduling (như Elixir và Haskell) giúp việc phân chia task được fair hơn giữa các tiến trình.


Về tổng quan, các mô thức lập trình trong FP đa phần đều hướng tới việc lập trình an toàn với hiệu năng cao.
Có nhiều cái để tranh cãi quá (nằm ở tất cả các mục từ 1 đến 5 luôn) mà giờ mình chán tận cổ việc tranh cãi online (thấy mất thời gian mà chẳng đi đến đâu).

Trước hết phải đi từ những cái cơ bản để xem xét việc so sánh FP và OOP khi nào thì có ý nghĩa (có những ngôn ngữ Functional và cũng đồng thời có OOP luôn, như là OCaml hay F#). Ví dụ ngay cái đầu tiên: so sánh ADT (algebraic data type) và NULL pointer.

Vấn đề ở đây không phải là NULL pointer là một đặc điểm của OOP đối nghịch với ADT (các ngôn ngữ không phải OOP như là C vẫn có NULL pointer, thậm chí lại rất phổ biến), mà là do tính chất rất đặc biệt của NULL pointer:
- pointer tới type A có thể nhận một giá trị đặc biệt (chính là NULL),
- pointer tới type A có thể bị dereferenced mà không qua type system (nghĩa là type checker không kiểm tra việc này),
-nếu việc dereference xảy ra khi giá trị pointer đang là NULL thì sẽ dẫn tới undefined behavior (crash, etc)

Trong FP không xảy ra điều này vì:
- pointer tới type A không thể nhận một giá trị đặc biệt (nói cách khác là pointer đều có đầy đủ provenance)
- để có giá trị đặc biệt thì phải chuyển sang dùng sum type (ví dụ Optional<T> = None | T), khi đó muốn thao tác trên NULL thì không còn có thể thao tác trên kiểu T nữa, mà phải thao tác trên kiểu Optional<T>, và việc thao tác này sẽ bị kiểm tra qua type system.

Ở cái thứ hai, trước khi nói đến pure và impure thì cần phải xác định thế nào là "side effect". Vì ngoài IO thì còn có nhiều cái có thể coi là side effect (ví dụ memory allocation, etc), tùy theo ta coi thế nào là side effect thì mới có định nghĩa thỏa đáng cho pure hay impure.

Cái thứ ba, FP dùng type class để thực hiện ad-hoc polymorphism qua parametric polymorphism, nhưng không phải chỉ có vậy vì FP có thể thực hiện trực tiếp sub-typing (ví dụ MLSub),...

Cái thứ bốn (theo mình là bạn viết lạc đề, kiểu như người ta hay nói "it's not even wrong").
 
Sửa lần cuối:
@Konstante Chưa hiểu ý của bạn lắm, mình viết 5 ý trên tai vì một số bạn trong thread không hiểu thắc mắc là (1) không biết tại sao FP có tính năng gì hơn OOP mà một số bạn cuồng FP thế coi Procedure programming < OOP < FP, (2) sau khi nghe xong thì thấy FP chả hơn gì OOP cả trong khi mọi người không hiểu rõ FP và OOP khác biệt chỗ nào.

Cơ mà đọc comment của bạn xong mình cảm giác giống như là bạn đang confirm các ý của mình là đúng hoặc chỉnh lại vì thấy có chút sai lệch so với suy nghĩ của bạn hơn là đang debate với mình. Cơ mà đọc xong mình cũng không biết comment của mình sai lệch gì so với comment của bạn cả 😃
 
@Konstante Chưa hiểu ý của bạn lắm, mình viết 5 ý trên tai vì một số bạn trong thread không hiểu thắc mắc là (1) không biết tại sao FP có tính năng gì hơn OOP mà một số bạn cuồng FP thế coi Procedure programming < OOP < FP, (2) sau khi nghe xong thì thấy FP chả hơn gì OOP cả trong khi mọi người không hiểu rõ FP và OOP khác biệt chỗ nào.

Cơ mà đọc comment của bạn xong mình cảm giác giống như là bạn đang confirm các ý của mình là đúng hoặc chỉnh lại vì thấy có chút sai lệch so với suy nghĩ của bạn hơn là đang debate với mình. Cơ mà đọc xong mình cũng không biết comment của mình sai lệch gì so với comment của bạn cả 😃
Ý mình là những cái bạn viết có quá nhiều thứ để có thể tranh luận, và mình không muốn tranh luận online (vì nó thường không dẫn đến đâu).

Mình thấy cách bạn viết nó không nhất quán, đang nói một ý chưa xong (và chưa đúng) thì chuyển sang ý khác (bạn cố gắng viết nhiều mà không tập trung viết đúng). Ví dụ nhé:

Bạn viết là "...ADT trong FP an toàn hơn so với việc sử dụng nil pointer trong OOP..."
Hai cái này (algebraic data type và nil pointer) chẳng liên quan gì tới nhau: nó không phải là cách tiếp cận của từng bên đối với một vấn đề cụ thể.

Hay là: "... Đối với function, FP đưa ra hai khái niệm là pure và impure..."
Khái niệm pure và impure không phải do functional language đưa ra mà nó tồn tại trong mọi ngôn ngữ lập trình. Thậm chí trong một bài khác của bạn, bạn còn cho rằng Haskell là pure (mình xin nói là cái này còn tùy định nghĩa).

Ngay câu tiếp theo "Để giải quyết bài toán về biến global....", mình xin hỏi là tại sao "biến global" lại là một bài toán cần giải quyết?

Tóm lại là gần như câu nào trong mọi ý từ 1 đến 5 bạn cũng viết sai (bạn còn chẳng biết là RefCell của Rust là runtime checking), càng nói về vấn đề cao cấp là sai càng nặng (bạn nói về những cái rất kinh như là lambda cube), mà cái sai quá hiển nhiên đến mức mình cảm thấy rất mệt mỏi khi đọc bài viết của bạn.
 
Sửa lần cuối:
1.

Bạn viết là "...ADT trong FP an toàn hơn so với việc sử dụng nil pointer trong OOP..."
Hai cái này (algebraic data type và nil pointer) chẳng liên quan gì tới nhau: nó không phải là cách tiếp cận của từng bên đối với một vấn đề cụ thể.

Ok phần này mình giải thích chưa rõ, để mình giải thích lại:

Trong các ngôn ngữ khác pointer được sử dụng với hai mục đích:
  • Một là để kiểm soát việc allocation (Rust sẽ dùng kiểu Box riêng)
  • Hai là để đánh dấu một biến là optional (Rust sẽ dùng kiểu Option)
Trong ví dụ của mình, mình đang sử dụng pointer với nghĩa thứ hai. Ví dụ một struct A trong Golang:

Mã:
type Payment struct {
    Amount Decimal
}

Khi sử dụng thì người dùng sẽ truy cập biến như sau:

Mã:
amount := payment.Amount.String()

Sau một thời gian người viết thư viện muốn chuyển giá trị Amount thành optional và chuyển biến Amount thành pointer:

Mã:
type Payment struct {
    Amount *Decimal
}

Đoạn code trên sau khi compile lại vẫn pass và khi chạy trên production thì sẽ bị lỗi null pointer exception do không check điều kiện null (việc này mình đã từng gặp khi sử dụng lib của Stripe trong Go).

Nếu sử dụng kiểu dữ liệu Option của ADT thì sẽ không gặp phải vấn đề trên vì việc sử dụng ADT được đảm bảo bởi pattern matching và compiler check.

Mình lấy ví dụ này vì mình thấy nhiều bạn lấy ví dụ là FP có ADT nhưng chưa nêu được ưu điểm của việc sử dụng ADT so với các tính năng của các ngôn ngữ lập trình khác. Pointer chỉ là một ví dụ để so sánh mình so sánh với ADT, mình hoàn toàn có thể sử dụng ví dụ về enum để thay thế. Nếu bạn thấy bạn có ví dụ nào rõ ràng hơn về ưu điểm của ADT thì có thể chia sẻ thay mình.

2.

Ở cái thứ hai, trước khi nói đến pure và impure thì cần phải xác định thế nào là "side effect". Vì ngoài IO thì còn có nhiều cái có thể coi là side effect (ví dụ memory allocation, etc), tùy theo ta coi thế nào là side effect thì mới có định nghĩa thỏa đáng cho pure hay impure.

Hay là: "... Đối với function, FP đưa ra hai khái niệm là pure và impure..."
Khái niệm pure và impure không phải do functional language đưa ra mà nó tồn tại trong mọi ngôn ngữ lập trình. Thậm chí trong một bài khác của bạn, bạn còn cho rằng Haskell là pure (mình xin nói là cái này còn tùy định nghĩa).

Quan điểm của mình là pure và impure có định nghĩa khá rõ ràng nên mình sẽ không bàn cãi gì về việc này:


Mình chưa từng nói Haskell là purely functional programming language. Cái mình nói là trước đây Haskell được thiết kế là purely functional programming language, nhưng thiết kế này gây ra khá nhiều hạn chế cho Haskell ví dụ như không thực hiện được I/O và các side-effect khác, và năm 1996 hạn chế này đã được xử lý bằng Monadic I/O. Sau khi thêm Monadic I/O thì Haskell không còn pure nữa mặc dù khi search về Haskell thì mọi người vẫn để ý dòng chữ Haskell được promote là purely functional programming language.

Nếu bạn thấy mình nói sai ở thread nào khác trước đấy bạn có thể quote lại giúp mình.

1763399457239.png


1763399482681.png


1763399511292.png



Ngoài ra mình nói là pure function là lý thuyết được đưa ra từ các ngôn ngữ functional programming, chứ mình không nói là lý thuyết về pure function không thể áp dụng trong các ngôn ngữ khác (như cách mình đang áp dụng với Go). Bằng chứng là nếu bạn nhìn vào các link dẫn nguồn của Wikipedia về pure function, bạn sẽ thấy chủ yếu nguồn dẫn đều là từ các ngôn ngữ lập trình functional hoặc Haskell.

Có thể assumption này của mình là sai, nếu bạn đưa ra được viện dẫn nào chính xác hơn thì mình sẽ sửa lại.

1763399542671.png


4.

Ngay câu tiếp theo "Để giải quyết bài toán về biến global....", mình xin hỏi là tại sao "biến global" lại là một bài toán cần giải quyết?

Do biến được khởi tạo ở global scope nên bất cứ hàm nào cũng có thể truy cập và thay đổi giá trị cả biến ngoài ý muốn, điều này dẫn đến một vài vấn đề sau:

  • Bất cứ hàm nào cũng có thể truy cập và thay đổi giá trị bên trong biến global. Do vậy biến global có thể bị thay đổi ngoài ý muốn và rất khó để debug
  • Compiler không thể optimize được code vì không biết biến global có thể được truy cập từ những hàm nào
  • Khi chạy multi-thread, việc truy cập vào cùng một biến có thể xảy ra data race

Cách OOP giải quyết vấn đề này là thay vì định nghĩa biến ở global scope, người dùng sẽ phải encapsulate biến thành các properties bên trong class. Tuy nhiên, việc này sẽ chỉ giúp giải quyết được vấn đề đầu tiên. Nói cách khác, vấn đề mới chỉ được chuyển từ global scope sang class scope. Các vấn đề vẫn còn sẽ là:

  • Trong cùng một class, bất cứ method nào cũng có thể truy cập được vào các properties. Sub-class cũng có thể truy cập được vào các properties. Điều này dẫn đến việc phải sinh ra thêm các access modifiers
  • Các vấn đề liên quan đến việc compiler không optimize được code, hoặc có khả năng xảy ra data-race khi các properties được truy cập ngẫu nhiên bởi các method được gọi trong các thread khác nhau vẫn được giữ nguyên.

Trong FP điều này được giải quyết như sau:

  • Mặc định các biến trong FP là immutable, các hàm trong FP là pure function nên các side-effect không thể xảy ra
  • Để thực hiện được side-effect người dùng sẽ phải sử dụng đến các kiểu dữ liệu đặc biệt như Rc, RefCell, Mutex, Arc. Tuy nhiên disclaimer, nếu người dùng sử dụng Rc + RefCell code sẽ không được đảm bảo chạy an toàn trong môi trường multi-thread. Nếu người dùng sử dụng Mutex, Arc thì code sẽ được đảm bảo chạy an toàn trong môi trường multi-thread.
  • Code có thể được dễ dàng được tối ưu bởi compiler vì mặc định toàn bộ code là pure và immutable. Các side-effect đã được annotate sẵn và wrap trong các kiểu dữ liệu mut, Rc, RefCell, Mutex, Arc nên không có việc một biến bị modify ngoài ý muốn mà compiler không biết.
  • Mặc định người viết lib (như DB connector, HTTP client) sẽ phải đảm bảo việc code được thực hiện an toàn trong môi trường multi-thread vì nếu code pure thì đã có compiler check, còn nếu code không pure thì sẽ phải được wrap trong kiểu dữ liệu như Rc, RefCell, Arc, Mutex.

Bạn biết tại sao mình thấy pure impure immutability lại liên quan đến biến global và encapsulation không? Nếu bạn nhìn lại ví dụ về impure function bạn sẽ thấy sự liên quan:

Mã:
a := 0

func increase() int {
    a = a + 1
    return a
}

for i := 0; i < 10; i++ {
    go func() {
        fmt.Println(increase())
    }()
}

bạn còn chẳng biết là RefCell của Rust là runtime checking

Mình chưa từng nói cơ chế Rc, RefCell không phải là cơ chế runtime. Cái mình nói là nếu code có side-effect thì sẽ phải được wrap trong kiểu dữ liệu đặc biệt như Rc, RefCell, Arc hoặc Mutex trước, nếu bạn không wrap side-effect trong các kiểu dữ liệu trên thì lúc compile compiler sẽ báo lỗi (ví dụ Send + Sync là cơ chế compiler check để đảm bảo biến của bạn được truyền an toàn trong môi trường multi-thread). Còn cái bạn nói đến là công đoạn sau khi biến đó được thực hiện chạy ở runtime, Rc, Arc, Mutex sẽ có cơ chế check riêng để đảm bảo code của bạn không bị data-race hoặc memory-leak.

5.

Tại sao mình đưa ví dụ về việc type-level programming trong FP vào? Bởi vì trước đây OOP được promote bởi type system và type-safe. Mình biết rằng nhiều bạn sẽ nghĩ về FP là như sau:

  • Functional programming sẽ chỉ là lối viết code, điểm khác nhau duy nhất giữa FP và OOP là thay vì dùng class thì mình sẽ dùng hàm
  • Vì FP chỉ sử dụng hàm nên type system của FP rất đơn giản (không có type system gì cả)
Chứ không nghĩ là type system trong FP phức tạp hơn rất nhiều nên mình đưa ra ví dụ để chia sẻ:
  • Lambda cube: Type system của Haskell là tổng hợp của các kiểu dữ liệu như sau: Nhận tham số là giá trị và trả về kiểu (dependent types), Nhận tham số là kiểu và trả về kiểu dữ liệu (generic), Nhận tham số là kiểu dữ liệu và trả về giá trị tương ứng.
  • Lambda calculus được đưa vào để thiết kế cho type system của Haskell. Nhờ việc đưa lambda calculus vào type system nên Haskell có thể xây dựng các kiểu dữ liệu higher-ranked types và chứng minh việc có thể hay không infer type của một số kiểu dữ liệu trong một số trường hợp nhất định. Từ đó đưa ra các trade-off khi xây dựng type-system.
1763398508950.png
 
Sửa lần cuối:
1.



Ok phần này mình giải thích chưa rõ, để mình giải thích lại:

Trong các ngôn ngữ khác pointer được sử dụng với hai mục đích:
  • Một là để kiểm soát việc allocation (Rust sẽ dùng kiểu Box riêng)
  • Hai là để đánh dấu một biến là optional (Rust sẽ dùng kiểu Option)
Trong ví dụ của mình, mình đang sử dụng pointer với nghĩa thứ hai. Ví dụ một struct A trong Golang:

Mã:
type Payment struct {
    Amount Decimal
}

Khi sử dụng thì người dùng sẽ truy cập biến như sau:

Mã:
amount := payment.Amount.String()

Sau một thời gian người viết thư viện muốn chuyển giá trị Amount thành optional và chuyển biến Amount thành pointer:

Mã:
type Payment struct {
    Amount *Decimal
}

Đoạn code trên sau khi compile lại vẫn pass và khi chạy trên production thì sẽ bị lỗi null pointer exception do không check điều kiện null (việc này mình đã từng gặp khi sử dụng lib của Stripe trong Go).

Nếu sử dụng kiểu dữ liệu Option của ADT thì sẽ không gặp phải vấn đề trên vì việc sử dụng ADT được đảm bảo bởi pattern matching và compiler check.

Mình lấy ví dụ này vì mình thấy nhiều bạn lấy ví dụ là FP có ADT nhưng chưa nêu được ưu điểm của việc sử dụng ADT so với các tính năng của các ngôn ngữ lập trình khác. Pointer chỉ là một ví dụ để so sánh mình so sánh với ADT, mình hoàn toàn có thể sử dụng ví dụ về enum để thay thế. Nếu bạn thấy bạn có ví dụ nào rõ ràng hơn về ưu điểm của ADT thì có thể chia sẻ thay mình.

2.





Quan điểm của mình là pure và impure có định nghĩa khá rõ ràng nên mình sẽ không bàn cãi gì về việc này:


Mình chưa từng nói Haskell là purely functional programming language. Cái mình nói là trước đây Haskell được thiết kế là purely functional programming language, nhưng thiết kế này gây ra khá nhiều hạn chế cho Haskell ví dụ như không thực hiện được I/O và các side-effect khác, và năm 1996 hạn chế này đã được xử lý bằng Monadic I/O. Sau khi thêm Monadic I/O thì Haskell không còn pure nữa mặc dù khi search về Haskell thì mọi người vẫn để ý dòng chữ Haskell được promote là purely functional programming language.

Nếu bạn thấy mình nói sai ở thread nào khác trước đấy bạn có thể quote lại giúp mình.

Xem tệp đính kèm 3340562

Xem tệp đính kèm 3340563

Xem tệp đính kèm 3340565


Ngoài ra mình nói là pure function là lý thuyết được đưa ra từ các ngôn ngữ functional programming, chứ mình không nói là lý thuyết về pure function không thể áp dụng trong các ngôn ngữ khác (như cách mình đang áp dụng với Go). Bằng chứng là nếu bạn mình vào các link dẫn nguồn của Wikipedia về pure function, bạn sẽ thấy chủ yếu nguồn dẫn đều là từ các ngôn ngữ lập trình functional hoặc Haskell.

Có thể assumption này của mình là sai, nếu bạn đưa ra được viện dẫn nào chính xác hơn thì mình sẽ sửa lại.

Xem tệp đính kèm 3340566

4.



Do biến được khởi tạo ở global scope nên bất cứ hàm nào cũng có thể truy cập và thay đổi giá trị cả biến ngoài ý muốn, điều này dẫn đến một vài vấn đề sau:

  • Bất cứ hàm nào cũng có thể truy cập và thay đổi giá trị bên trong biến global. Do vậy biến global có thể bị thay đổi ngoài ý muốn và rất khó để debug
  • Compiler không thể optimize được code vì không biết biến global có thể được truy cập từ những hàm nào
  • Khi chạy multi-thread, việc truy cập vào cùng một biến có thể xảy ra data race

Cách OOP giải quyết vấn đề này là thay vì định nghĩa biến ở global scope, người dùng sẽ phải encapsulate biến thành các properties bên trong class. Tuy nhiên, việc này sẽ chỉ giúp giải quyết được vấn đề đầu tiên. Nói cách khác, vấn đề mới chỉ được chuyển từ global scope sang class scope. Các vấn đề vẫn còn sẽ là:

  • Trong cùng một class, bất cứ method nào cũng có thể truy cập được vào các properties. Sub-class cũng có thể truy cập được vào các properties. Điều này dẫn đến việc phải sinh ra thêm các access modifiers
  • Các vấn đề liên quan đến việc compiler không optimize được code, hoặc có khả năng xảy ra data-race khi các properties được truy cập ngẫu nhiên bởi các method được gọi trong các thread khác nhau vẫn được giữ nguyên.

Trong FP điều này được giải quyết như sau:

  • Mặc định các biến trong FP là immutable, các hàm trong FP là pure function nên các side-effect không thể xảy ra
  • Để thực hiện được side-effect người dùng sẽ phải sử dụng đến các kiểu dữ liệu đặc biệt như Rc, RefCell, Mutex, Arc. Tuy nhiên disclaimer, nếu người dùng sử dụng Rc + RefCell code sẽ không được đảm bảo chạy an toàn trong môi trường multi-thread. Nếu người dùng sử dụng Mutex, Arc thì code sẽ được đảm bảo chạy an toàn trong môi trường multi-thread.
  • Code có thể được dễ dàng được tối ưu bởi compiler vì mặc định toàn bộ code là pure và immutable. Các side-effect đã được annotate sẵn và wrap trong các kiểu dữ liệu mut, Rc, RefCell, Mutex, Arc nên không có việc một biến bị modify ngoài ý muốn mà compiler không biết.
  • Mặc định người viết lib (như DB connector, HTTP client) sẽ phải đảm bảo việc code được thực hiện an toàn trong môi trường multi-thread vì nếu code pure thì đã có compiler check, còn nếu code không pure thì sẽ phải được wrap trong kiểu dữ liệu như Rc, RefCell, Arc, Mutex.

Bạn biết tại sao mình thấy pure impure immutability lại liên quan đến biến global và encapsulation không? Nếu bạn nhìn lại ví dụ về impure function bạn sẽ thấy sự liên quan:

Mã:
a := 0

func increase() int {
    a = a + 1
    return a
}

for i := 0; i < 10; i++ {
    go func() {
        fmt.Println(increase())
    }()
}



Mình chưa từng nói cơ chế Rc, RefCell không phải là cơ chế runtime. Cái mình nói là nếu code có side-effect thì sẽ phải được wrap trong kiểu dữ liệu đặc biệt như Rc, RefCell, Arc hoặc Mutex trước, nếu bạn không wrap side-effect trong các kiểu dữ liệu trên thì lúc compile compiler sẽ báo lỗi (ví dụ Send + Sync là cơ chế compiler check để đảm bảo biến của bạn được truyền an toàn trong môi trường multi-thread). Còn cái bạn nói đến là công đoạn sau khi biến đó được thực hiện chạy ở runtime, Rc, Arc, Mutex sẽ có cơ chế check riêng để đảm bảo code của bạn không bị data-race hoặc memory-leak.

5.

Tại sao mình đưa ví dụ về việc type-level programming trong FP vào? Bởi vì trước đây OOP được promote bởi type system và type-safe. Mình biết rằng nhiều bạn sẽ nghĩ về FP là như sau:

  • Functional programming sẽ chỉ là lối viết code, điểm khác nhau duy nhất giữa FP và OOP là thay vì dùng class thì mình sẽ dùng hàm
  • Vì FP chỉ sử dụng hàm nên type system của FP rất đơn giản (không có type system gì cả)
Chứ không nghĩ là type system trong FP phức tạp hơn rất nhiều nên mình đưa ra ví dụ để chia sẻ:
  • Lambda cube: Type system của Haskell là tổng hợp của các kiểu dữ liệu như sau: Nhận tham số là giá trị và trả về kiểu (dependent types), Nhận tham số là kiểu và trả về kiểu dữ liệu (generic), Nhận tham số là kiểu dữ liệu và trả về giá trị tương ứng.
  • Lambda calculus được đưa vào để thiết kế cho type system của Haskell. Nhờ việc đưa lambda calculus vào type system nên Haskell có thể xây dựng các kiểu dữ liệu higher-ranked types và chứng minh việc có thể hay không infer type của một số kiểu dữ liệu trong một số trường hợp nhất định. Từ đó đưa ra các trade-off khi xây dựng type-system.
Xem tệp đính kèm 3340554
wow thím giải thích kĩ vll, job thím có làm functional không? Mình hiếm thấy ai làm functional ở việt nam ngoài gương mặt kì cựu như erlang của viettel thì thấy có 1-2 FDI xài elixir phoenix, vs một fintech đà nẵng làm elixir
FfsqRRV.png
 
wow thím giải thích kĩ vll, job thím có làm functional không? Mình hiếm thấy ai làm functional ở việt nam ngoài gương mặt kì cựu như erlang của viettel thì thấy có 1-2 FDI xài elixir phoenix, vs một fintech đà nẵng làm elixir
FfsqRRV.png

Trước mình cũng từng có ý định vào trong Nam làm một vài job functional programming Haskell hay Elixir start from hoặc up to $3000 cơ mà mình không nhận vì một vài lý do:

  • Trước khi ra trường mình từng làm startup 0 đồng trải nghiệm không được tốt cho lắm
  • Lúc ra trường mình nghĩ mình cần trải nghiệm kĩ năng hơn là hype về tech nên mình chấp nhận việc làm Viettel để học quy trình hơn là việc theo đuổi một ngôn ngữ mặc dù lương ở Viettel không bằng lương mình ở Cốc Cốc làm trước đó. Mình cảm thấy carrier của mình vẫn quan trọng hơn nên mình không theo đuổi
  • Các ngôn ngữ FP mặc dù rất phát triển về mặt thiết kế ngôn ngữ nhưng về ecosystem thì còn immature và hơi rủi ro khi theo đuổi. Bản thân kinh nghiệm của mình lúc đó cũng không đủ để cover hoặc sử dụng thành thạo các ngôn ngữ FP như Haskell hay Elixir
Hiện tại thì mình đang stuck với các job Golang (mặc dù mình có thể code được nhiều ngôn ngữ khác nhau) nhưng mình vẫn quyết định tiếp tục theo đuổi vì:

  • Golang là một ngôn ngữ ít job nhưng rất niche, nếu theo đuổi vẫn có thể kiếm được job phù hợp.
  • Việc bạn biết nhiều ngôn ngữ mà không chuyên và chuyên về một ngôn ngữ là hoàn toàn khác nhau, nếu bạn không phải hàng expert có thể cover hoàn toàn các vấn đề tương tự phát sinh trong các ngôn ngữ khác (về cách sử dụng hay về ecosystem) thì không nên nhảy ngang.
  • Golang mặc dù có nhiều hạn chế nhưng vẫn có nhiều ưu điểm hơn nhiều ngôn ngữ khác rất nhiều.
  • Các job về Rust, Elixir và Haskell còn niche hơn Golang nên sẽ rủi ro hơn về mặt carrier nếu mình nhảy ngang sang các ngôn ngữ này.
Do đó Golang vẫn là ngôn ngữ chính của mình, Python và Node.js là ngôn ngữ phụ. Còn Elixir, Rust và Haskell là để phòng ngừa trong trường hợp có cơ hội và mình muốn thay đổi thử nghiệm thứ mới thì mình sẽ sử dụng.
 
Mình bắt đầu với lập trình hàm là từ việc học ở trường (trên trường dạy OCaml), sau đó khi làm việc thì dùng OCaml. Có viết vài side-project với Haskell và Fsharp.

Cái mà mình không thích ở lập trình hàm là hiệu năng của nó cực kỳ kém (so với C/C++ hay Rust) và để có hiệu năng tương tự thì rất mất công: hoặc phải cài đặt kiểu imperative, hoặc là phải dùng kỹ thuật rất đặc biệt.

Ví dụ cài đặt quicksort trên list (là cấu trúc dữ liệu cơ bản của FP tương ứng với cấu trúc mảng cơ bản trong các ngôn ngữ imperative) mà để có hiệu năng đúng O(n logn) thì hoàn toàn không đơn giản (mặc dù vẫn làm được). Lý do cơ bản là list là immutable và không cho phép truy cập ngẫu nhiên.

Và chỉ cần cấu trúc dữ liệu hơi phức tạp một chút (như là cài heap bằng cây nhị phân), thì từ cài đặt imperative + mutable rất đơn giản trên C/C++, lại trở nên rất khó khăn trong mọi ngôn ngữ FP.

Mới lập trình với FP thì rất dễ rơi vào bẫy hiệu năng kém này, nhìn ví dụ ngay trong bài đầu của thớt này và bên thớt học FP ở bên kia thì thấy ngay là cách viết code như vậy sẽ chậm hơn hẳn (về độ phức tạp thuật toán chứ chưa cần nói đến việc trình biên dịch sinh mã kém tối ưu hơn).

Ví dụ đoạn này:

Mã:
let main () =
        CustomerRepo.getCustomers ()
        |> List.map (Customer.customPrint |> maskNameIfSmallAge |> log)
        |> List.map Visualizer.print
        |> ignore

Vì Fsharp là strict evaluation nên mình tương đối chắc là đoạn code này sẽ phải duyệt danh sách customers hai lần (mà đáng ra chỉ phải duyệt một lần).

Hay đoạn này:

Mã:
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();

sẽ phải duyệt danh sách numbers ba lần.

Tất nhiên là có thể sửa bằng cách dùng fold một lần thôi nhưng như thế trông lại "không đẹp" (không có function composition, etc).
 
Sửa lần cuối:
Mình bắt đầu với lập trình hàm là từ việc học ở trường (trên trường dạy OCaml), sau đó khi làm việc thì dùng OCaml. Có viết vài side-project với Haskell và Fsharp.

Cái mà mình không thích ở lập trình hàm là hiệu năng của nó cực kỳ kém (so với C/C++ hay Rust) và để có hiệu năng tương tự thì rất mất công: hoặc phải cài đặt kiểu imperative, hoặc là phải dùng kỹ thuật rất đặc biệt.

Ví dụ cài đặt quicksort trên list (là cấu trúc dữ liệu cơ bản của FP tương ứng với cấu trúc mảng cơ bản trong các ngôn ngữ imperative) mà để có hiệu năng đúng O(n logn) thì hoàn toàn không đơn giản (mặc dù vẫn làm được). Lý do cơ bản là list là immutable và không cho phép truy cập ngẫu nhiên.

Và chỉ cần cấu trúc dữ liệu hơi phức tạp một chút (như là cài heap bằng cây nhị phân), thì từ cài đặt imperative + mutable rất đơn giản trên C/C++, lại trở nên rất khó khăn trong mọi ngôn ngữ FP.

Mới lập trình với FP thì rất dễ rơi vào bẫy hiệu năng kém này, nhìn ví dụ ngay trong bài đầu của thớt này và bên thớt học FP ở bên kia thì thấy ngay là cách viết code như vậy sẽ chậm hơn hẳn (về độ phức tạp thuật toán chứ chưa cần nói đến việc trình biên dịch sinh mã kém tối ưu hơn).

Ví dụ đoạn này:

Mã:
let main () =
        CustomerRepo.getCustomers ()
        |> List.map (Customer.customPrint |> maskNameIfSmallAge |> log)
        |> List.map Visualizer.print
        |> ignore

Vì Fsharp là strict evaluation nên mình tương đối chắc là đoạn code này sẽ phải duyệt danh sách customers hai lần (mà đáng ra chỉ phải duyệt một lần).

Hay đoạn này:

Mã:
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();

sẽ phải duyệt danh sách numbers ba lần.

Tất nhiên là có thể sửa bằng cách dùng fold một lần thôi nhưng như thế trông lại "không đẹp" (không có function composition, etc).
FSharp thì tôi mù không biết, nhưng đoạn code Java ở trên không duyệt 3 lần đâu. Mỗi element sẽ được thực hiện tuần tự các bước filter và map theo consumer là cái sum(). Mà tôi nhớ không nhầm thì sum() chính là thao tác fold (ở Java là reduce). Lúc nào sẽ rảnh ngồi đọc lại xem có nhớ đúng không.
Về bản chất performance với immutable value như bạn nói thì hoàn toàn nhất trí. Đoạn number * number này vào business thật mà là 2 cái struct data to tổ bố lại chỉ được dùng immutable là chết mất ngáp.
 
Ví dụ đoạn này:

Mã:
let main () =
        CustomerRepo.getCustomers ()
        |> List.map (Customer.customPrint |> maskNameIfSmallAge |> log)
        |> List.map Visualizer.print
        |> ignore

Vì Fsharp là strict evaluation nên mình tương đối chắc là đoạn code này sẽ phải duyệt danh sách customers hai lần (mà đáng ra chỉ phải duyệt một lần).
Hai lần List.map là hai lần duyệt danh sách - bạn nói đúng. Để có lazy evaluation thì đổi thành Seq.map + Seq.map + Seq.toList, hoặc Seq.map + Seq.iter và bỏ ignore.
 
Sửa lần cuối:

Thống kê chủ đề

Ngày tạo
Quynh 123,
Người trả lời cuối
HIL,
Trả lời
593
Lượt xem
97.327
Quay lại
Lên đầu trang