Những tính năng chức năng nào đáng giá một chút nhầm lẫn OOP cho những lợi ích mà chúng mang lại?


13

Sau khi học lập trình chức năng trong Haskell và F #, mô hình OOP có vẻ ngược với các lớp, giao diện, đối tượng. Những khía cạnh nào của FP tôi có thể mang lại cho công việc mà đồng nghiệp của tôi có thể hiểu được? Có bất kỳ phong cách FP nào đáng để nói chuyện với sếp của tôi về việc đào tạo lại đội của tôi để chúng tôi có thể sử dụng chúng không?

Các khía cạnh có thể có của FP:

  • Bất biến
  • Ứng dụng một phần và Currying
  • Hàm hạng nhất (con trỏ hàm / Đối tượng chức năng / Mẫu chiến lược)
  • Đánh giá lười biếng (và Monads)
  • Chức năng tinh khiết (không có tác dụng phụ)
  • Biểu thức (so với Tuyên bố - mỗi dòng mã tạo ra một giá trị thay vì hoặc ngoài việc gây ra tác dụng phụ)
  • Đệ quy
  • Kết hợp mẫu

Đây có phải là một thứ miễn phí mà chúng ta có thể làm bất cứ điều gì mà ngôn ngữ lập trình hỗ trợ đến giới hạn mà ngôn ngữ hỗ trợ không? Hoặc có một hướng dẫn tốt hơn?


6
Tôi đã có một kinh nghiệm tương tự. Sau khoảng 2 tháng đau đớn, tôi bắt đầu tìm thấy một sự cân bằng khá tốt của "thứ ánh xạ tới vật thể" và "thứ ánh xạ tới các chức năng". Nó giúp thực hiện một số hack nghiêm trọng trong một ngôn ngữ hỗ trợ cả hai. Cuối cùng, cả hai kỹ năng FP và OOP của tôi đều được cải thiện rất nhiều
Daniel Gratzer

3
FWIW, Linq vừa chức năng vừa lười biếng, và bạn có thể mô phỏng lập trình chức năng trong C # bằng cách sử dụng các phương thức tĩnh và tránh sự tồn tại của trạng thái.
Robert Harvey

1
Tại thời điểm này, bạn nên đọc sicp . Nó là miễn phí và được viết tốt. Nó cung cấp một so sánh tốt đẹp giữa hai mô hình.
Simon Bergot

4
FP và OOP cùng một lúc trong một số ý nghĩa trực giao và trong một số ý nghĩa kép. OOP là về sự trừu tượng hóa dữ liệu, FP là về (sự vắng mặt) của các tác dụng phụ. Việc bạn có tác dụng phụ hay không là trực giao với cách bạn trừu tượng hóa dữ liệu của mình. Ví dụ Lambda là cả chức năng và hướng đối tượng. Có, FP thường sử dụng các Kiểu dữ liệu trừu tượng, không phải các đối tượng, nhưng bạn cũng có thể sử dụng các đối tượng thay vào đó mà không phải là bất kỳ FP nào. OTOH, cũng có một mối quan hệ sâu sắc: một hàm là đẳng cấu với một đối tượng chỉ có một phương thức (đó là cách chúng được "làm giả" trong Java và được triển khai trong Java8, giật
Jörg W Mittag

3
Tôi nghĩ rằng khía cạnh mạnh mẽ nhất của câu hỏi của bạn phải làm với khả năng đọc. "Bao nhiêu phong cách lập trình chức năng phù hợp để mang đến làm việc tại một cửa hàng hướng đối tượng?" Hoặc những tính năng chức năng nào đáng giá một chút nhầm lẫn OOP cho những lợi ích mà chúng mang lại.
GlenPeterson

Câu trả lời:


13

Lập trình hàm là một mô hình khác với lập trình hướng đối tượng (một tư duy khác và cách suy nghĩ khác về các chương trình). Bạn đã bắt đầu nhận ra rằng ở đây có nhiều hơn một cách (hướng đối tượng) để suy nghĩ về các vấn đề và giải pháp của họ. Có những người khác (lập trình thủ tục và chung chung đến với tâm trí). Cách bạn phản ứng với kiến ​​thức mới này, cho dù bạn chấp nhận và tích hợp các công cụ và phương pháp tiếp cận mới này vào bộ kỹ năng của mình, sẽ quyết định liệu bạn có phát triển và trở thành một nhà phát triển lành nghề hơn, hoàn thiện hơn không.

Tất cả chúng ta đều được đào tạo để xử lý và thoải mái với một mức độ phức tạp nhất định. Tôi thích gọi đây là giới hạn hrair của một người (từ Watership Down, bạn có thể đếm cao đến mức nào). Đó là một điều tuyệt vời để mở rộng tâm trí của bạn, khả năng của bạn để xem xét nhiều lựa chọn hơn và có nhiều công cụ hơn để tiếp cận và giải quyết vấn đề. Nhưng đó là một sự thay đổi, và nó kéo bạn ra khỏi vùng thoải mái của bạn.

Một vấn đề bạn có thể gặp là bạn sẽ trở nên ít nội dung hơn để theo đám đông "mọi thứ là một đối tượng". Bạn có thể phải phát triển sự kiên nhẫn khi bạn làm việc với những người có thể không hiểu (hoặc muốn hiểu) tại sao một cách tiếp cận chức năng để phát triển phần mềm hoạt động tốt cho một số vấn đề nhất định. Cũng giống như một phương pháp lập trình chung hoạt động tốt cho các vấn đề nhất định.

Chúc may mắn!


3
Tôi muốn nói thêm rằng người ta có thể hiểu rõ hơn về một số khái niệm OOP truyền thống khi làm việc với các ngôn ngữ chức năng như Haskell hoặc Clojure. Cá nhân tôi nhận ra rằng đa hình thực sự là một khái niệm quan trọng (Giao diện trong Java hoặc kiểu chữ trong Haskell), trong khi tính kế thừa (cái mà tôi nghĩ là một khái niệm xác định) là một sự trừu tượng kỳ lạ.
wirrbel

6

Lập trình chức năng mang lại hiệu quả rất thực tế, thực tế, năng suất trong việc viết mã hàng ngày: một số tính năng thiên về sự căng thẳng, điều này rất tốt bởi vì bạn viết càng ít mã, bạn càng ít thất bại và càng ít phải bảo trì.

Là một nhà toán học, tôi thấy các công cụ chức năng ưa thích rất hấp dẫn, nhưng nó thường hữu ích khi thiết kế một ứng dụng: các cấu trúc này có thể mã hóa trong cấu trúc chương trình rất nhiều bất biến của chương trình, mà không biểu diễn các bất biến này bằng các biến.

Sự kết hợp yêu thích của tôi có thể trông khá tầm thường, tuy nhiên tôi tin rằng nó có tác động năng suất rất cao. Sự kết hợp này là Ứng dụng một phần và Chức năng CurryFirst Class mà tôi sẽ không bao giờ viết lại vòng lặp for : thay vào đó chuyển phần thân của vòng lặp sang chức năng lặp hoặc ánh xạ. Gần đây tôi đã được thuê cho một công việc C ++ và tôi nhận thấy một cách buồn cười, tôi hoàn toàn mất thói quen viết các vòng lặp!

Sự kết hợp của đệ quyKết hợp mẫu sẽ tiêu diệt sự cần thiết của mẫu thiết kế của Khách truy cập đó . Chỉ cần so sánh mã bạn cần lập trình một người đánh giá các biểu thức boolean: trong bất kỳ ngôn ngữ lập trình chức năng nào, đây phải là khoảng 15 dòng mã, trong OOP, điều đúng đắn cần làm là sử dụng mẫu thiết kế của Khách truy cập đó, biến ví dụ đồ chơi đó trong một bài luận sâu rộng. Ưu điểm là rõ ràng và tôi không nhận thấy bất kỳ sự bất tiện.


2
Tôi hoàn toàn đồng ý nhưng tôi đã bị đẩy lùi từ những người trong ngành có xu hướng đồng ý: Họ biết họ là khách truy cập, họ đã thấy và sử dụng nó nhiều lần nên mã trong đó là thứ họ hiểu và quen thuộc, cách tiếp cận khác mặc dù đơn giản và dễ dàng hơn là nước ngoài và do đó khó khăn hơn đối với họ. Đây là một thực tế đáng tiếc của ngành công nghiệp đã có hơn 15 năm OOP đâm vào mọi lập trình viên rằng hơn 100 dòng mã dễ hiểu hơn 10 đơn giản vì họ đã ghi nhớ hơn 100 dòng sau khi lặp lại chúng trong hơn một thập kỷ
Jimmy Hoffa

1
-1 - Mã ngắn hơn không có nghĩa là bạn đang viết mã "ít". Bạn đang viết cùng một mã bằng cách sử dụng ít ký tự hơn. Nếu bất cứ điều gì, bạn mắc nhiều lỗi hơn vì mã (thường) khó đọc hơn.
Telastyn

8
@Telastyn: Terse không giống như không thể đọc được. Ngoài ra, khối lượng lớn của nồi hơi cồng kềnh có cách riêng của họ là không thể đọc được.
Michael Shaw

1
@Telastyn Tôi nghĩ rằng bạn vừa chạm vào mấu chốt thực sự ở đây, vâng, có thể xấu và không thể đọc được, cồng kềnh có thể xấu và không thể đọc được, nhưng khóa không phải là độ dài thay đổi và mã viết khó hiểu. Điều quan trọng là như bạn đã đề cập ở trên số lượng hoạt động, tôi không đồng ý rằng số lượng hoạt động không tương quan với khả năng bảo trì, tôi nghĩ rằng làm ít việc hơn (với mã được viết rõ ràng) lợi cho khả năng đọc và bảo trì. Rõ ràng làm cùng một số thứ có chức năng chữ cái và biến tên sẽ không giúp đỡ, tốt FP cần đáng kể ít hoạt động vẫn còn viết rõ ràng
Jimmy Hoffa

2
@ user949300: nếu bạn muốn có một ví dụ hoàn toàn khác, thì ví dụ về Java 8 này như thế nào?: list.forEach(System.out::println);Theo quan điểm của FP, printlnlà một hàm lấy hai đối số, một mục tiêu PrintStreamvà một giá trị Objectnhưng phương thức Collectioncủa nó chỉ forEachmong đợi một hàm chỉ có một đối số có thể được áp dụng cho mọi yếu tố. Vì vậy, đối số đầu tiên được ràng buộc với thể hiện được tìm thấy trong việc System.outtạo ra một hàm mới với một đối số. Nó đơn giản hơnBiConsumer<…> c=PrintStream::println; PrintStream a1=System.out; list.forEach(a2 -> c.accept(a1, a2));
Holger

5

Bạn có thể phải hạn chế những phần kiến ​​thức bạn sử dụng trong công việc, cách Superman phải giả vờ là Clark Kent để tận hưởng những lợi ích của một cuộc sống bình thường. Nhưng biết nhiều hơn sẽ không bao giờ làm tổn thương bạn. Điều đó nói rằng, một số khía cạnh của Lập trình chức năng phù hợp với cửa hàng Hướng đối tượng và các khía cạnh khác có thể đáng nói với sếp của bạn để bạn có thể nâng cao trình độ kiến ​​thức trung bình của cửa hàng của mình và nhờ đó viết mã tốt hơn.

FP và OOP không loại trừ lẫn nhau. Nhìn vào Scala. Một số người cho rằng đó là điều tồi tệ nhất vì nó không trong sạch, nhưng một số người cho rằng đó là điều tốt nhất vì lý do tương tự.

Từng người một, đây là một số khía cạnh hoạt động tuyệt vời với OOP:

  • Hàm thuần túy (không có tác dụng phụ) - Mọi ngôn ngữ lập trình tôi biết đều hỗ trợ điều này. Chúng làm cho mã của bạn nhiều, dễ dàng hơn nhiều để lý do và nên được sử dụng bất cứ khi nào thực tế. Bạn không cần phải gọi nó là FP. Chỉ cần gọi nó là thực hành mã hóa tốt.

  • Tính không thay đổi: Chuỗi được cho là đối tượng Java được sử dụng phổ biến nhất và nó là bất biến. Tôi trình bày các Đối tượng Java không thay đổiBộ sưu tập Java không thay đổi trên blog của mình. Một số trong đó có thể được áp dụng cho bạn.

  • Các hàm hạng nhất (con trỏ hàm / Đối tượng chức năng / Mẫu chiến lược) - Java đã có phiên bản đột biến, đột biến của phiên bản 1.1 này với hầu hết các lớp API (và có hàng trăm) triển khai giao diện Listener. Runnable có lẽ là đối tượng chức năng được sử dụng phổ biến nhất. Các hàm hạng nhất hoạt động nhiều hơn để viết mã bằng ngôn ngữ không hỗ trợ chúng nguyên bản, nhưng đôi khi đáng để nỗ lực thêm khi chúng đơn giản hóa các khía cạnh khác của mã của bạn.

  • Đệ quy là hữu ích để xử lý cây. Trong một cửa hàng OOP, đó có lẽ là cách sử dụng đệ quy thích hợp chính. Sử dụng đệ quy cho vui trong OOP có lẽ nên được tán thành nếu không vì lý do nào khác ngoài hầu hết các ngôn ngữ OOP không có không gian ngăn xếp theo mặc định để biến điều này thành một ý tưởng hay.

  • Biểu thức (so với Câu lệnh - mỗi dòng mã tạo ra một giá trị thay vì hoặc ngoài việc gây ra tác dụng phụ) - Toán tử đánh giá duy nhất trong C, C ++ và Java là Toán tử Ternary . Tôi thảo luận về cách sử dụng phù hợp trên blog của tôi. Bạn có thể thấy bạn viết một số chức năng đơn giản có khả năng tái sử dụng và đánh giá cao.

  • Đánh giá lười biếng (và Monads) - chủ yếu giới hạn ở việc Khởi tạo lười biếng trong OOP. Không có các tính năng ngôn ngữ để hỗ trợ nó, bạn có thể tìm thấy một số API hữu ích, nhưng việc viết riêng của bạn rất khó. Tối đa hóa việc sử dụng luồng của bạn thay vào đó - xem các giao diện Nhà văn và Trình đọc để biết ví dụ.

  • Ứng dụng một phần và Currying - Không thực tế nếu không có chức năng hạng nhất.

  • Kết hợp mẫu - thường không được khuyến khích trong OOP.

Tóm lại, tôi không nghĩ công việc nên là miễn phí cho tất cả mọi thứ mà bạn có thể làm bất cứ điều gì mà ngôn ngữ lập trình hỗ trợ đến giới hạn mà ngôn ngữ hỗ trợ. Tôi nghĩ rằng khả năng đọc của đồng nghiệp nên là bài kiểm tra giấy quỳ của bạn cho mã được thuê. Nơi mà bạn thích nhất là tôi sẽ bắt đầu một số hoạt động giáo dục tại nơi làm việc để mở rộng tầm nhìn của đồng nghiệp.


Kể từ khi học FP, tôi đã quen với việc thiết kế mọi thứ để có giao diện trôi chảy, điều này dẫn đến kết quả gần giống với biểu thức, một hàm có một câu lệnh thực hiện rất nhiều thứ. Đó là cách gần nhất mà bạn thực sự có được nhưng đó là một cách tiếp cận tự nhiên từ độ tinh khiết khi bạn thấy bạn không còn bất kỳ phương thức void nào, sử dụng các phương thức mở rộng tĩnh trong C # hỗ trợ rất nhiều. Theo cách đó, điểm biểu hiện của bạn là điểm duy nhất tôi không đồng ý, mọi thứ khác đều được phát hiện với kinh nghiệm của bản thân tôi khi học FP và làm một công việc trong ngày .NET
Jimmy Hoffa

Điều thực sự làm phiền tôi trong C # bây giờ là tôi không thể sử dụng các đại biểu thay vì giao diện một phương thức vì 2 lý do đơn giản: 1. bạn không thể thực hiện lambas đệ quy mà không cần hack (gán cho null trước rồi đến lambda thứ hai) hoặc Y- tổ hợp (xấu như địa ngục trong C #). 2. không có bí danh loại nào bạn có thể sử dụng trong phạm vi dự án, vì vậy chữ ký của đại biểu của bạn khá nhanh chóng trở nên không thể quản lý được. Vì vậy, chỉ với 2 lý do ngu ngốc này, tôi không thể thưởng thức C # nữa, bởi vì điều duy nhất tôi có thể làm cho nó hoạt động là bằng cách sử dụng các giao diện một phương thức chỉ là công việc phụ không cần thiết.
Trident D'Gao

@bonomo Java 8 có java.util.feft.BiConsumer chung có thể hữu ích trong C #: public interface BiConsumer<T, U> { public void accept(T t, U u); }Có các giao diện chức năng hữu ích khác trong java.util.feft.
GlenPeterson

@bonomo Này, tôi hiểu rồi, đây là nỗi đau của Haskell. Bất cứ khi nào bạn đọc ai đó nói "Học FP làm cho tôi tốt hơn ở OOP" có nghĩa là họ đã học được Ruby hoặc thứ gì đó không thuần túy và khai báo như Haskell. Haskell làm cho nó rõ ràng OOP là mức độ vô dụng thấp. Nỗi đau lớn nhất mà bạn gặp phải là suy luận kiểu dựa trên ràng buộc là không thể giải quyết được khi bạn không ở trong hệ thống loại HM, do đó, suy luận kiểu dựa trên cosntraint hoàn toàn không được thực hiện: blog.msdn.com/b/ericlippert/archive / 2012/03/09 / Ngày
Jimmy Hoffa

1
Most OOP languages don't have the stack space for it Có thật không? Tất cả những gì bạn cần là 30 cấp đệ quy để quản lý hàng tỷ nút trong cây nhị phân cân bằng. Tôi khá chắc chắn rằng không gian ngăn xếp của tôi phù hợp cho nhiều cấp độ hơn thế này.
Robert Harvey

3

Ngoài lập trình chức năng và lập trình hướng đối tượng, còn có lập trình khai báo (SQL, XQuery). Học từng phong cách giúp bạn có được những hiểu biết mới và bạn sẽ học cách chọn công cụ phù hợp cho công việc.

Nhưng vâng, có thể rất bực bội khi viết mã bằng ngôn ngữ và biết rằng nếu bạn đang sử dụng một thứ khác, bạn có thể làm việc hiệu quả hơn cho một miền có vấn đề cụ thể. Tuy nhiên, ngay cả khi bạn đang sử dụng một ngôn ngữ như Java, có thể áp dụng các khái niệm từ FP cho mã Java của bạn, mặc dù theo các cách vòng. Ví dụ, khung Guava thực hiện một số điều này.


2

Là một lập trình viên tôi nghĩ bạn không bao giờ nên ngừng học hỏi. Điều đó nói rằng, thật thú vị khi học FP đang làm mờ các kỹ năng OOP của bạn. Tôi có xu hướng nghĩ về việc học OOP như học cách đi xe đạp; bạn không bao giờ quên làm thế nào để làm điều đó.

Khi tôi học được các đặc tính của FP, tôi thấy mình suy nghĩ toán học hơn và có được một viễn cảnh tốt hơn về các phương tiện mà tôi viết phần mềm. Đó là kinh nghiệm cá nhân của tôi.

Khi bạn có được nhiều kinh nghiệm hơn, các khái niệm lập trình cốt lõi sẽ khó mất hơn nhiều. Vì vậy, tôi khuyên bạn nên làm cho nó dễ dàng trên FP cho đến khi các khái niệm OOP hoàn toàn được củng cố trong tâm trí của bạn. FP là một sự thay đổi mô hình nhất định. Chúc may mắn!


4
Học OOP giống như học bò. Nhưng một khi bạn đứng vững trên đôi chân của mình, bạn sẽ chỉ dùng đến việc bò khi bạn quá say. Tất nhiên bạn không thể quên làm thế nào để làm điều đó, nhưng bạn thường không muốn. Và sẽ là một trải nghiệm đau đớn khi đi bộ với các trình thu thập thông tin khi bạn biết bạn có thể chạy.
SK-logic

@ SK-logic, tôi thích phép ẩn dụ của bạn
Trident D'Gao

@ SK-Logic: Học lập trình mệnh lệnh là gì? Kéo mình trên bụng?
Robert Harvey

@RobertHarvey cố gắng đào hang dưới lòng đất bằng một cái muỗng rỉ sét và một cỗ bài.
Jimmy Hoffa

0

Có rất nhiều câu trả lời hay, vì vậy tôi sẽ giải quyết một tập hợp con câu hỏi của bạn; cụ thể là, tôi rất tin tưởng vào tiền đề của câu hỏi của bạn, vì OOP và các tính năng chức năng không loại trừ lẫn nhau.

Nếu bạn sử dụng C ++ 11, có rất nhiều loại tính năng lập trình chức năng được tích hợp trong thư viện ngôn ngữ / tiêu chuẩn kết hợp tốt (khá) với OOP. Tất nhiên, tôi không chắc chắn ông chủ hoặc đồng nghiệp của bạn sẽ nhận được TMP tốt như thế nào, nhưng vấn đề là bạn có thể nhận được nhiều tính năng này ở dạng này hoặc dạng khác trong các ngôn ngữ không chức năng / OOP, như C ++.

Sử dụng các mẫu với đệ quy thời gian biên dịch dựa trên 3 điểm đầu tiên của bạn,

  • Bất biến
  • Đệ quy
  • Kết hợp mẫu

Trong đó các giá trị mẫu là bất biến (hằng số thời gian biên dịch), bất kỳ phép lặp nào đều được thực hiện bằng cách sử dụng đệ quy và phân nhánh được thực hiện bằng cách sử dụng khớp mẫu (nhiều hơn hoặc ít hơn), dưới dạng phân giải quá tải.

Đối với các điểm khác, việc sử dụng std::bindstd::functioncung cấp cho bạn ứng dụng chức năng một phần và các con trỏ hàm được tích hợp sẵn trong ngôn ngữ. Các đối tượng có thể gọi là các đối tượng chức năng (cũng như ứng dụng chức năng một phần). Lưu ý rằng bởi các đối tượng có thể gọi được, tôi có nghĩa là những đối tượng xác định chúng operator ().

Đánh giá lười biếng và các chức năng thuần túy sẽ khó hơn một chút; đối với các hàm thuần túy, bạn có thể sử dụng các hàm lambda chỉ thu theo giá trị, nhưng điều này không lý tưởng.

Cuối cùng, đây là một ví dụ về sử dụng đệ quy thời gian biên dịch với ứng dụng hàm một phần. Đó là một ví dụ hơi khó hiểu, nhưng nó cho thấy hầu hết các điểm trên. Nó sẽ liên kết đệ quy các giá trị trong một tuple nhất định với một hàm đã cho và tạo một đối tượng hàm (có thể gọi được)

#include <iostream>
#include <functional>

//holds a compile-time index sequence
template<std::size_t ... >
struct index_seq
{};

//builds the index_seq<...> struct with the indices (boils down to compile-time indexing)
template<std::size_t N, std::size_t ... Seq>
struct gen_indices
  : gen_indices<N-1, N-1, Seq ... >
{};

template<std::size_t ... Seq>
struct gen_indices<0, Seq ... >
{
    typedef index_seq<Seq ... > type;
};


template <typename RType>
struct bind_to_fcn
{
    template <class Fcn, class ... Args>
    std::function<RType()> fcn_bind(Fcn fcn, std::tuple<Args...> params)
    {
        return bindFunc(typename gen_indices<sizeof...(Args)>::type(), fcn, params);
    }

    template<std::size_t ... Seq, class Fcn, class ... Args>
    std::function<RType()> bindFunc(index_seq<Seq...>, Fcn fcn, std::tuple<Args...> params)
    {
        return std::bind(fcn, std::get<Seq>(params) ...);
    }
};

//some arbitrary testing function to use
double foo(int x, float y, double z)
{
    return x + y + z;
}

int main(void)
{
    //some tuple of parameters to use in the function call
    std::tuple<int, float, double> t = std::make_tuple(1, 2.04, 0.1);                                                                                                                                                                                                      
    typedef double(*SumFcn)(int,float,double);

    bind_to_fcn<double> binder;
    auto other_fcn_obj = binder.fcn_bind<SumFcn>(foo, t);
    std::cout << other_fcn_obj() << std::endl;
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.