Hướng dẫn disadvantages of using functions in python - nhược điểm của việc sử dụng các hàm trong python

Người dân nói rằng thực hành Zen là khó khăn, nhưng có một sự hiểu lầm về lý do tại sao.

Show

Trong chương cuối cùng tôi đã xem xét lợi ích của lập trình chức năng, và như tôi đã chỉ ra, có khá nhiều. Trong chương này, tôi sẽ xem xét những nhược điểm tiềm năng của FP.

Giống như tôi đã làm trong chương trước, lần đầu tiên tôi sẽ đề cập đến những nhược điểm của chương trình chức năng nói chung.

  1. Viết các chức năng thuần túy là dễ dàng, nhưng kết hợp chúng thành một ứng dụng hoàn chỉnh là nơi mọi thứ trở nên khó khăn.
  2. Thuật ngữ toán học tiên tiến (monad, monoid, functor, v.v.) làm cho FP đáng sợ.
  3. Đối với nhiều người, đệ quy không cảm thấy tự nhiên.
  4. Bởi vì bạn có thể đột biến dữ liệu hiện có, thay vào đó bạn sử dụng một mẫu mà tôi gọi, cập nhật khi bạn sao chép.
  5. Các chức năng thuần túy và I/O don lồng thực sự pha trộn.
  6. Chỉ sử dụng các giá trị bất biến và đệ quy có khả năng dẫn đến các vấn đề về hiệu suất, bao gồm sử dụng RAM và tốc độ.

Sau đó, tôi sẽ xem xét các nhược điểm cụ thể hơn của chương trình chức năng trong Scala,:

  1. Bạn có thể trộn các kiểu FP và OOP.
  2. Scala không có một thư viện FP tiêu chuẩn.

1) Viết các chức năng thuần túy là dễ dàng, nhưng kết hợp chúng thành một ứng dụng hoàn chỉnh là nơi mọi thứ trở nên khó khăn

Viết một chức năng thuần túy nói chung là khá dễ dàng. Khi bạn có thể xác định chữ ký loại của mình, các hàm thuần túy sẽ dễ dàng hơn để viết vì không có các biến có thể thay đổi, đầu vào ẩn, trạng thái ẩn và I/O. Ví dụ: hàm

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}
9 trong mã này:

val possiblePlays = OffensiveCoordinator.determinePossiblePlays(gameState)

là một chức năng thuần túy, và đằng sau nó là hàng ngàn dòng mã chức năng khác. Viết tất cả các chức năng thuần túy này đã mất thời gian, nhưng nó không bao giờ khó khăn. Tất cả các chức năng tuân theo cùng một mẫu:

  1. Dữ liệu trong
  2. Áp dụng một thuật toán (để chuyển đổi dữ liệu)
  3. Dữ liệu ra

Điều đó đang được nói, phần khó là, Làm thế nào để tôi dán tất cả các chức năng thuần túy này với nhau theo kiểu FP? Câu hỏi đó có thể dẫn đến mã tôi đã trình bày trong chương đầu tiên:

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}

Như bạn có thể biết, khi lần đầu tiên bạn bắt đầu lập trình theo kiểu FP thuần túy, hãy dán các chức năng thuần túy với nhau để tạo ra một ứng dụng FP hoàn chỉnh là một trong những khối vấp ngã lớn nhất mà bạn gặp phải. Trong các bài học sau trong cuốn sách này, tôi hiển thị các giải pháp về cách dán các hàm tinh khiết cùng nhau vào một ứng dụng hoàn chỉnh.

2) Thuật ngữ toán học nâng cao làm cho FP đáng sợ

Tôi không biết về bạn, nhưng khi tôi lần đầu tiên nghe các thuật ngữ như tổ hợp, monoid, monad và functor, tôi không biết mọi người đang nói về cái gì. Và tôi đã được trả tiền để viết phần mềm từ đầu những năm 1990.

Như tôi đã thảo luận trong chương tiếp theo, các thuật ngữ như thế này đang đáng sợ, và yếu tố sợ hãi của Hồi giáo trở thành một rào cản đối với việc học FP.

Bởi vì tôi đề cập đến chủ đề này trong chương tiếp theo, tôi đã giành được viết thêm về nó ở đây.

3) Đối với nhiều người, đệ quy không cảm thấy tự nhiên

Một lý do tôi có thể không biết về các thuật ngữ toán học đó là vì bằng cấp của tôi là về kỹ thuật hàng không vũ trụ, không phải khoa học máy tính. Có thể vì lý do tương tự, tôi biết về đệ quy, nhưng không bao giờ phải sử dụng nó. Đó là, cho đến khi tôi trở nên nghiêm túc về việc viết mã FP thuần túy.

Như tôi đã viết trong "FP là gì?" Chương, điều xảy ra khi bạn chỉ sử dụng các hàm thuần túy và giá trị bất biến là bạn phải sử dụng đệ quy. Trong mã FP thuần túy, bạn không còn sử dụng các trường

val names = List("chris", "ed", "maurice")
0 với các vòng
val names = List("chris", "ed", "maurice")
1, vì vậy cách duy nhất để lặp qua các phần tử trong một bộ sưu tập là sử dụng đệ quy.

May mắn thay, bạn có thể học cách viết mã đệ quy. Nếu có một bí mật cho quá trình này, thì đó là cách học cách để suy nghĩ trong đệ quy. Khi bạn có được suy nghĩ đó và thấy rằng có các mô hình để các thuật toán đệ quy, bạn sẽ thấy rằng đệ quy trở nên dễ dàng hơn, thậm chí tự nhiên.

Hai đoạn trước tôi đã viết, Cách duy nhất để lặp lại các yếu tố trong một bộ sưu tập là sử dụng đệ quy, nhưng điều đó không đúng 100%. Ngoài việc đạt được một tư duy suy nghĩ đệ quy của người Viking, ở đây, một bí mật khác: Một khi bạn hiểu các phương pháp của Bộ sưu tập Scala, bạn đã giành được việc sử dụng đệ quy thường xuyên như bạn nghĩ. Theo cùng một cách mà các phương thức của Bộ sưu tập là các phương thức thay thế cho các vòng lặp

val names = List("chris", "ed", "maurice")
1 tùy chỉnh, chúng cũng thay thế cho nhiều thuật toán đệ quy tùy chỉnh.

Như một ví dụ về điều này, khi bạn lần đầu tiên bắt đầu làm việc với Scala và bạn có một

val names = List("chris", "ed", "maurice")
3 như thế này:

val names = List("chris", "ed", "maurice")

Nó tự nhiên để viết một biểu thức ____ 21/________ 25 như thế này:

val capNames = for (e <- names) yield e.capitalize

Như bạn sẽ thấy trong các bài học sắp tới, bạn cũng có thể viết một thuật toán đệ quy để giải quyết vấn đề này.

Nhưng một khi bạn hiểu các phương thức của Bộ sưu tập Scala, bạn sẽ biết rằng phương thức

val names = List("chris", "ed", "maurice")
6 là sự thay thế cho các thuật toán đó:

val capNames = fruits.map(_.e.capitalize)

Khi bạn thoải mái với các phương pháp của bộ sưu tập, bạn sẽ thấy rằng bạn tiếp cận với chúng trước khi bạn tiếp cận đệ quy.

Tôi viết nhiều hơn về đệ quy và các phương pháp của Bộ sưu tập Scala trong các bài học sắp tới.

4) Bởi vì bạn có thể thay đổi dữ liệu hiện có, thay vào đó bạn sử dụng một mẫu mà tôi gọi, cập nhật khi bạn sao chép

Trong hơn 20 năm, tôi đã viết mã mệnh lệnh, nơi dễ dàng - và cực kỳ phổ biến - để đột biến dữ liệu hiện có. Chẳng hạn, Ngày xửa ngày xưa, tôi có một cháu gái tên là Emily Emily Maness,:

val emily = Person("Emily", "Maness")

Sau đó, một ngày cô ấy kết hôn và họ của cô ấy đã trở thành những người khác, vì vậy, có vẻ hợp lý khi cập nhật họ của cô ấy, như thế này:

emily.setLastName("Wells")

Trong FP bạn không làm điều này. Bạn don lồng đột biến các đối tượng hiện có.

Thay vào đó, những gì bạn làm là (a) bạn sao chép một đối tượng hiện có vào một đối tượng mới và sau đó là bản sao của dữ liệu đang chảy từ đối tượng cũ sang đối tượng mới, bạn (b) cập nhật bất kỳ trường nào bạn muốn thay đổi theo Cung cấp các giá trị mới cho các trường đó, chẳng hạn như

val names = List("chris", "ed", "maurice")
7 trong ví dụ này:

Hướng dẫn disadvantages of using functions in python - nhược điểm của việc sử dụng các hàm trong python

Cách bạn cập nhật khi bạn sao chép trên mạng trong Scala/FP là với phương thức

val names = List("chris", "ed", "maurice")
8 đi kèm với các lớp trường hợp. Đầu tiên, bạn bắt đầu với một lớp trường hợp:

case class Person (firstName: String, lastName: String)

Sau đó, khi cháu gái của bạn được sinh ra, bạn viết mã như thế này:

val emily1 = Person("Emily", "Maness")

Sau đó, khi cô ấy kết hôn và thay đổi họ của mình, bạn viết điều này:

val emily2 = emily1.copy(lastName = "Wells")

Sau dòng mã đó,

val names = List("chris", "ed", "maurice")
9 có giá trị
val capNames = for (e <- names) yield e.capitalize
0.

Lưu ý: Tôi cố tình sử dụng tên biến

val capNames = for (e <- names) yield e.capitalize
1 và
val capNames = for (e <- names) yield e.capitalize
2 trong ví dụ này để làm rõ rằng bạn không bao giờ thay đổi biến ban đầu. Trong FP, bạn liên tục tạo các biến trung gian như
val capNames = for (e <- names) yield e.capitalize
3 và
val capNames = for (e <- names) yield e.capitalize
4 trong quá trình cập nhật trên mạng khi bạn sao chép quy trình, nhưng có các kỹ thuật FP làm cho các biến trung gian đó trong suốt.

Tôi cho thấy những kỹ thuật đó trong các bài học sắp tới.

“Cập nhật khi bạn sao chép” trở nên tồi tệ hơn với các đối tượng lồng nhau

Bản cập nhật của bạn khi bạn sao chép kỹ thuật không quá khó khi bạn làm việc với đối tượng

val capNames = for (e <- names) yield e.capitalize
5 đơn giản này, nhưng hãy nghĩ về điều này: Điều gì xảy ra khi bạn có các đối tượng lồng nhau, chẳng hạn như
val capNames = for (e <- names) yield e.capitalize
6 có
val capNames = for (e <- names) yield e.capitalize
5 có
val capNames = for (e <- names) yield e.capitalize
8, Và người đó muốn thêm một thẻ tín dụng mới, hoặc cập nhật một thẻ hiện có? .

Nói tóm lại, đây là một vấn đề thực sự dẫn đến một số mã trông khó chịu và nó trở nên xấu hơn với mỗi lớp lồng nhau. May mắn thay, các nhà phát triển FP khác đã gặp phải vấn đề này rất lâu trước khi tôi làm, và họ đã đưa ra cách để làm cho quá trình này dễ dàng hơn.

Tôi đề cập đến vấn đề này và giải pháp của nó trong một số bài học sau trong cuốn sách này.

5) Các chức năng thuần túy và I/O don lồng thực sự trộn lẫn

Như tôi đã viết trong bài học lập trình chức năng là gì, một hàm thuần túy là một hàm (a) có đầu ra chỉ phụ thuộc vào đầu vào của nó và (b) không có tác dụng phụ. Do đó, theo định nghĩa, bất kỳ chức năng nào liên quan đến những điều này đều không tinh khiết:

  • Tệp I/O
  • Cơ sở dữ liệu I/O
  • Internet I/O
  • Bất kỳ loại đầu vào UI/GUI nào
  • Bất kỳ chức năng nào biến đổi biến
  • Bất kỳ chức năng nào sử dụng các biến "ẩn"

Với tình huống này, một câu hỏi tuyệt vời là, Làm thế nào một ứng dụng FP có thể hoạt động mà không có những thứ này?

Câu trả lời ngắn gọn là những gì tôi đã viết trong Sách nấu ăn Scala và trong bài học trước: Bạn viết càng nhiều mã ứng dụng của mình theo kiểu FP càng tốt, và sau đó bạn viết một lớp I/O mỏng xung quanh bên ngoài mã FP , giống như đặt I/O đóng băng xung quanh một chiếc bánh FP FP:

Hướng dẫn disadvantages of using functions in python - nhược điểm của việc sử dụng các hàm trong python

Chức năng thuần khiết và không trong sạch

Trong thực tế, không có ngôn ngữ lập trình nào thực sự là tinh khiết, ít nhất là không phải theo định nghĩa của tôi. .

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}
0

Giải thích ngắn gọn về mã này là Haskell có loại

val capNames = for (e <- names) yield e.capitalize
9 mà bạn phải sử dụng làm trình bao bọc khi viết các hàm I/O. Điều này được thực thi bởi trình biên dịch Haskell.

Ví dụ,

val capNames = fruits.map(_.e.capitalize)
0 là hàm Haskell đọc một dòng từ stdin và trả về một loại tương đương với
val capNames = fruits.map(_.e.capitalize)
1 trong scala. Bất cứ khi nào một hàm Haskell trả về một cái gì đó được bọc trong một
val capNames = for (e <- names) yield e.capitalize
9, như
val capNames = fruits.map(_.e.capitalize)
1, chức năng đó chỉ có thể được sử dụng ở một số nơi trong một ứng dụng Haskell.

Nếu điều đó nghe có vẻ khó khăn và giới hạn, thì đó là. Nhưng hóa ra nó là một điều tốt.

Một số người ngụ ý rằng trình bao bọc

val capNames = for (e <- names) yield e.capitalize
9 này làm cho các chức năng đó thuần túy, nhưng theo tôi, điều này không đúng. Lúc đầu, tôi nghĩ rằng tôi đã bối rối về điều này-rằng tôi đã không hiểu điều gì đó-và sau đó tôi đã đọc trích dẫn này từ Martin Odersky trên scala-lang.org:

Các Monad IO không làm cho một chức năng tinh khiết. Nó chỉ làm cho nó rõ ràng rằng nó không trong sạch.

Hiện tại bạn có thể nghĩ về một ví dụ

val capNames = for (e <- names) yield e.capitalize
9 giống như một scala
val capNames = fruits.map(_.e.capitalize)
6. Chính xác hơn, bạn có thể nghĩ nó là một
val capNames = fruits.map(_.e.capitalize)
6 luôn trả về
val capNames = fruits.map(_.e.capitalize)
8, chẳng hạn như
val capNames = fruits.map(_.e.capitalize)
9 hoặc
val emily = Person("Emily", "Maness")
0.

Như bạn có thể tưởng tượng, chỉ vì bạn quấn một

val emily = Person("Emily", "Maness")
1 mà bạn có được từ thế giới bên ngoài bên trong
val emily = Person("Emily", "Maness")
2, điều đó không có nghĩa là
val emily = Person("Emily", "Maness")
1 đã giành chiến thắng. Chẳng hạn, nếu bạn nhắc tôi về tên của tôi, tôi có thể trả lời về Al Al Alvin, Alvin, và nếu bạn nhắc cháu gái của tôi về tên của cô ấy, cô ấy sẽ trả lời Emily Emily, và v.v. Tôi nghĩ rằng bạn sẽ đồng ý rằng
val emily = Person("Emily", "Maness")
4,
val emily = Person("Emily", "Maness")
5 và
val emily = Person("Emily", "Maness")
6 là các giá trị khác nhau.

Do đó, mặc dù (a) loại trở lại của các hàm I/O Haskell phải được bọc trong loại

val capNames = for (e <- names) yield e.capitalize
9 và (b) trình biên dịch Haskell chỉ cho phép
val capNames = for (e <- names) yield e.capitalize
9 ở một số nơi nhất định, chúng là các hàm không tinh khiết: chúng có thể trả về một Giá trị khác nhau mỗi lần chúng được gọi.

Lợi ích của loại Haskell từ val capNames = for (e <- names) yield e.capitalize9

Nó có một chút sớm trong cuốn sách này để tôi viết về tất cả những điều này, nhưng ... lợi ích chính của phương pháp Haskell

val capNames = for (e <- names) yield e.capitalize
9 là nó tạo ra một sự tách biệt rõ ràng giữa (a) các hàm thuần túy và (b) các hàm không tinh khiết. Sử dụng Scala để chứng minh ý tôi, tôi có thể nhìn vào chức năng này và biết từ chữ ký của nó rằng chức năng thuần túy của nó:

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}
1

Tương tự, khi tôi thấy rằng chức năng tiếp theo này trả về một cái gì đó trong trình bao bọc

val capNames = for (e <- names) yield e.capitalize
9, tôi biết từ chữ ký của nó rằng nó là một chức năng không tinh khiết:

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}
2

Điều đó thực sự rất tuyệt, và tôi viết thêm về điều này trong các bài học I/O của cuốn sách này.

Tôi đã thảo luận về đầu vào/đầu ra của UI/GUI trong phần này, nhưng tôi thảo luận nhiều hơn trong các trò chơi Tôi có nên sử dụng FP ở khắp mọi nơi không? phần tiếp theo.

6) Chỉ sử dụng các giá trị bất biến và đệ quy có thể dẫn đến các vấn đề về hiệu suất, bao gồm sử dụng RAM và tốc độ

Một tác giả có thể gặp rắc rối khi tuyên bố rằng một mô hình lập trình có thể sử dụng nhiều bộ nhớ hơn hoặc chậm hơn các phương pháp khác, vì vậy hãy để tôi bắt đầu phần này bằng cách rất rõ ràng:

Khi bạn lần đầu tiên viết một thuật toán FP đơn giản (Naive ngây thơ), có thể-chỉ có thể-rằng các giá trị bất biến và sao chép dữ liệu mà tôi đã đề cập trước đó có thể là một vấn đề hiệu suất.

Tôi trình bày một ví dụ về vấn đề này trong một bài đăng trên blog về các thuật toán Quicksort Scala. Trong bài viết đó, tôi chỉ ra rằng thuật toán cơ bản (Naive naive)) Thuật toán

emily.setLastName("Wells")
2 được tìm thấy trong Scala Scala bằng ví dụ, PDF sử dụng khoảng 660 MB RAM trong khi sắp xếp một mảng mười triệu số nguyên và chậm hơn bốn lần so với sử dụng phương pháp ____63 .

Phải nói rằng, điều quan trọng cần lưu ý là cách

emily.setLastName("Wells")
3 hoạt động. Trong Scala 2.12, nó chuyển trực tiếp
emily.setLastName("Wells")
5 đến
emily.setLastName("Wells")
6. Cách mà phương thức
emily.setLastName("Wells")
7 hoạt động thay đổi theo phiên bản Java, nhưng Java 8 gọi phương thức
emily.setLastName("Wells")
7 trong
emily.setLastName("Wells")
9. Mã trong phương thức đó (và một phương thức khác mà nó gọi) dài ít nhất 300 dòng và phức tạp hơn nhiều so với thuật toán
emily.setLastName("Wells")
2 đơn giản/ngây thơ mà tôi hiển thị.

Do đó, mặc dù đúng là thuật toán đơn giản, ngây thơ

emily.setLastName("Wells")
2 trong Scala Scala bằng ví dụ, PDF có những vấn đề về hiệu suất, tôi cần rõ ràng rằng tôi đang so sánh (a) một thuật toán rất đơn giản mà bạn có thể viết ban đầu, đến (b) một thuật toán được tối ưu hóa hiệu suất lớn hơn nhiều.

Tóm lại, trong khi đây là một vấn đề tiềm năng với mã FP đơn giản/ngây thơ, tôi cung cấp các giải pháp cho những vấn đề này trong một bài học có tiêu đề, lập trình chức năng và hiệu suất.

7) Nhược điểm của Scala/FP: Bạn có thể trộn các kiểu FP và OOP

Nếu bạn là một người theo chủ nghĩa thuần túy của FP, thì một nhược điểm của việc sử dụng lập trình chức năng trong Scala là Scala hỗ trợ cả OOP và FP, và do đó, nó có thể trộn hai kiểu mã hóa trong cùng một cơ sở mã.

Mặc dù đó là một nhược điểm tiềm năng, nhiều năm trước tôi đã biết về một triết lý có tên là Nhà House Rules, loại bỏ vấn đề này. Với các quy tắc nhà, các nhà phát triển kết hợp với nhau và đồng ý về một phong cách lập trình. Khi đạt được sự đồng thuận, đó là phong cách mà bạn sử dụng. Giai đoạn = Stage.

Như một ví dụ đơn giản về điều này, khi tôi sở hữu một công ty tư vấn lập trình máy tính, các nhà phát triển muốn có một phong cách mã hóa Java trông như thế này:

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}
3

Như được hiển thị, họ muốn niềng răng xoăn trên các dòng của riêng họ và mã được thụt vào bốn không gian. Tôi nghi ngờ rằng tất cả mọi người trong đội đều yêu thích phong cách đó, nhưng một khi chúng tôi đồng ý về nó, đó là nó.

Tôi nghĩ rằng bạn có thể sử dụng triết lý quy tắc nhà để nêu rõ phần nào của ngôn ngữ Scala mà tổ chức của bạn sẽ sử dụng trong các ứng dụng của bạn. Chẳng hạn, nếu bạn muốn sử dụng phong cách FP FP thuần túy nghiêm ngặt, hãy sử dụng các quy tắc tôi đặt ra trong cuốn sách này. Bạn luôn có thể thay đổi các quy tắc sau này, nhưng điều quan trọng là bắt đầu với một cái gì đó.

8) Hạn chế của Scala/FP: Scala không có thư viện FP tiêu chuẩn

Một nhược điểm tiềm năng khác đối với lập trình chức năng trong Scala là có một thư viện tích hợp để hỗ trợ một số kỹ thuật FP nhất định. Chẳng hạn, nếu bạn muốn sử dụng kiểu dữ liệu

val capNames = for (e <- names) yield e.capitalize
9 làm trình bao bọc xung quanh các chức năng Scala/FP không tinh khiết của bạn, thì có một loại được tích hợp vào các thư viện Scala tiêu chuẩn.

Để giải quyết vấn đề này, các thư viện độc lập như Scalaz, Mèo và các thư viện khác đã được tạo ra. Nhưng, trong khi các giải pháp này được xây dựng thành một ngôn ngữ như Haskell, chúng là các thư viện độc lập trong Scala.

Tôi thấy rằng tình huống này làm cho việc học Scala/FP trở nên khó khăn hơn. Chẳng hạn, bạn có thể mở bất kỳ cuốn sách Haskell nào và tìm một cuộc thảo luận về loại

val capNames = for (e <- names) yield e.capitalize
9 và các tính năng ngôn ngữ tích hợp khác, nhưng điều tương tự cũng không đúng với Scala. (Tôi thảo luận về điều này nhiều hơn trong các bài học I/O trong cuốn sách này.)

"Tôi có nên sử dụng FP ở mọi nơi không?"

THẬN TRỌNG: Một vấn đề với việc phát hành một cuốn sách một vài chương tại một thời điểm là các chương sau mà bạn sẽ hoàn thành việc viết sau một thời gian sau đó có thể có tác động đến nội dung trước đó. Đối với cuốn sách này, đó là trường hợp liên quan đến phần này. Tôi chỉ làm việc với các ví dụ nhỏ về lập trình phản ứng chức năng cho đến nay, vì vậy khi tôi tìm hiểu thêm về nó, tôi hy vọng rằng kiến ​​thức mới sẽ ảnh hưởng đến nội dung trong phần này. Do đó, một sự thận trọng: Phần này vẫn đang được xây dựng và có thể thay đổi đáng kể.

Sau khi tôi liệt kê tất cả các lợi ích của lập trình chức năng trong chương trước, tôi đã đặt câu hỏi, tôi có nên viết tất cả mã của mình theo kiểu FP không? Vào thời điểm đó, bạn có thể đã nghĩ, tất nhiên! Công cụ FP này nghe có vẻ tuyệt vời!

Bây giờ bạn đã thấy một số nhược điểm của FP, tôi nghĩ rằng tôi có thể cung cấp một câu trả lời tốt hơn.

1A) GUI và FP thuần túy không phù hợp

Phần đầu tiên của câu trả lời của tôi là tôi thích viết các ứng dụng Android và tôi cũng thích viết mã Java Swing và Javafx và giao diện giữa (a) các khung đó và (b) mã tùy chỉnh của bạn không phù hợp với FP .

Như một ví dụ về ý tôi, trong một trò chơi bóng đá Android mà tôi làm việc trong thời gian rảnh rỗi, khung trò chơi OOP mà tôi sử dụng cung cấp một phương pháp

case class Person (firstName: String, lastName: String)
4 mà tôi đã phải ghi đè để cập nhật màn hình:

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}
4

Bên trong phương pháp đó, tôi có rất nhiều mã vẽ Gui bắt buộc hiện đang tạo UI này:

Hướng dẫn disadvantages of using functions in python - nhược điểm của việc sử dụng các hàm trong python

Có một nơi dành cho mã FP tại thời điểm này. Khung hy vọng tôi sẽ cập nhật các pixel trên màn hình trong phương pháp này và nếu bạn đã từng viết bất cứ điều gì như trò chơi video, bạn sẽ biết rằng để đạt được hiệu suất tốt nhất - và tránh nhấp nháy màn hình - thường thì tốt nhất là chỉ cập nhật các pixel cần phải thay đổi. Vì vậy, đây thực sự là một phương pháp cập nhật trên mạng, trái ngược với một phương pháp hoàn toàn vẽ lại cho phương pháp màn hình.

Hãy nhớ rằng, các từ như cập nhật trên mạng và người đột biến không có trong từ vựng FP.

Các khách hàng dày khác của người Viking, các khung GUI như Swing và Javafx có các giao diện tương tự, nơi chúng là OOP và bắt buộc theo thiết kế. Một ví dụ khác, tôi đã viết một trình soạn thảo văn bản nhỏ mà tôi đặt tên là Alp ALPAD, và tính năng chính của nó là nó cho phép tôi dễ dàng thêm và xóa các tab để giữ ít ghi chú được tổ chức:

Hướng dẫn disadvantages of using functions in python - nhược điểm của việc sử dụng các hàm trong python

Cách bạn viết mã swing như thế này là lần đầu tiên bạn tạo một

case class Person (firstName: String, lastName: String)
5:

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}
5

Sau khi được tạo, bạn giữ khung được bảng đó tồn tại trong toàn bộ cuộc sống của ứng dụng. Sau đó, khi bạn muốn thêm một tab mới, bạn biến đổi thể hiện

case class Person (firstName: String, lastName: String)
5 như thế này:

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}
6

Đó là cách mà mã máy khách dày thường hoạt động: bạn tạo các thành phần và sau đó biến đổi chúng trong suốt vòng đời của ứng dụng để tạo giao diện người dùng mong muốn. Điều tương tự cũng đúng với hầu hết các thành phần xoay khác, như

case class Person (firstName: String, lastName: String)
7,
case class Person (firstName: String, lastName: String)
8,
case class Person (firstName: String, lastName: String)
9, v.v.

Bởi vì các khung này là OOP và bắt buộc, điểm giao diện này là nơi các chức năng FP và thuần túy thường không phù hợp.

Nếu bạn biết về lập trình phản ứng chức năng (FRP), vui lòng đứng bên cạnh; Tôi viết nhiều hơn về điểm này trong thời gian ngắn.

Khi bạn làm việc với các khung này, bạn phải tuân thủ phong cách của họ tại điểm giao diện này, nhưng không có gì để bạn không viết phần còn lại của mã theo kiểu FP. Trong trò chơi bóng đá Android của tôi, tôi có một cuộc gọi chức năng trông như thế này:

val possiblePlays = OffensiveCoordinator.determinePossiblePlays(gameState)

Trong mã đó,

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}
9 là một hàm thuần túy và đằng sau nó là vài nghìn dòng các chức năng thuần túy khác. Vì vậy, trong khi mã GUI phải phù hợp với khung trò chơi Android mà tôi sử dụng, phần ra quyết định của ứng dụng của tôi-logic kinh doanh trên mạng-được viết theo kiểu FP.

1b) Hãy cẩn thận với những gì tôi vừa viết

Đã tuyên bố rằng, hãy để tôi thêm một vài cảnh báo.

Đầu tiên, các ứng dụng web hoàn toàn khác với các ứng dụng máy khách dày (swing, javafx). Trong một dự án máy khách dày, toàn bộ ứng dụng thường được viết bằng một cơ sở mã lớn dẫn đến một thực thi nhị phân mà người dùng cài đặt trên máy tính của họ. Eclipse, Intellij Idea và Netbeans là những ví dụ về điều này.

Ngược lại, các ứng dụng web mà tôi đã viết trong vài năm qua sử dụng (a) một trong nhiều công nghệ dựa trên JavaScript cho giao diện người dùng và (b) khung chơi ở phía máy chủ. Với các ứng dụng web như thế này, bạn có dữ liệu không tinh khiết vào ứng dụng scala/play thông qua ánh xạ dữ liệu và các chức năng nghỉ ngơi, và bạn cũng có thể tương tác với các cuộc gọi cơ sở dữ liệu không tinh khi Phần logic logic của ứng dụng của bạn có thể được viết với các chức năng thuần túy.

Thứ hai, khái niệm về lập trình phản ứng chức năng (FRP) kết hợp các kỹ thuật FP với lập trình GUI. Dự án Rxjava bao gồm mô tả này:

Rxjava là một triển khai Java VM các phần mở rộng phản ứng: một thư viện để soạn các chương trình không đồng bộ và dựa trên sự kiện bằng cách sử dụng các chuỗi có thể quan sát được ... Tuyên bố trong khi trừu tượng hóa mối quan tâm về những thứ như luồng cấp thấp, đồng bộ hóa, an toàn chủ đề và cấu trúc dữ liệu đồng thời.

(Lưu ý rằng lập trình khai báo trái ngược với lập trình bắt buộc.)

Trang web ReactiveX.IO tuyên bố:

ReactiveX là sự kết hợp của các ý tưởng tốt nhất từ ​​mẫu quan sát viên, mẫu iterator và lập trình chức năng.

Tôi cung cấp một số ví dụ FRP sau trong cuốn sách này, nhưng ví dụ ngắn này từ trang web RXSCALA cho bạn một hương vị của khái niệm:

def updateHealth(delta: Int): Game[Int] = StateT[IO, GameState, Int] { (s: GameState) =>
    val newHealth = s.player.health + delta
    IO((s.copy(player = s.player.copy(health = newHealth)), newHealth))
}
8

Mã này làm như sau:

  1. Sử dụng một người có thể quan sát được, thì nó nhận được một luồng các giá trị
    val emily = Person("Emily", "Maness")
    1. Cho dòng giá trị đó, nó ...
  2. Giảm mười giá trị đầu tiên
  3. “Take” năm giá trị tiếp theo
  4. Nối thêm chuỗi
    val emily1 = Person("Emily", "Maness")
    2 vào cuối mỗi năm giá trị đó
  5. Đầu ra các giá trị kết quả với
    val emily1 = Person("Emily", "Maness")
    3

Như ví dụ này cho thấy, mã nhận được luồng giá trị được viết theo kiểu chức năng, sử dụng các phương thức như

val emily1 = Person("Emily", "Maness")
4,
val emily1 = Person("Emily", "Maness")
5 và
val names = List("chris", "ed", "maurice")
6, kết hợp chúng thành một chuỗi các cuộc gọi, từng chuỗi.

Tôi bao gồm FRP trong một bài học sau trong cuốn sách này, nhưng nếu bạn muốn tìm hiểu thêm ngay bây giờ, dự án RXSCALA được đặt tại đây và lập trình phản ứng của Netflix Hồi trong API Netflix với bài đăng trên blog của Rxjava là một khởi đầu tốt.

Trang Haskell.org này hiển thị công việc hiện tại về việc tạo GUI bằng FRP. (Tôi không phải là một chuyên gia về các công cụ này, nhưng tại thời điểm viết bài này, hầu hết các công cụ này dường như là thử nghiệm hoặc không đầy đủ.)

2) Chủ nghĩa thực dụng (công cụ tốt nhất cho công việc)

Tôi có xu hướng trở thành một người theo chủ nghĩa thực dụng nhiều hơn một người theo chủ nghĩa thuần túy, vì vậy khi tôi cần hoàn thành công việc, tôi muốn sử dụng công cụ tốt nhất cho công việc.

Chẳng hạn, khi tôi mới bắt đầu làm việc với Scala và cần một cách để tìm ra các dự án SBT mới, tôi đã viết một kịch bản Unix Shell. Vì đây là cách sử dụng cá nhân của tôi và tôi chỉ làm việc trên các hệ thống Mac và Unix, nên tạo ra một tập lệnh shell là cách đơn giản nhất để tạo ra một bộ thư mục con tiêu chuẩn và tệp build.sbt.

Ngược lại, nếu tôi cũng làm việc trên Microsoft Windows Systems hoặc nếu tôi quan tâm đến việc tạo ra một giải pháp mạnh mẽ hơn như trình kích hoạt Lightbend, tôi có thể đã viết một ứng dụng Scala/FP, nhưng tôi đã không có những yếu tố thúc đẩy đó.

Một cách khác để suy nghĩ về điều này là thay vì hỏi, là FP là công cụ phù hợp cho mọi ứng dụng tôi cần viết ?, Hãy tiếp tục và đặt câu hỏi đó với một công nghệ khác. Chẳng hạn, bạn có thể hỏi, tôi có nên sử dụng các diễn viên Akka để viết mọi ứng dụng không? Nếu bạn quen thuộc với AKKA, tôi nghĩ rằng bạn sẽ đồng ý rằng việc viết một ứng dụng AKKA để tạo một vài thư mục con và tệp build.sbt sẽ quá mức - mặc dù AKKA là một công cụ tuyệt vời cho các ứng dụng khác.

Bản tóm tắt

Tóm lại, những hạn chế tiềm năng của lập trình chức năng nói chung là:

  1. Viết các chức năng thuần túy là dễ dàng, nhưng kết hợp chúng thành một ứng dụng hoàn chỉnh là nơi mọi thứ trở nên khó khăn.
  2. Thuật ngữ toán học tiên tiến (monad, monoid, functor, v.v.) làm cho FP đáng sợ.
  3. Đối với nhiều người, đệ quy không cảm thấy tự nhiên.
  4. Bởi vì bạn có thể đột biến dữ liệu hiện có, thay vào đó bạn sử dụng một mẫu mà tôi gọi, cập nhật khi bạn sao chép.
  5. Các chức năng thuần túy và I/O don lồng thực sự pha trộn.
  6. Chỉ sử dụng các giá trị bất biến và đệ quy có khả năng dẫn đến các vấn đề về hiệu suất, bao gồm sử dụng RAM và tốc độ.

Hạn chế tiềm năng của *lập trình chức năng trong scala là:

  1. Bạn có thể trộn các kiểu FP và OOP.
  2. Scala không có một thư viện FP tiêu chuẩn.

Cái gì tiếp theo

Đã bao quát những lợi ích và nhược điểm của lập trình chức năng, trong chương tiếp theo, tôi muốn giúp đỡ giải phóng tâm trí của bạn, như Morpheus có thể nói. Chương đó là về một cái gì đó tôi gọi là rào cản thuật ngữ FP vĩ đại, và làm thế nào để vượt qua rào cản đó.

Xem thêm

  • Bài đăng trên blog của tôi
  • Lập trình trong Scala
  • Jesper Nordenberg, Has Hasell vs Scala, bài viết
  • Thông tin về trình chỉnh sửa văn bản của tôi
  • Phần mở rộng phản ứng của người Viking trên ReactiveX.IO
  • Lập trình khai báo
  • Lập trình bắt buộc
  • Dự án RXSCALA
  • Lập trình phản ứng của Netflix
  • Lập trình phản ứng chức năng trên haskell.org
  • Chất kích hoạt đèn

Những nhược điểm của các chức năng trong Python là gì?

Nhược điểm của Python..
Tốc độ chậm. Chúng tôi đã thảo luận ở trên rằng Python là một ngôn ngữ được giải thích và ngôn ngữ được gõ tự động. ....
Không hiệu quả bộ nhớ. Để cung cấp sự đơn giản cho nhà phát triển, Python phải thực hiện một sự đánh đổi nhỏ. ....
Yếu trong điện toán di động. ....
Truy cập cơ sở dữ liệu. ....
Lỗi thời gian chạy ..

Những ưu điểm và nhược điểm của chức năng trong Python là gì?

Bảng so sánh cho những ưu điểm và nhược điểm của Python.

Vấn đề với lập trình chức năng là gì?

Các vấn đề hiệu quả Các ngôn ngữ lập trình chức năng thường kém hiệu quả hơn trong việc sử dụng CPU và bộ nhớ so với các ngôn ngữ bắt buộc như C và Pascal.Điều này có liên quan đến thực tế là một số cấu trúc dữ liệu có thể thay đổi như mảng có triển khai rất đơn giản bằng cách sử dụng phần cứng hiện tại. Functional programming languages are typically less efficient in their use of CPU and memory than imperative languages such as C and Pascal. This is related to the fact that some mutable data structures like arrays have a very straightforward implementation using present hardware.

Những hạn chế của Python là gì?

Lỗi thời gian chạy: Một trong những nhược điểm chính của ngôn ngữ này là thiết kế của nó có nhiều vấn đề.Các lập trình viên Python phải đối mặt với một số vấn đề liên quan đến thiết kế ngôn ngữ.Ngôn ngữ này đòi hỏi nhiều thử nghiệm hơn và nó cũng có lỗi chỉ hiển thị khi chạy điều này là do ngôn ngữ được gõ động.