Câu trả lời lập trình chức năng cho các bất biến dựa trên kiểu là gì?


9

Tôi biết rằng khái niệm bất biến tồn tại trên nhiều mô hình lập trình. Ví dụ, bất biến vòng lặp có liên quan trong OO, lập trình chức năng và thủ tục.

Tuy nhiên, một loại rất hữu ích được tìm thấy trong OOP là bất biến dữ liệu của một loại cụ thể. Đây là những gì tôi gọi là "bất biến dựa trên kiểu" trong tiêu đề. Ví dụ, một Fractionloại có thể có a numeratordenominator, với bất biến là gcd của chúng luôn là 1 (tức là phân số ở dạng rút gọn). Tôi chỉ có thể đảm bảo điều này bằng cách đóng gói một số loại, không để dữ liệu của nó được đặt tự do. Đổi lại, tôi không bao giờ phải kiểm tra xem nó có giảm hay không, vì vậy tôi có thể đơn giản hóa các thuật toán như kiểm tra đẳng thức.

Mặt khác, nếu tôi chỉ đơn giản khai báo một Fractionloại mà không cung cấp bảo đảm này thông qua đóng gói, tôi không thể viết bất kỳ chức năng nào trên loại này mà giả sử rằng phân số bị giảm, vì trong tương lai ai đó có thể đi cùng và thêm một cách về việc nắm giữ một phần không giảm.

Nói chung, việc thiếu loại bất biến này có thể dẫn đến:

  • Các thuật toán phức tạp hơn vì các điều kiện trước cần phải được kiểm tra / đảm bảo ở nhiều nơi
  • Vi phạm DRY vì các điều kiện trước lặp đi lặp lại này thể hiện cùng một kiến ​​thức cơ bản (rằng bất biến phải là đúng)
  • Phải thực thi các điều kiện trước thông qua các lỗi thời gian chạy thay vì đảm bảo thời gian biên dịch

Vì vậy, câu hỏi của tôi là câu trả lời lập trình chức năng cho loại bất biến này là gì. Có một cách thành ngữ chức năng để đạt được ít nhiều cùng một điều? Hoặc có một số khía cạnh của lập trình chức năng làm cho lợi ích ít liên quan?


nhiều ngôn ngữ chức năng có thể làm điều này một cách tầm thường ... Scala, F # và các ngôn ngữ khác chơi tốt với OOP, nhưng Haskell cũng vậy ... về cơ bản, bất kỳ ngôn ngữ nào cho phép bạn xác định loại và hành vi của chúng đều hỗ trợ điều này.
AK_

@AK_ Tôi biết F # có thể làm điều này (mặc dù IIRC nó yêu cầu một số bước nhảy nhỏ) và đoán Scala có thể là một ngôn ngữ mô hình chéo khác. Thật thú vị khi Haskell có thể làm điều đó - có một liên kết? Điều tôi thực sự tìm kiếm là câu trả lời thành ngữ chức năng , thay vì các ngôn ngữ cụ thể cung cấp một tính năng. Nhưng tất nhiên mọi thứ có thể trở nên khá mờ nhạt và chủ quan một khi bạn bắt đầu nói về những gì là thành ngữ, đó là lý do tại sao tôi bỏ nó ra khỏi câu hỏi.
Ben Aaronson

Đối với các trường hợp điều kiện tiên quyết không thể được kiểm tra tại thời gian biên dịch, việc kiểm tra trong hàm tạo là một thành ngữ. Hãy xem xét một PrimeNumberlớp học. Sẽ là quá tốn kém để thực hiện nhiều kiểm tra dự phòng cho tính nguyên thủy cho mỗi thao tác, nhưng nó không phải là một loại thử nghiệm có thể được thực hiện tại thời điểm biên dịch. (Rất nhiều thao tác bạn muốn thực hiện trên các số nguyên tố, giả sử nhân, không tạo thành một bao đóng , tức là kết quả có thể không được đảm bảo là số nguyên tố (Đăng bài dưới dạng nhận xét vì tôi không biết tự lập trình chức năng.)
rwong

Một câu hỏi dường như không liên quan, nhưng ... Các khẳng định hoặc bài kiểm tra đơn vị có quan trọng hơn không?
rwong

@rwong Vâng, một số ví dụ hay đấy. Tôi thực sự không rõ ràng 100% điểm cuối cùng mà bạn đang lái xe, mặc dù.
Ben Aaronson

Câu trả lời:


2

Một số ngôn ngữ chức năng như OCaml có các cơ chế tích hợp để triển khai các kiểu dữ liệu trừu tượng do đó thực thi một số bất biến . Các ngôn ngữ không có cơ chế như vậy phụ thuộc vào người dùng, không nhìn vào thảm, để thực thi các bất biến.

Các loại dữ liệu trừu tượng trong OCaml

Trong OCaml, các mô-đun được sử dụng để cấu trúc một chương trình. Một mô-đun có một triển khaichữ ký , cái sau là một loại tóm tắt các giá trị và loại được xác định trong mô-đun, trong khi mô-đun cung cấp các định nghĩa thực tế. Điều này có thể được so sánh một cách lỏng lẻo với diptych .c/.hquen thuộc với các lập trình viên C.

Ví dụ, chúng ta có thể thực hiện Fractionmô-đun như thế này:

# module Fraction = struct
  type t = Fraction of int * int
  let rec gcd a b =
    match a mod b with
    | 0 -> b
    | r -> gcd b r

  let make a b =
   if b = 0 then
     invalid_arg "Fraction.make"
   else let d = gcd (abs a) (abs b) in
     Fraction(a/d, b/d)

  let to_string (Fraction(a,b)) =
    Printf.sprintf "Fraction(%d,%d)" a b

  let add (Fraction(a1,b1)) (Fraction(a2,b2)) =
    make (a1*b2 + a2*b1) (b1*b2)

  let mult (Fraction(a1,b1)) (Fraction(a2,b2)) =
    make (a1*a2) (b1*b2)
end;;

module Fraction :
  sig
    type t = Fraction of int * int
    val gcd : int -> int -> int
    val make : int -> int -> t
    val to_string : t -> string
    val add : t -> t -> t
    val mult : t -> t -> t
  end

Định nghĩa này có thể được sử dụng như thế này:

# Fraction.add (Fraction.make 8 6) (Fraction.make 14 21);;
- : Fraction.t = Fraction.Fraction (2, 1)

Bất cứ ai cũng có thể trực tiếp tạo ra các giá trị của phân số loại, bỏ qua mạng lưới an toàn được tích hợp trong Fraction.make:

# Fraction.Fraction(0,0);;
- : Fraction.t = Fraction.Fraction (0, 0)

Để ngăn chặn điều này, có thể ẩn định nghĩa cụ thể của loại Fraction.tnhư thế:

# module AbstractFraction : sig
  type t
  val make : int -> int -> t
  val to_string : t -> string
  val add : t -> t -> t
  val mult : t -> t -> t
end = Fraction;;

module AbstractFraction :
sig
  type t
  val make : int -> int -> t
  val to_string : t -> string
  val add : t -> t -> t
  val mult : t -> t -> t
end

Cách duy nhất để tạo một AbstractFraction.tlà sử dụng AbstractFraction.makehàm.

Các kiểu dữ liệu trừu tượng trong Lược đồ

Ngôn ngữ Scheme không có cùng cơ chế của các kiểu dữ liệu trừu tượng như OCaml. Nó dựa vào người dùng, không nhìn vào tấm thảm để đạt được sự đóng gói.

Trong Lược đồ, thông thường sẽ xác định các vị từ như fraction?nhận ra các giá trị tạo cơ hội để xác thực đầu vào. Theo kinh nghiệm của tôi, việc sử dụng chủ yếu là để cho phép người dùng xác thực đầu vào của nó, nếu anh ta giả mạo một giá trị, thay vì xác thực đầu vào trong mỗi cuộc gọi thư viện.

Tuy nhiên, có một số chiến lược để thực thi sự trừu tượng hóa các giá trị được trả về, như trả lại bao đóng mang lại giá trị khi áp dụng hoặc trả về tham chiếu đến giá trị trong nhóm do thư viện quản lý - nhưng tôi chưa bao giờ thấy bất kỳ giá trị nào trong thực tế.


+1 Điều đáng nói là không phải tất cả các ngôn ngữ OO đều thực thi đóng gói.
Michael Shaw

5

Đóng gói không phải là một tính năng đi kèm với OOP. Bất kỳ ngôn ngữ hỗ trợ mô đun hóa thích hợp có nó.

Đây là cách bạn làm điều đó trong Haskell:

-- Rational.hs
module Rational (
    -- This is the export list. Functions not in this list aren't visible to importers.
    Rational, -- Exports the data type, but not its constructor.
    ratio,
    numerator,
    denominator
    ) where

data Rational = Rational Int Int

-- This is the function we provide for users to create rationals
ratio :: Int -> Int -> Rational
ratio num den = let (num', den') = reduce num den
                 in Rational num' den'

-- These are the member accessors
numerator :: Rational -> Int
numerator (Rational num _) = num

denominator :: Rational -> Int
denominator (Rational _ den) = den

reduce :: Int -> Int -> (Int, Int)
reduce a b = let g = gcd a b
             in (a `div` g, b `div` g)

Bây giờ, để tạo một Rational, bạn sử dụng hàm tỷ lệ, thực thi bất biến. Bởi vì dữ liệu là bất biến, sau này bạn không thể vi phạm bất biến.

Tuy nhiên, điều này làm bạn mất một cái gì đó: người dùng không còn có thể sử dụng khai báo giải cấu trúc tương tự như mẫu số và sử dụng tử số.


4

Bạn làm theo cách tương tự: tạo một hàm tạo thực thi ràng buộc và đồng ý sử dụng hàm tạo đó bất cứ khi nào bạn tạo một giá trị mới.

multiply lhs rhs = ReducedFraction (lhs.num * rhs.num) (lhs.denom * rhs.denom)

Nhưng Karl, trong OOP bạn không phải đồng ý sử dụng hàm tạo. Ồ vậy sao

class Fraction:
  ...
  Fraction multiply(Fraction lhs, Fraction rhs):
    Fraction result = lhs.clone()
    result.num *= rhs.num
    result.denom *= rhs.denom
    return result

Trên thực tế, cơ hội cho loại lạm dụng này ít hơn trong FP. Bạn phải đặt constructor cuối cùng, vì tính bất biến. Tôi ước mọi người sẽ ngừng nghĩ về việc đóng gói như một kiểu bảo vệ chống lại những đồng nghiệp bất tài, hoặc làm giảm nhu cầu truyền đạt các ràng buộc. Nó không làm điều đó. Nó chỉ giới hạn những nơi bạn phải kiểm tra. Lập trình viên FP tốt cũng sử dụng đóng gói. Nó chỉ xuất hiện dưới hình thức truyền đạt một vài chức năng ưa thích để thực hiện một số loại sửa đổi nhất định.


Chà, có thể (và thành ngữ) để viết mã bằng C #, chẳng hạn, điều này không cho phép những gì bạn đã làm ở đó. Và tôi nghĩ rằng có một sự khác biệt khá rõ ràng giữa một lớp duy nhất chịu trách nhiệm thực thi một bất biến và mọi chức năng được viết bởi bất kỳ ai, bất cứ nơi nào sử dụng một loại nhất định phải thực thi cùng một bất biến.
Ben Aaronson

@BenAaronson Lưu ý sự khác biệt giữa "thi hành""truyền bá" một bất biến.
rwong

1
+1. Kỹ thuật này thậm chí còn mạnh hơn trong FP vì các giá trị bất biến không thay đổi; do đó bạn có thể chứng minh mọi thứ về chúng "một lần và mãi mãi" bằng cách sử dụng các loại. Điều này là không thể đối với các đối tượng có thể thay đổi bởi vì những gì đúng với chúng bây giờ có thể không đúng sau này; tốt nhất bạn có thể kiểm tra lại trạng thái của đối tượng.
Doval

@Doval Tôi không thấy nó. Đặt sang một bên rằng hầu hết (?) Các ngôn ngữ OO chính có cách biến các biến thành bất biến. Trong OO tôi có: Tạo một thể hiện, sau đó hàm của tôi sẽ thay đổi các giá trị của thể hiện đó theo cách có thể hoặc không phù hợp với bất biến. Trong FP tôi có: Tạo một thể hiện, sau đó hàm của tôi tạo một thể hiện thứ hai với các giá trị khác nhau theo cách có thể hoặc không phù hợp với bất biến. Tôi không thấy sự bất biến đã giúp tôi cảm thấy tự tin hơn nữa rằng sự bất biến của tôi phù hợp với tất cả các trường hợp thuộc loại
Ben Aaronson

2
@BenAaronson Tính không thay đổi sẽ không giúp bạn chứng minh rằng bạn đã triển khai loại của mình một cách chính xác (nghĩa là tất cả các hoạt động bảo tồn một số bất biến nhất định.) Điều tôi nói là nó cho phép bạn truyền bá sự thật về các giá trị. Bạn mã hóa một số điều kiện (ví dụ số này là chẵn) trong một loại (bằng cách kiểm tra nó trong hàm tạo) và giá trị được tạo ra là bằng chứng cho thấy giá trị ban đầu thỏa mãn điều kiện. Với các đối tượng có thể thay đổi, bạn kiểm tra trạng thái hiện tại và giữ kết quả ở dạng boolean. Boolean đó chỉ tốt cho đến khi đối tượng không bị đột biến do đó điều kiện là sai.
Doval
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.