Được đóng cửa được coi là phong cách chức năng không tinh khiết?


33

Được đóng cửa được coi là không tinh khiết trong lập trình chức năng?

Dường như người ta thường có thể tránh việc đóng cửa bằng cách chuyển các giá trị trực tiếp đến một hàm. Vì vậy, nên tránh đóng cửa khi có thể?

Nếu chúng không trong sạch, và tôi đúng khi nói rằng chúng có thể tránh được, tại sao nhiều ngôn ngữ lập trình chức năng lại hỗ trợ đóng cửa?

Một trong những tiêu chí cho một hàm thuần túy là "Hàm luôn đánh giá cùng một giá trị kết quả được đưa ra cùng (các) giá trị đối số ."

Giả sử

f: x -> x + y

f(3)sẽ không luôn luôn cho kết quả tương tự. f(3)phụ thuộc vào giá trị ykhông phải là đối số của f. Như vậy fkhông phải là một hàm thuần túy.

Vì tất cả các bao đóng đều dựa vào các giá trị không phải là đối số, làm thế nào có thể cho bất kỳ bao đóng nào là thuần túy? Đúng, về mặt lý thuyết, giá trị đóng có thể là hằng số nhưng không có cách nào để biết rằng chỉ bằng cách xem mã nguồn của chính hàm đó.

Điều này dẫn tôi đến là cùng một chức năng có thể là thuần túy trong một tình huống nhưng không tinh khiết trong một tình huống khác. Người ta không thể luôn xác định xem một hàm có thuần túy hay không bằng cách nghiên cứu mã nguồn của nó. Thay vào đó, người ta có thể phải xem xét nó trong bối cảnh môi trường của nó tại thời điểm nó được gọi trước khi có thể phân biệt như vậy.

Tôi có nghĩ về điều này một cách chính xác?


6
Tôi sử dụng các lần đóng cửa mọi lúc trong Haskell và Haskell hoàn toàn thuần khiết như vậy.
Thomas Eding

5
Trong một ngôn ngữ chức năng thuần túy, ykhông thể thay đổi, vì vậy đầu ra của f(3)sẽ luôn giống nhau.
Lily Chung

4
ylà một phần của định nghĩa fmặc dù nó không được đánh dấu rõ ràng là đầu vào f- nó vẫn là trường hợp fđược định nghĩa theo y(chúng ta có thể biểu thị hàm f_y, để tạo sự phụ thuộc vào yrõ ràng), và do đó thay đổi ymang lại một chức năng khác . Các chức năng cụ thể f_yđược xác định cho một cụ thể ylà rất nhiều tinh khiết. (Ví dụ: hai chức năng f: x -> x + 3f: x -> x + 5các chức năng khác nhau và cả hai đều thuần túy, mặc dù chúng tôi đã sử dụng cùng một chữ cái để biểu thị chúng.)
ShreevatsaR

Câu trả lời:


26

Độ tinh khiết có thể được đo lường bằng hai điều:

  1. Có phải hàm luôn trả về cùng một đầu ra, cho cùng một đầu vào; tức là nó có minh bạch không?
  2. Liệu chức năng sửa đổi bất cứ điều gì bên ngoài của chính nó, tức là nó có tác dụng phụ?

Nếu câu trả lời cho 1 là có và câu trả lời cho 2 là không, thì hàm này là thuần túy. Đóng chỉ làm cho hàm không tinh khiết nếu bạn sửa đổi biến đóng.


Không phải là quyết định mục đầu tiên? Hay đó cũng là một phần của sự tinh khiết? Tôi không quá quen thuộc với khái niệm "tinh khiết" trong bối cảnh lập trình.

4
@JimmyHoffa: Không nhất thiết. Bạn có thể đặt đầu ra của bộ hẹn giờ phần cứng trong một chức năng và không có gì bên ngoài chức năng được sửa đổi.
Robert Harvey

1
@RobertHarvey Đây có phải là tất cả về cách chúng ta xác định đầu vào cho một chức năng? Trích dẫn của tôi từ wikipedia tập trung vào các đối số chức năng, trong khi đó bạn cũng coi (các) biến đóng là đầu vào.
user2179977

8
@ user2179977: trừ khi chúng có thể thay đổi, bạn không nên coi các biến đóng là đầu vào bổ sung cho hàm. Thay vào đó, bạn nên coi chính bao đóng là một hàm và là một hàm khác khi nó đóng trên một giá trị khác y. Vì vậy, ví dụ chúng ta định nghĩa một hàm gsao cho g(y)chính nó là hàm x -> x + y. Sau đó glà hàm số nguyên trả về hàm, g(3)là hàm số nguyên trả về số nguyên và g(2)là hàm khác của số nguyên trả về số nguyên. Tất cả ba chức năng là tinh khiết.
Steve Jessop

1
@Darkhogg: Vâng. Xem cập nhật của tôi.
Robert Harvey

10

Đóng cửa xuất hiện Lambda Tính, đây là hình thức lập trình chức năng thuần túy nhất có thể, vì vậy tôi sẽ không gọi chúng là "không trong sạch" ...

Đóng cửa không phải là "không trong sạch" bởi vì các chức năng trong các ngôn ngữ chức năng là công dân hạng nhất - điều đó có nghĩa là chúng có thể được coi là giá trị.

Hãy tưởng tượng điều này (mã giả):

foo(x) {
    let y = x + 1
    ...
}

ylà một giá trị. Giá trị của nó phụ thuộc vào x, nhưng xlà bất biến nên ygiá trị cũng bất biến. Chúng ta có thể gọi foonhiều lần với các đối số khác nhau sẽ tạo ra các ys khác nhau , nhưng ytất cả chúng đều sống ở các phạm vi khác nhau và phụ thuộc vào các xs khác nhau nên độ tinh khiết vẫn còn nguyên.

Bây giờ hãy thay đổi nó:

bar(x) {
    let y(z) = x + z
    ....
}

Ở đây chúng ta đang sử dụng một bao đóng (chúng ta đang đóng trên x), nhưng nó cũng giống như foocác lệnh gọi barkhác nhau với các đối số khác nhau tạo ra các giá trị khác nhau của y(nhớ - hàm là các giá trị) mà tất cả đều không thay đổi nên độ tinh khiết vẫn còn nguyên.

Ngoài ra, xin lưu ý rằng việc đóng cửa có tác dụng rất giống với cà ri:

adder(a)(b) {
    return a + b
}
baz(x) {
    let y = adder(x)
    ...
}

bazkhông thực sự khác biệt so với bar- trong cả hai chúng tôi tạo ra một giá trị hàm có tên ytrả về đối số của nó cộng x. Trong thực tế, trong Lambda Tính, bạn sử dụng các bao đóng để tạo các hàm với nhiều đối số - và nó vẫn không trong sạch.


9

Những người khác đã đưa ra câu hỏi chung độc đáo trong câu trả lời của họ, vì vậy tôi sẽ chỉ xem xét xóa sự nhầm lẫn mà bạn báo hiệu trong bản chỉnh sửa của mình.

Việc đóng không trở thành đầu vào của hàm, thay vào đó, nó đi vào thân hàm. Để cụ thể hơn, một hàm đề cập đến một giá trị trong phạm vi bên ngoài trong cơ thể của nó.

Bạn có ấn tượng rằng nó làm cho chức năng không tinh khiết. Đó không phải là trường hợp, nói chung. Trong lập trình chức năng, hầu hết thời gian là các giá trị bất biến . Điều đó cũng đúng với giá trị đóng.

Giả sử bạn có một đoạn mã như thế này:

let make y =
    fun x -> x + y

Gọi make 3make 4sẽ cung cấp cho bạn hai chức năng với các bao đóng đối makevới yđối số. Một trong số họ sẽ trở lại x + 3, người kia x + 4. Chúng là hai chức năng riêng biệt, và cả hai đều thuần túy. Chúng được tạo ra bằng cách sử dụng cùng makechức năng, nhưng đó là nó.

Lưu ý hầu hết thời gian một vài đoạn trở lại.

  1. Trong Haskell, là thuần túy, bạn chỉ có thể đóng trên các giá trị bất biến. Không có trạng thái đột biến để đóng hơn. Bạn chắc chắn sẽ có được một chức năng thuần túy theo cách đó.
  2. Trong các ngôn ngữ chức năng không tinh khiết, như F #, bạn có thể đóng các ô tham chiếu và các loại tham chiếu và có được một hàm không tinh khiết có hiệu lực. Bạn đúng ở chỗ bạn phải theo dõi phạm vi mà hàm được xác định để biết nó có thuần hay không. Bạn có thể dễ dàng biết liệu một giá trị có thể thay đổi trong các ngôn ngữ đó hay không, vì vậy nó không thành vấn đề.
  3. Trong các ngôn ngữ OOP hỗ trợ các bao đóng, như C # và JavaScript, tình huống tương tự như các ngôn ngữ chức năng không tinh khiết, nhưng việc theo dõi phạm vi bên ngoài trở nên khó khăn hơn do các biến có thể thay đổi theo mặc định.

Lưu ý rằng đối với 2 và 3, các ngôn ngữ đó không cung cấp bất kỳ đảm bảo nào về độ tinh khiết. Tạp chất không phải là một tài sản của sự đóng cửa, mà là của chính ngôn ngữ. Đóng cửa không thay đổi hình ảnh nhiều.


1
Bạn hoàn toàn có thể đóng các giá trị có thể thay đổi trong Haskell, nhưng một thứ như vậy sẽ được chú thích với đơn vị IO.
Daniel Gratzer

1
@jozefg không, bạn đóng trên một IO Agiá trị bất biến , và kiểu đóng của bạn là IO (B -> C)hoặc somesuch. Độ tinh khiết được duy trì
Caleth 7/12/17

5

Thông thường tôi sẽ yêu cầu bạn làm rõ định nghĩa của bạn về "không trong sạch", nhưng trong trường hợp này nó không thực sự quan trọng. Giả sử bạn đối lập với thuật ngữ hoàn toàn có chức năng , câu trả lời là "không", bởi vì không có gì về việc đóng cửa vốn đã phá hoại. Nếu ngôn ngữ của bạn hoàn toàn là chức năng mà không có sự đóng cửa, thì ngôn ngữ đó vẫn hoàn toàn là chức năng với sự đóng cửa. Nếu thay vào đó bạn có nghĩa là "không hoạt động", câu trả lời vẫn là "không"; đóng cửa tạo điều kiện cho việc tạo ra các chức năng.

Dường như người ta thường có thể tránh việc đóng cửa bằng cách chuyển dữ liệu trực tiếp đến một chức năng.

Có, nhưng sau đó chức năng của bạn sẽ có thêm một tham số và điều đó sẽ thay đổi loại của nó. Đóng cho phép bạn tạo các hàm dựa trên các biến mà không cần thêm tham số. Điều này hữu ích khi bạn có một hàm có 2 đối số và bạn muốn tạo một phiên bản của nó chỉ mất 1 đối số.

EDIT: Liên quan đến chỉnh sửa / ví dụ của riêng bạn ...

Giả sử

f: x -> x + y

f (3) sẽ không luôn luôn cho kết quả tương tự. f (3) phụ thuộc vào giá trị của y mà không phải là đối số của f. Do đó f không phải là một hàm thuần túy.

Phụ thuộc là sự lựa chọn sai từ ở đây. Trích dẫn bài viết Wikipedia rất giống bạn đã làm:

Trong lập trình máy tính, một hàm có thể được mô tả là một hàm thuần nếu cả hai câu lệnh này về hàm giữ:

  1. Hàm luôn đánh giá cùng một giá trị kết quả được đưa ra cùng (các) giá trị đối số. Giá trị kết quả chức năng không thể phụ thuộc vào bất kỳ thông tin hoặc trạng thái ẩn nào có thể thay đổi khi tiến hành thực hiện chương trình hoặc giữa các lần thực hiện khác nhau của chương trình, cũng không thể phụ thuộc vào bất kỳ đầu vào bên ngoài nào từ các thiết bị I / O.
  2. Đánh giá kết quả không gây ra bất kỳ tác dụng phụ hoặc đầu ra có thể quan sát được về mặt ngữ nghĩa, chẳng hạn như đột biến của các đối tượng có thể thay đổi hoặc đầu ra cho các thiết bị I / O.

Giả sử ylà bất biến (thường là trường hợp trong các ngôn ngữ chức năng), điều kiện 1 được thỏa mãn: với tất cả các giá trị x, giá trị của f(x)không thay đổi. Điều này cần phải rõ ràng từ thực tế ylà không khác với một hằng số, và x + 3là thuần túy. Nó cũng rõ ràng không có đột biến hoặc I / O đang diễn ra.


3

Rất nhanh chóng: một sự thay thế là "minh bạch tham chiếu" nếu "thay thế như dẫn đến thích" và một chức năng là "thuần túy" nếu tất cả các hiệu ứng của nó được chứa trong giá trị trả về của nó. Cả hai đều có thể được làm chính xác, nhưng điều quan trọng cần lưu ý là chúng không giống nhau và thậm chí không có nghĩa là cái kia.

Bây giờ hãy nói về việc đóng cửa.

Nhàm chán (chủ yếu là thuần túy) "đóng cửa"

Việc đóng cửa xảy ra bởi vì khi chúng tôi đánh giá một thuật ngữ lambda, chúng tôi diễn giải các biến (bị ràng buộc) là các tra cứu môi trường. Do đó, khi chúng tôi trả về một thuật ngữ lambda là kết quả của việc đánh giá các biến bên trong nó sẽ "đóng lại" các giá trị mà chúng nhận được khi nó được xác định.

Trong phép tính lambda đơn giản, đây là loại tầm thường và toàn bộ khái niệm chỉ tan biến. Để chứng minh rằng, đây là một trình thông dịch tính toán lambda tương đối nhẹ:

-- untyped lambda calculus values are functions
data Value = FunVal (Value -> Value)

-- we write expressions where variables take string-based names, but we'll
-- also just assume that nobody ever shadows names to avoid having to do
-- capture-avoiding substitutions

type Name = String

data Expr
  = Var Name
  | App Expr Expr
  | Abs Name Expr

-- We model the environment as function from strings to values, 
-- notably ignoring any kind of smooth lookup failures
type Env = Name -> Value

-- The empty environment
env0 :: Env
env0 _ = error "Nope!"

-- Augmenting the environment with a value, "closing over" it!
addEnv :: Name -> Value -> Env -> Env
addEnv nm v e nm' | nm' == nm = v
                  | otherwise = e nm

-- And finally the interpreter itself
interp :: Env -> Expr -> Value
interp e (Var name) = e name          -- variable lookup in the env
interp e (App ef ex) =
  let FunVal f = interp e ef
      x        = interp e ex
  in f x                              -- application to lambda terms
interp e (Abs name expr) =
  -- augmentation of a local (lexical) environment
  FunVal (\value -> interp (addEnv name value e) expr)

Phần quan trọng cần chú ý là addEnvkhi chúng ta gia tăng môi trường bằng một tên mới. Hàm này chỉ được gọi là "bên trong" của Absthuật ngữ lực kéo được giải thích (thuật ngữ lambda). Môi trường được "tra cứu" bất cứ khi nào chúng ta đánh giá một Varthuật ngữ và vì vậy những người đó Varquyết tâm với bất cứ điều gì Nameđược đề cập đến trong Envđó bị bắt bởi Abslực kéo có chứa Var.

Bây giờ, một lần nữa, trong điều khoản LC đơn giản, điều này là nhàm chán. Nó có nghĩa là các biến bị ràng buộc chỉ là hằng số như bất cứ ai quan tâm. Họ được đánh giá trực tiếp và ngay lập tức như các giá trị mà họ biểu thị trong môi trường theo phạm vi từ vựng cho đến thời điểm đó.

Đây cũng là (gần như) tinh khiết. Ý nghĩa duy nhất của bất kỳ thuật ngữ nào trong phép tính lambda của chúng tôi được xác định bởi giá trị trả về của nó. Ngoại lệ duy nhất là tác dụng phụ của việc không chấm dứt được thể hiện bằng thuật ngữ Omega:

-- in simple LC syntax:
--
-- (\x -> (x x)) (\x -> (x x))
omega :: Expr
omega = App (Abs "x" (App (Var "x") 
                          (Var "x")))
            (Abs "x" (App (Var "x") 
                          (Var "x")))

Thú vị (không tinh khiết) đóng cửa

Bây giờ đối với một số nền tảng nhất định, các bao đóng được mô tả trong LC đơn giản ở trên thật nhàm chán vì không có khái niệm nào về khả năng tương tác với các biến chúng tôi đã đóng. Cụ thể, từ "đóng cửa" có xu hướng gọi mã như Javascript sau

> function mk_counter() {
  var n = 0;
  return function incr() {
    return n += 1;
  }
}
undefined

> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3

Điều này chứng tỏ rằng chúng ta đã đóng trên nbiến trong hàm bên trong incrvà gọi incrtương tác có ý nghĩa với biến đó. mk_counterlà thuần túy, nhưng incrkhông tinh khiết (và cũng không minh bạch về mặt tham chiếu).

Điều gì khác nhau giữa hai trường hợp này?

Khái niệm "biến"

Nếu chúng ta xem xét sự thay thế và trừu tượng có nghĩa gì trong ý nghĩa LC đơn giản, chúng ta nhận thấy rằng chúng hoàn toàn đơn giản. Các biến có nghĩa đen không gì khác hơn là tra cứu môi trường ngay lập tức. Trừu tượng Lambda theo nghĩa đen không gì khác hơn là tạo ra một môi trường tăng cường để đánh giá biểu thức bên trong. Không có chỗ trong mô hình này cho loại hành vi mà chúng ta đã thấy với mk_counter/ incrvì không có biến thể nào được cho phép.

Đối với nhiều người, đây là trung tâm của "biến" có nghĩa là biến thể của Haiti. Tuy nhiên, các nhà ngữ nghĩa học muốn phân biệt giữa loại biến được sử dụng trong LC và loại "biến" được sử dụng trong Javascript. Để làm như vậy, họ có xu hướng gọi cái sau là "ô có thể thay đổi" hoặc "khe".

Danh pháp này theo cách sử dụng "biến" trong lịch sử lâu dài trong toán học, trong đó nó có nghĩa giống như "không xác định": biểu thức (toán học) "x + x" không cho phép xthay đổi theo thời gian, thay vào đó nó có nghĩa là bất kể của giá trị (đơn, không đổi) xmất.

Do đó, chúng tôi nói "vị trí" để nhấn mạnh khả năng đưa các giá trị vào một vị trí và đưa chúng ra ngoài.

Để thêm sự nhầm lẫn, trong Javascript, các "vị trí" này trông giống như các biến: chúng tôi viết

var x;

để tạo một và sau đó khi chúng ta viết

x;

nó cho biết chúng tôi đang tìm kiếm giá trị hiện được lưu trữ trong khe đó. Để làm cho điều này rõ ràng hơn, các ngôn ngữ thuần túy có xu hướng nghĩ về các vị trí như lấy tên làm tên (toán học, tính toán lambda). Trong trường hợp này, chúng tôi phải dán nhãn rõ ràng khi chúng tôi nhận hoặc đặt từ một vị trí. Ký hiệu như vậy có xu hướng trông giống như

-- create a fresh, empty slot and name it `x` in the context of the 
-- expression E
let x = newSlot in E

-- look up the value stored in the named slot named `x`, return that value
get x

-- store a new value, `v`, in the slot named `x`, return the slot
put x v

Ưu điểm của ký hiệu này là giờ đây chúng ta có sự phân biệt rõ ràng giữa các biến toán học và các vị trí có thể thay đổi. Các biến có thể lấy các vị trí làm giá trị của chúng, nhưng vị trí cụ thể được đặt tên bởi một biến là không đổi trong phạm vi của nó.

Sử dụng ký hiệu này, chúng ta có thể viết lại mk_counterví dụ (lần này theo cú pháp giống Haskell, mặc dù ngữ nghĩa giống như Haskell đã quyết định):

mkCounter = 
  let x = newSlot 
  in (\() -> let old = get x 
             in get (put x (old + 1)))

Trong trường hợp này, chúng tôi đang sử dụng các quy trình thao túng vị trí có thể thay đổi này. Để thực hiện nó, chúng ta cần phải đóng không chỉ một môi trường liên tục của các tên như xmà còn là một môi trường có thể thay đổi chứa tất cả các vị trí cần thiết. Điều này gần với khái niệm chung về "đóng cửa" mọi người rất yêu thích.

Một lần nữa, mkCounterlà rất không tinh khiết. Nó cũng rất mờ đục. Nhưng lưu ý rằng các tác dụng phụ không phát sinh từ việc bắt hoặc đóng tên mà thay vào đó là việc bắt giữ các tế bào có thể thay đổi và các hoạt động tác dụng phụ trên nó như thế nào getput.

Cuối cùng, tôi nghĩ rằng đây là câu trả lời cuối cùng cho câu hỏi của bạn: độ tinh khiết không bị ảnh hưởng bởi việc bắt biến (toán học) mà thay vào đó là các hoạt động hiệu ứng phụ được thực hiện trên các vị trí có thể thay đổi được đặt tên bởi các biến đã bắt.

Chỉ có điều trong các ngôn ngữ không cố gắng gần gũi với LC hoặc không cố gắng duy trì độ tinh khiết mà hai khái niệm này thường bị lẫn lộn dẫn đến nhầm lẫn.


1

Không, các bao đóng không làm cho hàm không tinh khiết, miễn là giá trị đóng không đổi (không bị thay đổi bởi bao đóng cũng như mã khác), đó là trường hợp thông thường trong lập trình hàm.

Lưu ý rằng mặc dù bạn luôn có thể chuyển một giá trị làm đối số thay vào đó, thông thường bạn không thể làm như vậy mà không gặp khó khăn đáng kể. Ví dụ: (cà phê):

closedValue = 42
return (arg) -> console.log "#{closedValue} #{arg}"

Theo đề nghị của bạn, bạn chỉ có thể quay lại:

return (arg, closedValue) -> console.log "#{closedValue} #{arg}"

Hàm này không được gọi vào thời điểm này, chỉ được xác định , vì vậy bạn phải tìm cách chuyển giá trị mong muốn của mình closedValueđến điểm mà hàm thực sự được gọi. Tốt nhất điều này tạo ra rất nhiều khớp nối. Tệ nhất, bạn không kiểm soát mã tại điểm gọi, vì vậy thực sự không thể.

Các thư viện sự kiện trong các ngôn ngữ không hỗ trợ đóng thường cung cấp một số cách khác để truyền dữ liệu tùy ý trở lại cuộc gọi lại, nhưng nó không hay và tạo ra nhiều sự phức tạp cho cả người bảo trì thư viện và người dùng thư viện.

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.