Ghi nhớ giá trị trong chương trình chức năng


20

Tôi đã quyết định nhận cho mình nhiệm vụ học lập trình chức năng. Cho đến nay, đó là một vụ nổ, và tôi đã "nhìn thấy ánh sáng" như nó vốn có. Thật không may, tôi thực sự không biết bất kỳ lập trình viên chức năng nào mà tôi có thể trả lời câu hỏi. Giới thiệu trao đổi ngăn xếp.

Tôi đang tham gia khóa học phát triển web / phần mềm, nhưng người hướng dẫn của tôi không quen với lập trình chức năng. Anh ấy ổn với tôi khi sử dụng nó, và anh ấy chỉ nhờ tôi giúp anh ấy hiểu cách thức hoạt động để anh ấy có thể đọc mã của tôi tốt hơn.

Tôi quyết định cách tốt nhất để làm điều này là bằng cách minh họa một hàm toán học đơn giản, như nâng một giá trị lên một sức mạnh. Về lý thuyết tôi có thể dễ dàng làm điều đó với một hàm dựng sẵn, nhưng điều đó sẽ đánh bại mục đích của một ví dụ.

Dù sao, tôi đang gặp một số khó khăn để tìm ra cách giữ một giá trị. Vì đây là chương trình chức năng nên tôi không thể thay đổi biến. Nếu tôi viết mã này một cách bắt buộc, nó sẽ trông giống như thế này:

(Sau đây là tất cả mã giả)

f(x,y) {
  int z = x;
  for(int i = 0, i < y; i++){
    x = x * z;
  }
  return x;
}

Trong lập trình chức năng, tôi không chắc chắn. Đây là những gì tôi nghĩ ra:

f(x,y,z){
  if z == 'null',
    f(x,y,x);
  else if y > 1,
    f(x*z,y-1,z);
  else
    return x;
}

Thê nay đung không? Tôi cần phải giữ một giá trị, ztrong cả hai trường hợp, nhưng tôi không chắc làm thế nào để làm điều này trong lập trình hàm. Về lý thuyết, cách tôi làm nó hoạt động, nhưng tôi không chắc liệu nó có "đúng" hay không. Có cách nào tốt hơn để làm điều đó?


32
Nếu bạn muốn ví dụ của mình được thực hiện nghiêm túc, hãy để nó giải quyết một vấn đề thực tế hơn là một vấn đề toán học. Đó là một câu sáo rỗng giữa các nhà phát triển rằng "tất cả FP đều tốt cho việc giải quyết các vấn đề toán học" và nếu ví dụ của bạn là một hàm toán học khác, bạn chỉ củng cố bản mẫu, thay vì làm cho những gì bạn đang làm có vẻ hữu ích.
Mason Wheeler

12
Nỗ lực của bạn thực sự khá tốt khi tính đến những cân nhắc trong thế giới thực. Tất cả các cuộc gọi đệ quy của bạn là các cuộc gọi đuôi , nghĩa là, hàm không làm gì khác sau khi gọi chúng. Điều đó có nghĩa là trình biên dịch hoặc trình thông dịch hỗ trợ nó có thể tối ưu hóa chúng để hàm đệ quy của bạn sử dụng một lượng bộ nhớ ngăn xếp cố định, thay vì số lượng tỷ lệ thuận với y.
8bittree

1
Cảm ơn rất nhiều vì đã hỗ trợ! Tôi vẫn còn rất mới về điều này, vì vậy mã giả của tôi không hoàn hảo. @MasonWheeler Tôi đoán, trong trường hợp này, mã của tôi không thực sự được coi là nghiêm trọng. Tôi vẫn đang học và lý do tôi yêu thích FP là vì nó là Math-y. Toàn bộ ví dụ của tôi là giải thích cho giáo viên của tôi tại sao tôi sử dụng FP. Anh ta không thực sự hiểu nó là gì, vì vậy đây có vẻ là một cách tốt để cho anh ta thấy những lợi thế.
Ucenna

5
Trong ngôn ngữ nào bạn có kế hoạch để viết mã? Đừng cố sử dụng một phong cách không phù hợp với ngôn ngữ mà bạn đang sử dụng.
Carsten S

Câu trả lời:


37

Trước hết, xin chúc mừng "nhìn thấy ánh sáng". Bạn đã làm cho thế giới phần mềm trở thành một nơi tốt hơn bằng cách mở rộng tầm nhìn của bạn.

Thứ hai, thực sự không có cách nào một giáo sư không hiểu lập trình chức năng sẽ có thể nói bất cứ điều gì hữu ích về mã của bạn, ngoại trừ các bình luận trite như "sự thụt đầu tắt". Điều này không có gì đáng ngạc nhiên trong một khóa học phát triển web, vì hầu hết việc phát triển web được thực hiện bằng HTML / CSS / JavaScript. Tùy thuộc vào mức độ bạn thực sự quan tâm đến việc học phát triển web, bạn có thể muốn nỗ lực tìm hiểu các công cụ mà giáo sư của bạn đang giảng dạy (đau đớn mặc dù có thể - tôi biết từ kinh nghiệm).

Để giải quyết câu hỏi đã nêu: nếu mã bắt buộc của bạn sử dụng một vòng lặp, thì rất có thể mã chức năng của bạn sẽ được đệ quy.

(* raises x to the power of y *)
fun pow (x: real) (y: int) : real = 
    if y = 1 then x else x * (pow x (y-1))

Lưu ý rằng thuật toán này thực sự ít nhiều giống với mã mệnh lệnh. Trong thực tế, người ta có thể coi vòng lặp ở trên là đường cú pháp cho các quá trình đệ quy lặp.

Như một lưu ý phụ z, trên thực tế , không cần một giá trị nào trong mã bắt buộc hoặc chức năng của bạn. Bạn nên viết chức năng bắt buộc của bạn như vậy:

def pow(x, y):
    var ret = 1
    for (i = 0; i < y; i++)
         ret = ret * x
    return ret

thay vì thay đổi ý nghĩa của biến x.


Đệ quy của bạn powkhông hoàn toàn đúng. Như nó là, pow 3 3trả lại 81, thay vì 27. Nó sẽ làelse x * pow x (y-1).
8bittree

3
Rất tiếc, viết mã chính xác là khó :) Đã sửa và tôi cũng đã thêm chú thích loại. @Ucenna Nó được coi là SML, nhưng tôi đã không sử dụng nó trong một thời gian vì vậy tôi có thể có cú pháp hơi sai. Có quá nhiều cách để khai báo hàm, tôi không bao giờ có thể nhớ đúng từ khóa. Bên cạnh những thay đổi cú pháp, mã giống hệt nhau trong JavaScript.
vườn

2
@jwg Javascript có một số khía cạnh chức năng: các hàm có thể xác định các hàm lồng nhau, hàm trả về và chấp nhận các hàm làm tham số; nó hỗ trợ các bao đóng với phạm vi từ vựng (mặc dù không có phạm vi động lisp). Tùy thuộc vào kỷ luật của lập trình viên để kiềm chế thay đổi trạng thái và thay đổi dữ liệu.
Kasper van den Berg

1
@jwg Không có định nghĩa về ngôn ngữ "chức năng" theo thỏa thuận (cũng không phải là "bắt buộc", "hướng đối tượng" hoặc "khai báo"). Tôi cố gắng không sử dụng các thuật ngữ này bất cứ khi nào có thể. Có quá nhiều ngôn ngữ dưới ánh mặt trời được phân thành bốn nhóm gọn gàng.
vườn

1
Mức độ phổ biến là một số liệu khủng khiếp, đó là lý do tại sao bất cứ khi nào ai đó đề cập rằng ngôn ngữ hoặc công cụ X phải tốt hơn bởi vì nó được sử dụng rộng rãi tôi biết rằng việc tiếp tục tranh luận sẽ là vô nghĩa. Tôi quen thuộc với gia đình ngôn ngữ ML hơn cá nhân Haskell. Nhưng tôi cũng không chắc nó có đúng không; Tôi đoán là phần lớn các nhà phát triển đã không thử Haskell ngay từ đầu.
vườn

33

Đây thực sự chỉ là phần phụ lục cho câu trả lời của người làm vườn, nhưng tôi muốn chỉ ra có một tên cho mẫu bạn đang nhìn thấy: gấp lại.

Trong lập trình chức năng, một nếp gấp là một cách để kết hợp một loạt các giá trị "ghi nhớ" một giá trị giữa mỗi hoạt động. Xem xét thêm một danh sách các số bắt buộc:

def sum_all(xs):
  total = 0
  for x in xs:
    total = total + x
  return total

Chúng tôi có một danh sách các giá trị xsvà một trạng thái ban đầu của 0(đại diện bởi totaltrong trường hợp này). Sau đó, với mỗi lần xnhập xs, chúng tôi kết hợp giá trị đó với trạng thái hiện tại theo một số thao tác kết hợp (trong trường hợp bổ sung này) và sử dụng kết quả làm trạng thái mới . Về bản chất, sum_all([1, 2, 3])tương đương với (3 + (2 + (1 + 0))). Mẫu này có thể được trích xuất thành hàm bậc cao hơn , hàm chấp nhận các hàm làm đối số:

def fold(items, initial_state, combiner_func):
  state = initial_state
  for item in items:
    state = combiner_func(item, state)
  return state

def sum_all(xs):
  return fold(xs, 0, lambda x y: x + y)

Việc thực hiện foldnày vẫn còn bắt buộc, nhưng nó cũng có thể được thực hiện theo cách đệ quy:

def fold_recursive(items, initial_state, combiner_func):
  if not is_empty(items):
    state = combiner_func(initial_state, first_item(items))
    return fold_recursive(rest_items(items), state, combiner_func)
  else:
    return initial_state

Được thể hiện dưới dạng gấp, chức năng của bạn chỉ đơn giản là:

def exponent(base, power):
  return fold(repeat(base, power), 1, lambda x y: x * y))

... nơi repeat(x, n)trả về một danh sách các nbản sao của x.

Nhiều ngôn ngữ, đặc biệt là những ngôn ngữ hướng đến lập trình chức năng, cung cấp gấp trong thư viện tiêu chuẩn của họ. Ngay cả Javascript cũng cung cấp nó dưới tên reduce. Nói chung, nếu bạn thấy mình sử dụng đệ quy để "ghi nhớ" một giá trị trong một vòng lặp nào đó, bạn có thể muốn gấp lại.


8
Chắc chắn học cách phát hiện khi một vấn đề có thể được giải quyết bằng cách gấp hoặc bản đồ. Trong FP, gần như tất cả các vòng lặp có thể được thể hiện dưới dạng nếp gấp hoặc bản đồ; vì vậy đệ quy rõ ràng thường không cần thiết.
Carcigenicate

1
Trong một số ngôn ngữ, bạn chỉ có thể viếtfold(repeat(base, power), 1, *)
user253751

4
Rico Kahler: scanvề cơ bản là foldnơi thay vì chỉ kết hợp danh sách các giá trị thành một giá trị, nó được kết hợp và mỗi giá trị trung gian được đưa ra ngoài dọc đường, tạo ra một danh sách tất cả các trạng thái trung gian được tạo thay vì chỉ tạo ra trạng thái cuối cùng. Nó có thể thực hiện được về mặt fold(mọi hoạt động lặp là).
Jack

4
@RicoKahler Và, theo như tôi có thể nói, giảm và gấp là điều tương tự. Haskell sử dụng thuật ngữ "gấp", trong khi Clojure thích "giảm". Hành vi của họ có vẻ giống tôi.
Carcigenicate

2
@Ucenna: Nó vừa là biến vừa là hàm. Trong lập trình hàm, các hàm là các giá trị giống như số và chuỗi - bạn có thể lưu trữ chúng trong các biến, chuyển chúng dưới dạng đối số cho các hàm khác, trả về chúng từ các hàm và thường coi chúng như các giá trị khác. Vì vậy, combiner_funclà một đối số và sum_allđang truyền một hàm ẩn danh (đó là lambdabit - nó tạo ra một giá trị hàm mà không đặt tên cho nó) xác định cách nó muốn kết hợp hai mục với nhau.
Jack

8

Đây là một câu trả lời bổ sung để giúp giải thích các bản đồ và nếp gấp. Đối với các ví dụ dưới đây, tôi sẽ sử dụng danh sách này. Hãy nhớ rằng, danh sách này là bất biến, vì vậy nó sẽ không bao giờ thay đổi:

var numbers = [1, 2, 3, 4, 5]

Tôi sẽ sử dụng các số trong các ví dụ của mình vì chúng dẫn đến mã dễ đọc. Tuy nhiên, hãy nhớ rằng các nếp gấp có thể được sử dụng cho bất cứ điều gì mà một vòng lặp mệnh lệnh truyền thống có thể được sử dụng cho.

Một bản đồ lấy một danh sách một cái gì đó, và một hàm và trả về một danh sách đã được sửa đổi bằng cách sử dụng hàm. Mỗi mục được truyền cho hàm và trở thành bất cứ thứ gì hàm trả về.

Ví dụ đơn giản nhất về điều này chỉ là thêm một số vào mỗi số trong danh sách. Tôi sẽ sử dụng mã giả để biến nó thành ngôn ngữ bất khả tri:

function add-two(n):
    return n + 2

var numbers2 =
    map(add-two, numbers) 

Nếu bạn đã in numbers2, bạn sẽ thấy [3, 4, 5, 6, 7]danh sách đầu tiên có 2 được thêm vào mỗi phần tử. Lưu ý chức năng add-twođã được đưa ra mapđể sử dụng.

Các nếp gấp tương tự nhau, ngoại trừ hàm bạn bắt buộc phải cung cấp cho chúng phải có 2 đối số. Đối số đầu tiên thường là bộ tích lũy (trong một nếp gấp bên trái, là phổ biến nhất). Bộ tích lũy là dữ liệu được truyền trong khi lặp. Đối số thứ hai là mục hiện tại của danh sách; giống như ở trên cho mapchức năng.

function add-together(n1, n2):
    return n1 + n2

var sum =
    fold(add-together, 0, numbers)

Nếu bạn in sumbạn sẽ thấy tổng của danh sách các số: 15.

Dưới đây là những gì các đối số để foldlàm:

  1. Đây là chức năng mà chúng tôi đang đưa ra. Việc gấp sẽ vượt qua chức năng của bộ tích lũy hiện tại và mục hiện tại của danh sách. Bất cứ chức năng nào trả về sẽ trở thành bộ tích lũy mới, sẽ được chuyển cho hàm vào lần tiếp theo. Đây là cách bạn "ghi nhớ" các giá trị khi bạn lặp theo kiểu FP. Tôi đã cho nó một hàm có 2 số và thêm chúng.

  2. Đây là tích lũy ban đầu; những gì bộ tích lũy bắt đầu như trước khi bất kỳ mục nào trong danh sách được xử lý. Khi bạn tính tổng các số, tổng số trước khi bạn thêm bất kỳ số nào lại với nhau? 0, mà tôi đã thông qua như là đối số thứ hai.

  3. Cuối cùng, như với bản đồ, chúng tôi cũng chuyển vào danh sách các số để nó xử lý.

Nếu nếp gấp vẫn không có ý nghĩa, hãy xem xét điều này. Khi bạn viết:

# Notice I passed the plus operator directly this time, 
#  instead of wrapping it in another function. 
fold(+, 0, numbers)

Về cơ bản, bạn đang đặt chức năng được chuyển giữa mỗi mục trong danh sách và thêm bộ tích lũy ban đầu vào bên trái hoặc bên phải (tùy thuộc vào việc nó là một nếp gấp bên trái hay bên phải), vì vậy:

[1, 2, 3, 4, 5]

Trở thành:

0 + 1 + 2 + 3 + 4 + 5
^ Note the initial accumulator being added onto the left (for a left fold).

Mà bằng 15.

Sử dụng mapkhi bạn muốn biến một danh sách thành một danh sách khác, có cùng độ dài.

Sử dụng foldkhi bạn muốn biến danh sách thành một giá trị, như tổng hợp danh sách các số.

Như @Jorg đã chỉ ra trong các bình luận, "giá trị đơn" không cần phải đơn giản như một con số; nó có thể là bất kỳ đối tượng nào, bao gồm danh sách hoặc bộ dữ liệu! Cách tôi thực sự có các lần nhấp chuột cho tôi là xác định bản đồ theo cách gấp. Lưu ý cách tích lũy là một danh sách:

function map(f, list):
    fold(
        function(xs, x): # xs is the list that has been processed so far
            xs.add( f(x) ) # Add returns the list instead of mutating it
        , [] # Before any of the list has been processed, we have an empty list
        , list) 

Thành thật mà nói, một khi bạn hiểu từng vấn đề, bạn sẽ nhận ra hầu như bất kỳ vòng lặp nào cũng có thể được thay thế bằng một nếp gấp hoặc bản đồ.


1
@Ucenna @Ucenna Có một vài lỗi với mã của bạn (như ikhông bao giờ được xác định), nhưng tôi nghĩ bạn có ý tưởng đúng. Một vấn đề với ví dụ của bạn là: hàm ( x), chỉ được truyền một phần tử của danh sách, không phải toàn bộ danh sách. Lần đầu tiên xđược gọi, nó đã vượt qua bộ tích lũy ban đầu của bạn ( y) làm đối số đầu tiên và phần tử đầu tiên là đối số thứ hai. Lần tiếp theo, nó xsẽ được chuyển qua bộ tích lũy mới ở bên trái (bất cứ thứ gì được xtrả lại lần đầu tiên) và phần tử thứ hai của danh sách là đối số thứ hai.
Carcigenicate

1
@Ucenna Bây giờ bạn đã có ý tưởng cơ bản, hãy xem lại cách thực hiện của Jack.
Carcigenicate

1
@Ucenna: Các lang khác nhau có các tùy chọn khác nhau cho dù hàm được cung cấp để lấy bộ tích lũy làm đối số thứ nhất hay thứ hai, thật không may. Một trong những lý do thật tuyệt khi sử dụng thao tác giao hoán như bổ sung để dạy các nếp gấp.
Jack

3
"Sử dụng foldkhi bạn muốn biến danh sách thành một giá trị duy nhất (như tổng hợp danh sách các số)." - Tôi chỉ muốn lưu ý rằng "giá trị đơn" này có thể phức tạp tùy ý bao gồm cả một danh sách! Trên thực tế, foldlà một phương pháp lặp chung, nó có thể làm mọi thứ lặp lại có thể làm. Ví dụ, mapcó thể được biểu thị một cách tầm thường như ở func map(f, l) = fold((xs, x) => append(xs, f(x)), [], l)đây, "giá trị đơn" được tính toán foldthực sự là một danh sách.
Jörg W Mittag

2
Có thể muốn làm với một danh sách, có thể được thực hiện với fold. Và nó không phải là một danh sách, mọi bộ sưu tập có thể được biểu thị là trống / không trống sẽ làm. Điều đó về cơ bản có nghĩa là bất kỳ iterator sẽ làm. (tôi đoán việc ném từ "catamorphism" vào đó sẽ có quá nhiều cho phần giới thiệu của người mới bắt đầu, mặc dù :-D)
Jörg W Mittag

1

Thật khó để tìm ra những vấn đề tốt không thể giải quyết bằng chức năng xây dựng. Và nếu nó được xây dựng, thì nó nên được sử dụng để trở thành một ví dụ về phong cách tốt trong ngôn ngữ x.

Trong haskell chẳng hạn, bạn đã có chức năng (^)trong Prelude.

Hoặc nếu bạn muốn làm điều đó nhiều chương trình hơn product (replicate y x)

Điều tôi đang nói là thật khó để thể hiện những điểm mạnh của phong cách / ngôn ngữ nếu bạn không sử dụng các tính năng mà nó cung cấp. Tuy nhiên, đây có thể là một bước tốt để thể hiện cách thức hoạt động của nó đằng sau hậu trường, nhưng tôi nghĩ bạn nên viết mã theo cách tốt nhất trong bất kỳ ngôn ngữ nào bạn đang sử dụng và sau đó giúp người đó hiểu điều gì đang xảy ra nếu cần.


1
Để liên kết một cách hợp lý câu trả lời này với câu trả lời khác, cần lưu ý rằng đó productchỉ là một hàm tắt để foldnhân với hàm của nó và 1 là đối số ban đầu của nó và đó replicatelà một hàm tạo ra một trình vòng lặp (hoặc danh sách; như tôi đã lưu ý ở trên cả hai về cơ bản là không thể phân biệt được trong haskell) cung cấp một số lượng đầu ra giống hệt nhau. Bây giờ, thật dễ hiểu khi cách triển khai này thực hiện tương tự như câu trả lời của @ Jack ở trên, chỉ sử dụng các phiên bản trường hợp đặc biệt được xác định trước của các chức năng tương tự để làm cho nó ngắn gọn hơn.
Periata Breatta
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.