Làm thế nào một chức năng thời gian có thể tồn tại trong lập trình chức năng?


646

Tôi phải thừa nhận rằng tôi không biết nhiều về lập trình chức năng. Tôi đã đọc về nó từ đây và ở đó, và vì vậy biết rằng trong lập trình chức năng, một hàm trả về cùng một đầu ra, cho cùng một đầu vào, bất kể hàm đó được gọi bao nhiêu lần. Nó chính xác giống như một hàm toán học ước tính cho cùng một đầu ra cho cùng một giá trị của các tham số đầu vào liên quan đến biểu thức hàm.

Ví dụ, hãy xem xét điều này:

f(x,y) = x*x + y; // It is a mathematical function

Cho dù bạn sử dụng bao nhiêu lần f(10,4), giá trị của nó sẽ luôn như vậy 104. Như vậy, bất cứ nơi nào bạn đã viết f(10,4), bạn có thể thay thế nó 104, mà không làm thay đổi giá trị của toàn bộ biểu thức. Thuộc tính này được gọi là tính minh bạch tham chiếu của một biểu thức.

Như Wikipedia nói ( liên kết ),

Ngược lại, trong mã chức năng, giá trị đầu ra của hàm chỉ phụ thuộc vào các đối số được nhập vào hàm, do đó, gọi hàm f hai lần với cùng một giá trị cho một đối số x sẽ tạo ra cùng một kết quả f (x) cả hai lần.

Một hàm thời gian (trả về thời gian hiện tại ) có thể tồn tại trong lập trình hàm không?

  • Nếu có, làm thế nào nó có thể tồn tại? Nó không vi phạm nguyên tắc lập trình chức năng? Nó đặc biệt vi phạm tính minh bạch tham chiếu là một trong những tài sản của lập trình chức năng (nếu tôi hiểu chính xác về nó).

  • Hoặc nếu không, làm thế nào người ta có thể biết thời gian hiện tại trong lập trình chức năng?


15
Tôi nghĩ rằng hầu hết (hoặc tất cả) các ngôn ngữ chức năng không quá nghiêm ngặt và kết hợp lập trình chức năng và mệnh lệnh. Ít nhất, đây là ấn tượng của tôi từ F #.
Alex F

13
@Adam: Làm thế nào để người gọi biết thời gian hiện tại ở nơi đầu tiên?
Nawaz

29
@Adam: Thật ra nó là bất hợp pháp (như trong: không thể) trong các ngôn ngữ chức năng thuần túy.
sepp2k

47
@Adam: Khá nhiều. Một ngôn ngữ có mục đích chung là thuần túy thường cung cấp một số phương tiện để có được "trạng thái thế giới" (nghĩa là những thứ như thời gian hiện tại, các tệp trong một thư mục, v.v.) mà không phá vỡ tính minh bạch tham chiếu. Trong Haskell, đó là đơn vị IO và trong Clean, nó thuộc loại thế giới. Vì vậy, trong các ngôn ngữ đó, một hàm cần thời gian hiện tại sẽ lấy nó làm đối số hoặc nó sẽ cần trả về một hành động IO thay vì kết quả thực tế của nó (Haskell) hoặc lấy trạng thái thế giới làm đối số (Sạch).
sepp2k

12
Khi nghĩ về FP, thật dễ để quên: một chiếc máy tính là một khối lớn của trạng thái có thể thay đổi. FP không thay đổi điều đó, nó chỉ che giấu nó.
Daniel

Câu trả lời:


176

Một cách khác để giải thích nó là: không có chức năng nào có thể có được thời gian hiện tại (vì nó liên tục thay đổi), nhưng một hành động có thể có được thời gian hiện tại. Giả sử đó getClockTimelà một hằng số (hoặc một hàm rỗng, nếu bạn muốn) đại diện cho hành động nhận thời gian hiện tại. Đây hành động là như nhau mỗi khi có vấn đề khi nó được sử dụng để nó là một hằng số thực.

Tương tự như vậy, giả sử printlà một chức năng cần một chút thời gian biểu diễn và in nó ra bàn điều khiển. Vì các lệnh gọi hàm không thể có tác dụng phụ trong một ngôn ngữ chức năng thuần túy, thay vào đó chúng ta tưởng tượng rằng đó là một hàm lấy dấu thời gian và trả về hành động in nó ra bàn điều khiển. Một lần nữa, đây là một chức năng thực sự, bởi vì nếu bạn cho nó cùng dấu thời gian, nó sẽ trả về cùng một hành động in nó mỗi lần.

Bây giờ, làm thế nào bạn có thể in thời gian hiện tại lên bàn điều khiển? Vâng, bạn phải kết hợp hai hành động. Vậy làm thế nào chúng ta có thể làm điều đó? Chúng ta không thể chỉ cần vượt qua getClockTimeđể print, vì in hy vọng một dấu thời gian, không phải là một hành động. Nhưng chúng ta có thể tưởng tượng rằng có một nhà điều hành, >>=kết hợp hai hành động, một trong đó được một dấu thời gian, và một trong đó có một như là đối số và in nó. Áp dụng điều này cho các hành động được đề cập trước đó, kết quả là ... tadaaa ... một hành động mới có được thời gian hiện tại và in nó. Và đây là tình cờ chính xác cách nó được thực hiện trong Haskell.

Prelude> System.Time.getClockTime >>= print
Fri Sep  2 01:13:23 東京 (標準時) 2011

Vì vậy, về mặt khái niệm, bạn có thể xem nó theo cách này: Một chương trình chức năng thuần túy không thực hiện bất kỳ I / O nào, nó xác định một hành động , sau đó hệ thống thời gian chạy thực thi. Các hành động là như nhau mỗi lần, nhưng kết quả của thực hiện nó phụ thuộc vào hoàn cảnh của khi nó được thực thi.

Tôi không biết điều này có rõ ràng hơn những lời giải thích khác không, nhưng đôi khi nó giúp tôi nghĩ về nó theo cách này.


33
Nó không thuyết phục tôi. Bạn thuận tiện gọi là getClockTimemột hành động thay vì một chức năng. Chà, nếu bạn gọi như vậy, sau đó gọi mọi hành động chức năng , thì ngay cả lập trình bắt buộc cũng sẽ trở thành chương trình chức năng. Hoặc có thể, bạn muốn gọi nó là actional programmming.
Nawaz

92
@Nawaz: Điều quan trọng cần lưu ý ở đây là bạn không thể thực thi một hành động từ bên trong một chức năng. Bạn chỉ có thể kết hợp các hành động và chức năng với nhau để tạo ra các hành động mới. Cách duy nhất để thực hiện một hành động là kết hợp nó thành mainhành động của bạn . Điều này cho phép mã chức năng thuần túy được tách ra khỏi mã mệnh lệnh và sự phân tách này được thực thi bởi hệ thống loại. Đối xử với các hành động như các đối tượng hạng nhất cũng cho phép bạn vượt qua chúng và xây dựng các "cấu trúc điều khiển" của riêng bạn.
hammar

36
Không phải tất cả mọi thứ trong Haskell đều là một chức năng - điều đó hoàn toàn vô nghĩa. Hàm là một cái gì đó có kiểu chứa một ->- đó là cách tiêu chuẩn xác định thuật ngữ và đó thực sự là định nghĩa hợp lý duy nhất trong ngữ cảnh của Haskell. Vì vậy, một cái gì đó có loại là IO Whateverkhông một hàm.
sepp2k

9
@ sepp2k Vậy, myList :: [a -> b] là một hàm? ;)
fuz

8
@ThomasEding Tôi thực sự đến bữa tiệc muộn, nhưng tôi chỉ muốn làm rõ điều này: putStrLnkhông phải là một hành động - đó là một chức năng trả về một hành động. getLinelà một biến có chứa một hành động. Các hành động là các giá trị, biến và hàm là "các thùng chứa" / "nhãn" mà chúng tôi cung cấp cho các hành động đó.
kqr

356

Có và không.

Ngôn ngữ lập trình chức năng khác nhau giải quyết chúng khác nhau.

Trong Haskell (một thứ rất thuần khiết), tất cả những thứ này phải xảy ra trong một thứ gọi là I / O Monad - xem tại đây .

Bạn có thể nghĩ về nó như đưa một đầu vào (và đầu ra) khác vào chức năng của bạn (trạng thái thế giới) hoặc dễ dàng hơn như một nơi "không ổn định" như nhận được thời gian thay đổi xảy ra.

Các ngôn ngữ khác như F # chỉ có một số tính không ổn định được xây dựng và do đó bạn có thể có một hàm trả về các giá trị khác nhau cho cùng một đầu vào - giống như các ngôn ngữ mệnh lệnh thông thường .

Như Jeffrey Burka đã đề cập trong bình luận của mình: Đây là phần giới thiệu hay về I / O Monad trực tiếp từ wiki Haskell.


223
Điều cốt yếu để nhận ra về đơn vị IO trong Haskell là nó không chỉ là một bản hack để khắc phục vấn đề này; monads là một giải pháp chung cho vấn đề xác định chuỗi hành động trong một số bối cảnh. Một bối cảnh có thể là thế giới thực, mà chúng ta có đơn vị IO. Một bối cảnh khác là trong một giao dịch nguyên tử, mà chúng ta có đơn vị STM. Tuy nhiên, một bối cảnh khác là trong việc thực hiện một thuật toán thủ tục (ví dụ Knuth shuffle) như là một hàm thuần túy, mà chúng ta có đơn vị ST. Và bạn có thể xác định đơn nguyên của riêng bạn quá. Monads là một loại dấu chấm phẩy quá tải.
Paul Johnson

2
Tôi thấy hữu ích khi không gọi những thứ như nhận "chức năng" thời gian hiện tại mà là một cái gì đó như "thủ tục" (mặc dù giải pháp Haskell là một ngoại lệ đối với điều này).
singpolyma

từ "thủ tục" cổ điển của phối cảnh Haskell (những thứ có kiểu như '... -> ()') có phần tầm thường như một hàm thuần túy với ... -> () không thể làm gì cả.
Carsten

3
Thuật ngữ Haskell điển hình là "hành động".
Sebastian Redl

6
"Monads là một loại dấu chấm phẩy quá tải." +1
user2805751

147

Trong Haskell, người ta sử dụng một cấu trúc gọi là đơn nguyên để xử lý các tác dụng phụ. Về cơ bản, một đơn nguyên có nghĩa là bạn đóng gói các giá trị vào một thùng chứa và có một số chức năng để xâu chuỗi các chức năng từ các giá trị đến các giá trị bên trong một thùng chứa. Nếu container của chúng tôi có loại:

data IO a = IO (RealWorld -> (a,RealWorld))

chúng ta có thể thực hiện các hành động IO một cách an toàn. Loại này có nghĩa là: Một hành động của loại IOlà một hàm, lấy mã thông báo loại RealWorldvà trả về mã thông báo mới, cùng với kết quả.

Ý tưởng đằng sau điều này là mỗi hành động IO đột biến trạng thái bên ngoài, được biểu thị bằng mã thông báo ma thuật RealWorld. Sử dụng các đơn nguyên, người ta có thể xâu chuỗi nhiều chức năng làm thay đổi thế giới thực với nhau. Chức năng quan trọng nhất của một đơn nguyên là >>=, phát âm liên kết :

(>>=) :: IO a -> (a -> IO b) -> IO b

>>=nhận một hành động và một chức năng lấy kết quả của hành động này và tạo ra một hành động mới từ hành động này. Kiểu trả về là hành động mới. Chẳng hạn, giả sử có một hàm now :: IO String, trả về một Chuỗi biểu thị thời gian hiện tại. Chúng ta có thể xâu chuỗi nó với chức năng putStrLnin ra:

now >>= putStrLn

Hoặc được viết bằng do-Notation, vốn quen thuộc hơn với một lập trình viên mệnh lệnh:

do currTime <- now
   putStrLn currTime

Tất cả điều này là thuần túy, khi chúng tôi ánh xạ đột biến và thông tin về thế giới bên ngoài vào RealWorldmã thông báo. Vì vậy, mỗi lần, bạn chạy hành động này, tất nhiên bạn nhận được một đầu ra khác nhau, nhưng đầu vào không giống nhau: RealWorldmã thông báo là khác nhau.


3
-1: Tôi không hài lòng với RealWorldmàn khói. Tuy nhiên, điều quan trọng nhất là làm thế nào đối tượng có mục đích này được truyền lại trong một chuỗi. Phần còn thiếu là nơi nó bắt đầu, nơi nguồn hoặc kết nối với thế giới thực - nó bắt đầu với chức năng chính chạy trong đơn vị IO.
u0b34a0f6ae

2
@ kaizer.se Bạn có thể nghĩ về một RealWorldđối tượng toàn cầu được truyền vào chương trình khi nó bắt đầu.
fuz

6
Về cơ bản, mainchức năng của bạn có một RealWorldđối số. Chỉ khi thực hiện thì nó mới được thông qua.
Louis Wasserman

13
Bạn thấy đấy, lý do tại sao họ che giấu RealWorldvà chỉ cung cấp các chức năng trừng phạt để thay đổi nó putStrLn, vì vậy một số lập trình viên Haskell không thay đổi RealWorldvới một trong những chương trình của họ như địa chỉ và ngày sinh của Haskell là họ trở thành hàng xóm bên cạnh lớn lên (điều này có thể làm hỏng tính liên tục không gian thời gian theo cách làm tổn thương ngôn ngữ lập trình Haskell.)
PyRulez

2
RealWorld -> (a, RealWorld) không bị phá vỡ như ẩn dụ ngay cả dưới sự đồng thời, miễn là bạn luôn nhớ rằng thế giới thực có thể bị thay đổi bởi các phần khác của vũ trụ bên ngoài chức năng của bạn (hoặc quá trình hiện tại của bạn) mọi lúc. Vì vậy (a) methaphor không bị phá vỡ và (b) mỗi khi một giá trị có RealWorldkiểu của nó được truyền cho một hàm, hàm phải được đánh giá lại, bởi vì thế giới thực sẽ thay đổi trong lúc đó ( được mô hình hóa như @fuz giải thích, trả lại một 'giá trị mã thông báo' khác nhau mỗi khi chúng ta tương tác với thế giới thực).
Qqwy

73

Hầu hết các ngôn ngữ lập trình chức năng không thuần túy, tức là chúng cho phép các hàm không chỉ phụ thuộc vào giá trị của chúng. Trong các ngôn ngữ đó, hoàn toàn có thể có một hàm trả về thời gian hiện tại. Từ các ngôn ngữ bạn đã gắn thẻ câu hỏi này với câu hỏi này áp dụng cho ScalaF # (cũng như hầu hết các biến thể khác của ML ).

Trong các ngôn ngữ như HaskellClean , thuần túy, tình huống sẽ khác. Trong Haskell, thời gian hiện tại sẽ không có sẵn thông qua một chức năng, nhưng được gọi là hành động IO, đó là cách đóng gói các tác dụng phụ của Haskell.

Trong Clean, nó sẽ là một hàm, nhưng hàm sẽ lấy giá trị thế giới làm đối số của nó và trả về giá trị thế giới mới (ngoài thời điểm hiện tại) làm kết quả. Hệ thống loại sẽ đảm bảo rằng mỗi giá trị thế giới chỉ có thể được sử dụng một lần (và mỗi chức năng tiêu thụ một giá trị thế giới sẽ tạo ra một giá trị mới). Bằng cách này, hàm thời gian sẽ phải được gọi với một đối số khác nhau mỗi lần và do đó sẽ được phép trả về một thời gian khác nhau mỗi lần.


2
Điều này làm cho âm thanh như thể Haskell và Clean làm những việc khác nhau. Từ những gì tôi hiểu, họ cũng làm như vậy, chỉ là Haskell đưa ra một cú pháp đẹp hơn (?) Để thực hiện điều này.
Konrad Rudolph

27
@Konrad: Họ làm điều tương tự theo nghĩa là cả hai đều sử dụng các tính năng hệ thống loại cho các hiệu ứng phụ trừu tượng, nhưng đó là về nó. Lưu ý rằng thật tốt khi giải thích đơn vị IO theo loại thế giới, nhưng tiêu chuẩn Haskell không thực sự xác định loại thế giới và thực tế không thể có được giá trị của loại Thế giới trong Haskell (trong khi thực tế rất có thể cần thiết trong sạch). Hơn nữa Haskell không có kiểu gõ duy nhất như một tính năng hệ thống loại, vì vậy nếu nó cho phép bạn truy cập vào một Thế giới, thì không thể đảm bảo rằng bạn sử dụng nó theo cách thuần túy theo cách Clean.
sepp2k

51

"Thời gian hiện tại" không phải là một chức năng. Nó là một tham số. Nếu mã của bạn phụ thuộc vào thời gian hiện tại, điều đó có nghĩa là mã của bạn được tham số hóa theo thời gian.


22

Nó hoàn toàn có thể được thực hiện một cách hoàn toàn chức năng. Có một số cách để làm điều đó, nhưng đơn giản nhất là có chức năng trả về thời gian không chỉ là thời gian mà còn là chức năng bạn phải gọi để có được phép đo lần sau .

Trong C # bạn có thể thực hiện nó như thế này:

// Exposes mutable time as immutable time (poorly, to illustrate by example)
// Although the insides are mutable, the exposed surface is immutable.
public class ClockStamp {
    public static readonly ClockStamp ProgramStartTime = new ClockStamp();
    public readonly DateTime Time;
    private ClockStamp _next;

    private ClockStamp() {
        this.Time = DateTime.Now;
    }
    public ClockStamp NextMeasurement() {
        if (this._next == null) this._next = new ClockStamp();
        return this._next;
    }
}

(Hãy nhớ rằng đây là một ví dụ có nghĩa là đơn giản, không thực tế. Đặc biệt, các nút danh sách không thể được thu gom rác vì chúng được bắt nguồn bởi ProgramStartTime.)

Lớp 'ClockStamp' này hoạt động như một danh sách được liên kết bất biến, nhưng thực sự các nút được tạo theo yêu cầu để chúng có thể chứa thời gian 'hiện tại'. Bất kỳ chức năng nào muốn đo thời gian nên có tham số 'clockStamp' và cũng phải trả về phép đo lần cuối trong kết quả của nó (vì vậy người gọi không thấy các phép đo cũ), như sau:

// Immutable. A result accompanied by a clockstamp
public struct TimeStampedValue<T> {
    public readonly ClockStamp Time;
    public readonly T Value;
    public TimeStampedValue(ClockStamp time, T value) {
        this.Time = time;
        this.Value = value;
    }
}

// Times an empty loop.
public static TimeStampedValue<TimeSpan> TimeALoop(ClockStamp lastMeasurement) {
    var start = lastMeasurement.NextMeasurement();
    for (var i = 0; i < 10000000; i++) {
    }
    var end = start.NextMeasurement();
    var duration = end.Time - start.Time;
    return new TimeStampedValue<TimeSpan>(end, duration);
}

public static void Main(String[] args) {
    var clock = ClockStamp.ProgramStartTime;
    var r = TimeALoop(clock);
    var duration = r.Value; //the result
    clock = r.Time; //must now use returned clock, to avoid seeing old measurements
}

Tất nhiên, sẽ hơi bất tiện khi phải vượt qua phép đo cuối cùng trong và ngoài, vào và ra, vào và ra. Có nhiều cách để ẩn bảng mẫu, đặc biệt là ở cấp độ thiết kế ngôn ngữ. Tôi nghĩ rằng Haskell sử dụng loại mánh khóe này và sau đó che giấu những phần xấu xí bằng cách sử dụng các đơn nguyên.


Thật thú vị, nhưng i++trong vòng lặp for không tham chiếu minh bạch;)
snim2

@ snim2 Tôi không hoàn hảo. : P Hãy an ủi trong thực tế rằng tính đột biến bẩn không ảnh hưởng đến tính minh bạch tham chiếu của kết quả. Nếu bạn vượt qua cùng một 'lần mua hàng cuối cùng' trong hai lần, bạn sẽ có được số đo tiếp theo cũ và trả lại kết quả tương tự.
Craig Gidney

@Strilanc Cảm ơn vì điều này. Tôi nghĩ trong mã bắt buộc, vì vậy thật thú vị khi thấy các khái niệm chức năng được giải thích theo cách này. Sau đó tôi có thể tưởng tượng một ngôn ngữ nơi sạch hơn về mặt tự nhiên và cú pháp này.
Thế chiến.

Trên thực tế, bạn cũng có thể đi theo con đường đơn nguyên trong C #, do đó tránh việc vượt qua dấu thời gian rõ ràng. Bạn cần một cái gì đó như struct TimeKleisli<Arg, Res> { private delegate Res(TimeStampedValue<Arg>); }. Nhưng mã với điều này vẫn sẽ không đẹp như Haskell với docú pháp.
leftaroundabout

@leftaroundabout bạn có thể giả vờ rằng bạn có một đơn vị trong C # bằng cách thực hiện chức năng liên kết như một phương thức được gọi SelectMany, cho phép cú pháp hiểu truy vấn. Mặc dù vậy, bạn vẫn không thể lập trình đa hình trên các đơn nguyên, vì vậy tất cả là một trận chiến khó khăn chống lại hệ thống loại yếu :(
sara

16

Tôi ngạc nhiên rằng không có câu trả lời hoặc ý kiến ​​nào đề cập đến than đá hoặc cưỡng chế. Thông thường, cưỡng chế được đề cập khi suy luận về cấu trúc dữ liệu vô hạn, nhưng nó cũng có thể áp dụng cho một luồng quan sát vô tận, chẳng hạn như thanh ghi thời gian trên CPU. Một mô hình đại số ẩn trạng thái; và các mô hình cưỡng chế quan sát trạng thái đó. ( Trạng thái xây dựng mô hình cảm ứng bình thường .)

Đây là một chủ đề nóng trong lập trình chức năng phản ứng. Nếu bạn quan tâm đến loại công cụ này, hãy đọc nó: http://digitalcommons.ohsu.edu/csetech/91/ (28 trang.)


3
Và nó liên quan đến câu hỏi này như thế nào?
Nawaz

5
Câu hỏi của bạn là về mô hình hóa hành vi phụ thuộc thời gian theo cách hoàn toàn chức năng, ví dụ, một hàm trả về đồng hồ hệ thống hiện tại. Bạn có thể tạo một cái gì đó tương đương với một đơn vị IO thông qua tất cả các hàm và cây phụ thuộc của chúng để có quyền truy cập vào trạng thái đó; hoặc bạn có thể mô hình hóa trạng thái bằng cách xác định các quy tắc quan sát thay vì quy tắc xây dựng. Đây là lý do tại sao mô hình hóa trạng thái phức tạp theo quy nạp trong lập trình chức năng có vẻ rất không tự nhiên, bởi vì trạng thái ẩn thực sự là một thuộc tính cưỡng chế .
Jeffrey Aguilera

Nguồn tuyệt vời! Có điều gì gần đây hơn không? Cộng đồng JS dường như vẫn đang vật lộn với sự trừu tượng hóa dữ liệu dòng.
Dmitri Zaitsev

12

Có, một hàm thuần túy có thể trả về thời gian, nếu nó đưa thời gian đó làm tham số. Đối số thời gian khác nhau, kết quả thời gian khác nhau. Sau đó, hình thành các chức năng khác của thời gian và kết hợp chúng với một từ vựng đơn giản về chức năng (-of-time) -transforming (bậc cao hơn). Vì cách tiếp cận là không trạng thái, thời gian ở đây có thể liên tục (không phụ thuộc vào độ phân giải) thay vì rời rạc, thúc đẩy rất nhiều mô đun . Trực giác này là nền tảng của lập trình phản ứng chức năng (FRP).


11

Đúng! Bạn nói đúng! Bây giờ () hoặc CurrentTime () hoặc bất kỳ chữ ký phương thức nào của hương vị đó không thể hiện tính minh bạch tham chiếu theo một cách. Nhưng theo chỉ dẫn cho trình biên dịch, nó được tham số hóa bởi đầu vào đồng hồ hệ thống.

Theo đầu ra, Now () có thể trông giống như không tuân theo tính minh bạch tham chiếu. Nhưng hành vi thực tế của đồng hồ hệ thống và chức năng trên nó là tuân thủ tính minh bạch tham chiếu.


11

Có, một chức năng thời gian nhận có thể tồn tại trong lập trình chức năng bằng cách sử dụng một phiên bản sửa đổi một chút về lập trình chức năng được gọi là lập trình chức năng không tinh khiết (mặc định hoặc chính là lập trình chức năng thuần túy).

Trong trường hợp nhận được thời gian (hoặc đọc tệp hoặc phóng tên lửa), mã cần phải tương tác với thế giới bên ngoài để hoàn thành công việc và thế giới bên ngoài này không dựa trên nền tảng thuần túy của lập trình chức năng. Để cho phép một thế giới lập trình chức năng thuần túy tương tác với thế giới bên ngoài không tinh khiết này, mọi người đã giới thiệu lập trình chức năng không tinh khiết. Xét cho cùng, phần mềm không tương tác với thế giới bên ngoài không hữu ích gì ngoài việc thực hiện một số tính toán toán học.

Rất ít ngôn ngữ lập trình lập trình chức năng có tính năng tạp chất này sẵn có trong đó sao cho không dễ tách ra mã nào không tinh khiết và mã nào là thuần túy (như F #, v.v.) và một số ngôn ngữ lập trình chức năng đảm bảo rằng khi bạn thực hiện một số nội dung không tinh khiết mã đó rõ ràng nổi bật so với mã thuần, như Haskell.

Một cách thú vị khác để thấy điều này là chức năng thời gian của bạn trong lập trình chức năng sẽ lấy một đối tượng "thế giới" có trạng thái hiện tại của thế giới như thời gian, số người sống trên thế giới, v.v. đối tượng sẽ luôn thuần khiết tức là bạn vượt qua trong cùng một trạng thái thế giới, bạn sẽ luôn nhận được cùng một lúc.


1
"Sau tất cả, một phần mềm không tương tác với thế giới bên ngoài sẽ không hữu ích gì ngoài việc thực hiện một số tính toán toán học." Theo tôi hiểu, ngay cả trong trường hợp này, đầu vào cho các tính toán sẽ được mã hóa cứng trong chương trình, cũng không hữu ích lắm. Ngay khi bạn muốn đọc dữ liệu đầu vào tính toán toán học từ tệp hoặc thiết bị đầu cuối, bạn cần mã không tinh khiết.
Giorgio

1
@Ankur: Đó là điều chính xác tương tự. Nếu chương trình đang tương tác với một cái gì đó khác hơn là chính nó (ví dụ thế giới thông qua bàn phím, có thể nói) thì nó vẫn không trong sạch.
danh tính

1
@Ankur: Vâng, tôi nghĩ bạn đúng! Mặc dù có thể không thực tế lắm khi truyền dữ liệu đầu vào lớn trên dòng lệnh, đây có thể là một cách thuần túy để thực hiện.
Giorgio

2
Có "đối tượng thế giới" bao gồm số người sống trên thế giới đã nâng máy tính thực thi lên một mức độ gần như toàn diện. Tôi nghĩ trường hợp bình thường là nó bao gồm những thứ như có bao nhiêu tệp trên HD của bạn và thư mục chính của người dùng hiện tại.
ziggystar

4
@ziggystar - "đối tượng thế giới" không thực sự bao gồm bất cứ điều gì - nó chỉ đơn giản là một ủy quyền cho trạng thái thay đổi của thế giới bên ngoài chương trình. Mục đích duy nhất của nó là đánh dấu rõ ràng trạng thái có thể thay đổi theo cách mà hệ thống loại có thể xác định nó.
Kris Nuttycombe

7

Câu hỏi của bạn giới thiệu hai biện pháp liên quan của ngôn ngữ máy tính: chức năng / mệnh lệnh và thuần túy / không tinh khiết.

Một ngôn ngữ chức năng xác định mối quan hệ giữa đầu vào và đầu ra của các chức năng và một ngôn ngữ bắt buộc mô tả các hoạt động cụ thể theo một thứ tự cụ thể để thực hiện.

Một ngôn ngữ thuần túy không tạo ra hoặc phụ thuộc vào các tác dụng phụ và một ngôn ngữ không tinh khiết sử dụng chúng xuyên suốt.

Một trăm phần trăm chương trình thuần túy về cơ bản là vô dụng. Họ có thể thực hiện một phép tính thú vị, nhưng vì họ không thể có tác dụng phụ nên họ không có đầu vào hoặc đầu ra nên bạn sẽ không bao giờ biết họ đã tính toán gì.

Để có ích ở tất cả, một chương trình phải có ít nhất là không tinh khiết. Một cách để làm cho một chương trình thuần túy trở nên hữu ích là đặt nó bên trong một lớp bọc không tinh khiết. Giống như chương trình Haskell chưa được kiểm tra này:

-- this is a pure function, written in functional style.
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

-- This is an impure wrapper around the pure function, written in imperative style
-- It depends on inputs and produces outputs.
main = do
    putStrLn "Please enter the input parameter"
    inputStr <- readLine
    putStrLn "Starting time:"
    getCurrentTime >>= print
    let inputInt = read inputStr    -- this line is pure
    let result = fib inputInt       -- this is also pure
    putStrLn "Result:"
    print result
    putStrLn "Ending time:"
    getCurrentTime >>= print

4
Sẽ rất hữu ích nếu bạn có thể giải quyết vấn đề cụ thể về thời gian và giải thích một chút về mức độ chúng tôi coi IOcác giá trị và kết quả là thuần túy.
AndrewC

Trong thực tế, ngay cả các chương trình thuần túy 100% làm nóng CPU, đó là một tác dụng phụ.
Jörg W Mittag

3

Bạn đang giới thiệu một chủ đề rất quan trọng trong lập trình chức năng, đó là thực hiện I / O. Cách mà nhiều ngôn ngữ thuần túy thực hiện là bằng cách sử dụng các ngôn ngữ dành riêng cho miền được nhúng, ví dụ: một ngôn ngữ con có nhiệm vụ là mã hóa các hành động , có thể có kết quả.

Ví dụ, thời gian chạy Haskell mong tôi xác định một hành động được gọi là main là bao gồm tất cả các hành động tạo nên chương trình của tôi. Thời gian chạy sau đó thực hiện hành động này. Hầu hết thời gian, khi làm như vậy nó thực thi mã thuần. Thỉnh thoảng bộ thực thi sẽ sử dụng dữ liệu được tính toán để thực hiện I / O và đưa dữ liệu trở lại thành mã thuần.

Bạn có thể phàn nàn rằng điều này nghe giống như gian lận, và theo một cách nào đó: bằng cách xác định các hành động và mong đợi bộ thực thi thực thi chúng, lập trình viên có thể làm mọi thứ mà một chương trình bình thường có thể làm. Nhưng hệ thống loại mạnh của Haskell tạo ra một rào cản mạnh mẽ giữa các phần thuần túy và "không trong sạch" của chương trình: bạn không thể chỉ cần thêm, giả sử, hai giây vào thời gian CPU hiện tại và in nó, bạn phải xác định một hành động dẫn đến hiện tại Thời gian của CPU và chuyển kết quả sang một hành động khác thêm hai giây và in kết quả. Tuy nhiên, việc viết quá nhiều chương trình được coi là phong cách xấu, bởi vì nó khiến bạn khó có thể suy ra những hiệu ứng nào gây ra, so với các loại Haskell cho chúng ta biết mọi thứ chúng ta có thể biết về giá trị là gì.

Ví dụ: clock_t c = time(NULL); printf("%d\n", c + 2);trong C, so với main = getCPUTime >>= \c -> print (c + 2*1000*1000*1000*1000)Haskell. Toán tử >>=được sử dụng để soạn các hành động, chuyển kết quả của hàm thứ nhất sang hàm dẫn đến hành động thứ hai. Điều này trông khá phức tạp, trình biên dịch Haskell hỗ trợ đường cú pháp cho phép chúng ta viết mã sau như sau:

type Clock = Integer -- To make it more similar to the C code

-- An action that returns nothing, but might do something
main :: IO ()
main = do
    -- An action that returns an Integer, which we view as CPU Clock values
    c <- getCPUTime :: IO Clock
    -- An action that prints data, but returns nothing
    print (c + 2*1000*1000*1000*1000) :: IO ()

Cái sau có vẻ khá cấp bách phải không?


1

Nếu có, làm thế nào nó có thể tồn tại? Nó không vi phạm nguyên tắc lập trình chức năng? Nó đặc biệt vi phạm tính minh bạch tham chiếu

Nó không tồn tại trong một ý nghĩa chức năng thuần túy.

Hoặc nếu không, làm thế nào người ta có thể biết thời gian hiện tại trong lập trình chức năng?

Đầu tiên có thể hữu ích để biết cách lấy thời gian trên máy tính. Về cơ bản có mạch trên tàu theo dõi thời gian (đó là lý do một máy tính thường sẽ cần một pin nhỏ). Sau đó, có thể có một số quy trình nội bộ đặt giá trị thời gian tại một thanh ghi bộ nhớ nhất định. Về cơ bản, đun sôi đến một giá trị mà CPU có thể lấy ra.


Đối với Haskell, có một khái niệm về 'Hành động IO' đại diện cho một loại có thể được thực hiện để thực hiện một số quy trình IO. Vì vậy, thay vì tham chiếu một timegiá trị, chúng tôi tham chiếu một IO Timegiá trị. Tất cả điều này sẽ hoàn toàn là chức năng. Chúng tôi không tham khảo timenhưng một cái gì đó dọc theo dòng chữ 'đọc giá trị của thanh ghi thời gian' .

Khi chúng ta thực sự thực hiện chương trình Haskell, hành động IO thực sự sẽ diễn ra.

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.