Làm thế nào bạn có thể làm bất cứ điều gì hữu ích mà không có trạng thái đột biến?


265

Gần đây tôi đã đọc rất nhiều thứ về lập trình chức năng và tôi có thể hiểu hầu hết về nó, nhưng điều duy nhất tôi không thể nghĩ ra là mã hóa không trạng thái. Dường như với tôi, đơn giản hóa việc lập trình bằng cách loại bỏ trạng thái có thể thay đổi giống như "đơn giản hóa" một chiếc xe hơi bằng cách xóa bảng điều khiển: sản phẩm hoàn chỉnh có thể đơn giản hơn, nhưng may mắn làm cho nó tương tác với người dùng cuối.

Chỉ cần về mọi ứng dụng người dùng tôi có thể nghĩ liên quan đến trạng thái như một khái niệm cốt lõi. Nếu bạn viết một tài liệu (hoặc một bài viết SO), trạng thái sẽ thay đổi với mỗi đầu vào mới. Hoặc nếu bạn chơi một trò chơi video, có rất nhiều biến trạng thái, bắt đầu bằng vị trí của tất cả các nhân vật, những người có xu hướng di chuyển liên tục. Làm thế nào bạn có thể làm bất cứ điều gì hữu ích mà không theo dõi các giá trị thay đổi?

Mỗi khi tôi tìm thấy một cái gì đó thảo luận về vấn đề này, nó được viết bằng chức năng kỹ thuật thực sự - ese giả định một nền tảng nặng nề mà tôi không có. Có ai biết một cách để giải thích điều này với một người có hiểu biết tốt, vững chắc về mã hóa mệnh lệnh nhưng ai là người hoàn toàn n00b về mặt chức năng?

EDIT: Một loạt các câu trả lời cho đến nay dường như đang cố gắng thuyết phục tôi về những lợi thế của các giá trị bất biến. Tôi có được phần đó. Nó làm cho ý nghĩa hoàn hảo. Điều tôi không hiểu là làm thế nào bạn có thể theo dõi các giá trị phải thay đổi và thay đổi liên tục mà không có các biến số có thể thay đổi.



1
Ý kiến ​​khiêm tốn cá nhân của tôi là nó giống như sức mạnh và tiền bạc. Luật lợi nhuận giảm dần áp dụng. Nếu bạn khá mạnh mẽ, có thể có ít động lực để mạnh hơn một chút nhưng sẽ không hại gì khi làm việc đó (và một số người làm với niềm đam mê). Điều tương tự áp dụng cho nhà nước đột biến toàn cầu. Sở thích cá nhân của tôi là chấp nhận rằng khi kỹ năng mã hóa của tôi phát triển, việc hạn chế số lượng trạng thái có thể thay đổi toàn cầu trong mã của tôi là điều tốt. Nó có thể không bao giờ hoàn hảo nhưng thật tốt khi làm việc để giảm thiểu trạng thái đột biến toàn cầu.
AturSams

Giống như với tiền, một điểm sẽ đạt được là đầu tư nhiều thời gian hơn vào nó, không còn hữu ích nữa và các ưu tiên khác sẽ tăng lên hàng đầu. Ví dụ, nếu bạn đạt được sức mạnh lớn nhất có thể (theo ẩn dụ của tôi) thì nó có thể không phục vụ bất kỳ mục đích hữu ích nào và thậm chí có thể trở thành gánh nặng. Nhưng vẫn tốt để phấn đấu hướng tới mục tiêu có thể không đạt được và đầu tư nguồn lực vừa phải vào nó.
AturSams

7
Tóm lại, trong FP, các hàm không bao giờ sửa đổi trạng thái. Cuối cùng, họ sẽ trả lại một cái gì đó thay thế trạng thái hiện tại. Nhưng nhà nước không bao giờ được sửa đổi (đột biến) tại chỗ.
jinglểula

Có nhiều cách để có được trạng thái mà không bị đột biến (sử dụng ngăn xếp từ những gì tôi hiểu), nhưng câu hỏi này có ý nghĩa bên cạnh quan điểm (mặc dù đó là một câu hỏi hay). Khó nói về cô đọng, nhưng đây là một bài viết hy vọng trả lời câu hỏi của bạn vừa.com / @ jbmilgrom / . TLDR là ngữ nghĩa của ngay cả một chương trình chức năng trạng thái là bất biến, tuy nhiên các giao tiếp b / w của chức năng chương trình được xử lý.
jbmilgrom

Câu trả lời:


166

Hoặc nếu bạn chơi một trò chơi video, có rất nhiều biến trạng thái, bắt đầu bằng vị trí của tất cả các nhân vật, những người có xu hướng di chuyển liên tục. Làm thế nào bạn có thể làm bất cứ điều gì hữu ích mà không theo dõi các giá trị thay đổi?

Nếu bạn quan tâm, đây là một loạt các bài viết mô tả lập trình trò chơi với Erlang.

Bạn có thể sẽ không thích câu trả lời này, nhưng bạn sẽ không nhận được chương trình chức năng cho đến khi bạn sử dụng nó. Tôi có thể đăng các mẫu mã và nói "Ở đây, bạn không thấy " - nhưng nếu bạn không hiểu cú pháp và các nguyên tắc cơ bản, thì mắt bạn sẽ sáng lên. Theo quan điểm của bạn, có vẻ như tôi đang làm điều tương tự như một ngôn ngữ bắt buộc, nhưng chỉ thiết lập tất cả các loại ranh giới để cố tình làm cho việc lập trình trở nên khó khăn hơn. Quan điểm của tôi, bạn chỉ đang trải qua nghịch lý Blub .

Lúc đầu tôi đã hoài nghi, nhưng tôi đã nhảy lên tàu lập trình chức năng vài năm trước và yêu nó. Thủ thuật với lập trình chức năng là có thể nhận ra các mẫu, các phép gán biến cụ thể và di chuyển trạng thái bắt buộc sang ngăn xếp. Ví dụ, một vòng lặp for trở thành đệ quy:

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

Nó không đẹp lắm, nhưng chúng tôi có hiệu ứng tương tự không có đột biến. Tất nhiên, bất cứ nơi nào có thể, chúng tôi muốn tránh lặp lại hoàn toàn và chỉ trừu tượng hóa nó đi:

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

Phương thức Seq.iter sẽ liệt kê thông qua bộ sưu tập và gọi hàm ẩn danh cho mỗi mục. Rất tiện dụng :)

Tôi biết, in số không chính xác ấn tượng. Tuy nhiên, chúng ta có thể sử dụng cùng một cách tiếp cận với các trò chơi: giữ tất cả trạng thái trong ngăn xếp và tạo một đối tượng mới với các thay đổi của chúng ta trong lệnh gọi đệ quy. Theo cách này, mỗi khung hình là một ảnh chụp nhanh không trạng thái của trò chơi, trong đó mỗi khung hình chỉ đơn giản tạo ra một đối tượng hoàn toàn mới với những thay đổi mong muốn của bất kỳ đối tượng không trạng thái nào cần cập nhật. Mã giả cho điều này có thể là:

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

Các phiên bản mệnh lệnh và chức năng là giống hệt nhau, nhưng phiên bản chức năng rõ ràng không sử dụng trạng thái có thể thay đổi. Mã chức năng giữ cho tất cả trạng thái được giữ trên ngăn xếp - điều tuyệt vời của phương pháp này là, nếu có lỗi xảy ra, việc gỡ lỗi rất dễ dàng, tất cả những gì bạn cần là theo dõi ngăn xếp.

Điều này chia tỷ lệ lên tới bất kỳ số lượng đối tượng nào trong trò chơi, bởi vì tất cả các đối tượng (hoặc bộ sưu tập các đối tượng liên quan) có thể được hiển thị trong luồng của riêng chúng.

Chỉ cần về mọi ứng dụng người dùng tôi có thể nghĩ liên quan đến trạng thái như một khái niệm cốt lõi.

Trong các ngôn ngữ chức năng, thay vì làm thay đổi trạng thái của các đối tượng, chúng ta chỉ cần trả về một đối tượng mới với những thay đổi mà chúng ta muốn. Nó hiệu quả hơn âm thanh. Các cấu trúc dữ liệu, ví dụ, rất dễ biểu diễn dưới dạng cấu trúc dữ liệu bất biến. Ví dụ, ngăn xếp rất dễ thực hiện:

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

Đoạn mã trên xây dựng hai danh sách bất biến, nối chúng lại với nhau để tạo một danh sách mới và nối thêm kết quả. Không có trạng thái đột biến được sử dụng bất cứ nơi nào trong ứng dụng. Nó trông hơi cồng kềnh, nhưng đó chỉ vì C # là một ngôn ngữ dài dòng. Đây là chương trình tương đương trong F #:

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

Không thể thay đổi cần thiết để tạo và thao tác danh sách. Gần như tất cả các cấu trúc dữ liệu có thể dễ dàng chuyển đổi thành tương đương chức năng của chúng. Tôi đã viết một trang ở đây cung cấp các triển khai bất biến của các ngăn xếp, hàng đợi, đống trái, cây đỏ đen, danh sách lười biếng. Không một đoạn mã nào chứa bất kỳ trạng thái có thể thay đổi nào. Để "biến đổi" một cây, tôi tạo một cây hoàn toàn mới với nút mới tôi muốn - điều này rất hiệu quả vì tôi không cần phải tạo một bản sao của mỗi nút trong cây, tôi có thể sử dụng lại nút cũ trong nút mới của mình cây.

Sử dụng một ví dụ quan trọng hơn, tôi cũng đã viết trình phân tích cú pháp SQL này hoàn toàn không trạng thái (hoặc ít nhất mã của tôi là không trạng thái, tôi không biết liệu thư viện từ vựng cơ bản có phải là trạng thái không trạng thái không).

Lập trình phi trạng thái cũng biểu cảm và mạnh mẽ như lập trình trạng thái, nó chỉ cần một chút luyện tập để rèn luyện bản thân để bắt đầu suy nghĩ không trạng thái. Tất nhiên, "lập trình không trạng thái khi có thể, lập trình trạng thái khi cần thiết" dường như là phương châm của hầu hết các ngôn ngữ chức năng không tinh khiết. Không có hại gì khi rơi vào các biến đổi khi phương pháp chức năng không sạch sẽ và hiệu quả.


7
Tôi thích ví dụ Pacman. Nhưng điều đó có thể giải quyết một vấn đề chỉ để nêu lên một vấn đề khác: Điều gì xảy ra nếu một thứ khác giữ tham chiếu đến đối tượng Pacman hiện tại? Sau đó, nó sẽ không được thu gom rác và thay thế; thay vào đó bạn kết thúc với hai bản sao của đối tượng, một trong số đó không hợp lệ. Làm thế nào để bạn xử lý vấn đề này?
Mason Wheeler

9
Rõ ràng là bạn cần tạo một "thứ khác" mới với đối tượng Pacman mới;) Tất nhiên, nếu chúng ta đi quá xa, chúng ta sẽ tạo lại biểu đồ đối tượng cho toàn bộ thế giới của mình mỗi khi có gì đó thay đổi. Một cách tiếp cận tốt hơn được mô tả ở đây ( prog21.dadgum.com/26.html ): thay vì để các đối tượng tự cập nhật và tất cả các phụ thuộc của chúng, việc chuyển các thông điệp về trạng thái của chúng sang vòng lặp sự kiện sẽ xử lý tất cả các thông báo đang cập nhật Điều này giúp dễ dàng hơn nhiều trong việc quyết định đối tượng nào trong biểu đồ cần cập nhật và đối tượng nào không.
Juliet

6
@Juliet, tôi có một nghi ngờ - trong suy nghĩ hoàn toàn cấp bách của tôi, đệ quy phải kết thúc vào một lúc nào đó, nếu không cuối cùng bạn sẽ tạo ra một tràn ngăn xếp. Trong ví dụ về pacman đệ quy, ngăn xếp được giữ ở vịnh như thế nào - đối tượng có được bật lên ở đầu hàm không?
BlueStrat

9
@BlueStrat - câu hỏi hay ... nếu đó là "cuộc gọi đuôi" ... tức là cuộc gọi đệ quy là điều cuối cùng trong chức năng ... thì hệ thống không cần tạo khung ngăn xếp mới ... nó có thể chỉ sử dụng lại cái trước Đây là một tối ưu hóa phổ biến cho các ngôn ngữ lập trình chức năng. vi.wikipedia.org/wiki/Tail_call
reteptilian

4
@MichaelOsofsky, khi tương tác với cơ sở dữ liệu và API, luôn có một "thế giới bên ngoài" có trạng thái giao tiếp. Trong trường hợp này, bạn không thể thực hiện 100% chức năng. Điều quan trọng là giữ cho mã 'không liên kết' này bị cô lập và trừu tượng hóa để chỉ có một mục nhập và một lối ra cho thế giới bên ngoài. Bằng cách này bạn có thể giữ phần còn lại của mã của bạn hoạt động.
Chielt

76

Câu trả lời ngắn gọn: bạn không thể.

Vì vậy, những gì ồn ào về bất biến sau đó?

Nếu bạn thành thạo ngôn ngữ bắt buộc, thì bạn sẽ biết rằng "toàn cầu là xấu". Tại sao? Bởi vì họ giới thiệu (hoặc có tiềm năng giới thiệu) một số phụ thuộc rất khó gỡ rối trong mã của bạn. Và phụ thuộc là không tốt; bạn muốn mã của bạn được mô-đun . Các phần của chương trình không ảnh hưởng đến các phần khác ít nhất có thể. Và FP đưa bạn đến với chén thánh của mô đun: không có tác dụng phụ nào cả . Bạn chỉ cần có f (x) = y. Đặt x vào, đưa y ra. Không có thay đổi đối với x hoặc bất cứ điều gì khác. FP làm cho bạn ngừng suy nghĩ về trạng thái, và bắt đầu suy nghĩ về các giá trị. Tất cả các chức năng của bạn chỉ đơn giản là nhận giá trị và tạo ra giá trị mới.

Điều này có một số lợi thế.

Trước hết, không có tác dụng phụ có nghĩa là các chương trình đơn giản hơn, dễ lý luận hơn. Không phải lo lắng rằng việc giới thiệu một phần mới của chương trình sẽ can thiệp và làm hỏng một phần đang hoạt động.

Thứ hai, điều này làm cho chương trình song song hóa một cách tầm thường (song song hiệu quả là một vấn đề khác).

Thứ ba, có một số lợi thế hiệu suất có thể. Nói rằng bạn có một chức năng:

double x = 2 * x

Bây giờ bạn đặt giá trị 3 vào và bạn nhận được giá trị là 6. Mỗi lần. Nhưng bạn có thể làm điều đó trong mệnh lệnh là tốt, phải không? Vâng. Nhưng vấn đề là trong mệnh lệnh, bạn có thể làm nhiều hơn thế . Tôi có thể làm:

int y = 2;
int double(x){ return x * y; }

nhưng tôi cũng có thể làm

int y = 2;
int double(x){ return x * (y++); }

Trình biên dịch bắt buộc không biết liệu tôi sẽ có tác dụng phụ hay không, điều này khiến việc tối ưu hóa trở nên khó khăn hơn (nghĩa là gấp đôi 2 không phải là 4 mỗi lần). Người có chức năng biết tôi sẽ không - do đó, nó có thể tối ưu hóa mỗi khi thấy "double 2".

Bây giờ, mặc dù việc tạo ra các giá trị mới mỗi lần dường như vô cùng lãng phí đối với các loại giá trị phức tạp về bộ nhớ máy tính, nhưng nó không phải như vậy. Bởi vì, nếu bạn có f (x) = y và các giá trị x và y "hầu như giống nhau" (ví dụ: các cây chỉ khác nhau ở một vài lá) thì x và y có thể chia sẻ các phần của bộ nhớ - bởi vì cả hai sẽ không biến đổi .

Vì vậy, nếu điều không thể thay đổi này là rất lớn, tại sao tôi lại trả lời rằng bạn không thể làm bất cứ điều gì hữu ích mà không có trạng thái đột biến. Chà, không có tính biến đổi, toàn bộ chương trình của bạn sẽ là một hàm f (x) = y khổng lồ. Và điều tương tự cũng xảy ra với tất cả các phần trong chương trình của bạn: chỉ các chức năng và các chức năng theo nghĩa "thuần túy" ở đó. Như tôi đã nói, điều này có nghĩa là f (x) = y mỗi lần. Vì vậy, ví dụ readFile ("myFile.txt") sẽ cần trả về cùng một giá trị chuỗi mỗi lần. Không quá hữu ích.

Do đó, mỗi FP cung cấp một số phương tiện trạng thái đột biến. Các ngôn ngữ chức năng "thuần túy" (ví dụ Haskell) thực hiện việc này bằng cách sử dụng các khái niệm hơi đáng sợ như đơn nguyên, trong khi các ngôn ngữ "không trong sạch" (ví dụ ML) cho phép điều này trực tiếp.

Và tất nhiên, các ngôn ngữ chức năng đi kèm với một loạt các tính năng khác giúp cho việc lập trình hiệu quả hơn, chẳng hạn như các chức năng hạng nhất, v.v.


2
<< readFile ("myFile.txt") sẽ cần trả về cùng một giá trị chuỗi mỗi lần. Không quá hữu ích. >> Tôi đoán nó rất hữu ích miễn là bạn ẩn toàn cầu, một hệ thống tập tin. Nếu bạn coi nó như một tham số thứ hai và để các tiến trình khác trả về một tham chiếu mới cho hệ thống tệp mỗi khi chúng sửa đổi nó với filesystem2 = write (filesystem1, fd, pos, "string") và để tất cả các quy trình trao đổi tham chiếu của chúng với hệ thống tệp , chúng ta có thể có được một bức tranh rõ ràng hơn về hệ điều hành.
lươn ghEEz

@eelghEEz, đây là cách tiếp cận tương tự mà Datomic đưa vào cơ sở dữ liệu.
Jason

1
+1 để so sánh rõ ràng và súc tích giữa các mô hình. Một gợi ý là int double(x){ return x * (++y); }vì hiện tại vẫn sẽ là 4, mặc dù vẫn có tác dụng phụ không được báo cáo, trong khi đó ++ysẽ trở lại 6.
BrainFRZ

@eelghEEz Tôi không chắc chắn về một sự thay thế, thực sự, có ai khác không? Để giới thiệu thông tin vào bối cảnh FP (thuần túy), bạn "thực hiện phép đo", ví dụ "tại dấu thời gian X, nhiệt độ là Y". Nếu ai đó hỏi về nhiệt độ, họ có thể ngầm hiểu X = ngay bây giờ, nhưng họ có thể không thể yêu cầu nhiệt độ là một hàm phổ biến của thời gian, phải không? FP xử lý trạng thái bất biến, và bạn phải tạo một trạng thái bất biến - từ các nguồn bên trong bên ngoài - từ một trạng thái có thể thay đổi. Các chỉ số, dấu thời gian, v.v ... rất hữu ích nhưng trực giao với khả năng biến đổi - giống như VCS là để kiểm soát phiên bản.
John P

29

Lưu ý rằng việc lập trình chức năng không có 'trạng thái' là một chút sai lệch và có thể là nguyên nhân của sự nhầm lẫn. Nó chắc chắn không có "trạng thái có thể thay đổi", nhưng nó vẫn có thể có các giá trị bị thao túng; chúng chỉ không thể được thay đổi tại chỗ (ví dụ: bạn phải tạo các giá trị mới từ các giá trị cũ).

Đây là một sự đơn giản hóa quá mức, nhưng hãy tưởng tượng bạn có một ngôn ngữ OO, trong đó tất cả các thuộc tính trên các lớp chỉ được đặt một lần trong hàm tạo, tất cả các phương thức là các hàm tĩnh. Bạn vẫn có thể thực hiện khá nhiều phép tính bằng cách có các phương thức lấy các đối tượng chứa tất cả các giá trị họ cần cho các phép tính của họ và sau đó trả về các đối tượng mới với kết quả (có thể là một thể hiện mới của cùng một đối tượng).

Có thể "khó" để dịch mã hiện tại sang mô hình này, nhưng đó là bởi vì nó thực sự đòi hỏi một cách suy nghĩ hoàn toàn khác về mã. Là một tác dụng phụ mặc dù trong hầu hết các trường hợp, bạn có rất nhiều cơ hội để song song miễn phí.

Phụ lục: (Về chỉnh sửa của bạn về cách theo dõi các giá trị cần thay đổi)
Chúng sẽ được lưu trữ trong một cấu trúc dữ liệu bất biến tất nhiên ...

Đây không phải là một 'giải pháp' được đề xuất, nhưng cách dễ nhất để thấy rằng điều này sẽ luôn hoạt động là bạn có thể lưu trữ các giá trị bất biến này vào một bản đồ (từ điển / hashtable) như cấu trúc, được khóa bởi 'tên biến'.

Rõ ràng trong các giải pháp thực tế, bạn sẽ sử dụng một cách tiếp cận lành mạnh hơn, nhưng điều này cho thấy trường hợp xấu nhất nếu không có gì khác bạn có thể 'mô phỏng' trạng thái có thể thay đổi với bản đồ mà bạn mang theo qua cây cầu nguyện của mình.


2
OK, tôi đã thay đổi tiêu đề. Câu trả lời của bạn dường như dẫn đến một vấn đề thậm chí còn tồi tệ hơn. Nếu tôi phải tạo lại mọi đối tượng mỗi khi có gì đó ở trạng thái thay đổi, tôi sẽ dành toàn bộ thời gian CPU của mình để làm gì ngoài việc xây dựng các đối tượng. Tôi đang suy nghĩ về lập trình trò chơi ở đây, nơi bạn có rất nhiều thứ di chuyển trên màn hình (và ngoài màn hình) cùng một lúc, cần phải có khả năng tương tác với nhau. Toàn bộ động cơ có một tốc độ khung hình: mọi thứ bạn sẽ làm, bạn phải làm trong X số giây. Chắc chắn có cách nào tốt hơn là liên tục tái chế toàn bộ đồ vật?
Mason Wheeler

4
Cái hay của nó là sự bất biến nằm ở ngôn ngữ chứ không phải ở cách thực hiện. Với một vài thủ thuật, bạn có thể có trạng thái bất biến trong ngôn ngữ trong khi thực tế thực tế đang thay đổi trạng thái tại chỗ. Xem ví dụ đơn nguyên ST của Haskell.
CesarB

4
@Mason: Điểm quan trọng là trình biên dịch có thể quyết định tốt hơn nơi nào (luồng) an toàn để thay đổi trạng thái tại chỗ hơn bạn có thể.
jerryjvl

Tôi nghĩ đối với các trò chơi, bạn nên tránh bất biến cho bất kỳ phần nào mà tốc độ không thành vấn đề. Mặc dù một ngôn ngữ bất biến có thể tối ưu hóa cho bạn, nhưng không có gì sẽ nhanh hơn việc sửa đổi bộ nhớ mà CPU đang làm nhanh. Và vì vậy, nếu nó xuất hiện có 10 hoặc 20 nơi mà bạn cần bắt buộc, tôi nghĩ bạn chỉ nên tránh hoàn toàn bất biến trừ khi bạn có thể mô đun hóa nó cho các khu vực rất tách biệt như menu trò chơi. Và logic trò chơi nói riêng có thể là một nơi tốt để sử dụng bất biến bởi vì tôi cảm thấy thật tuyệt khi thực hiện mô hình hóa phức tạp của các hệ thống thuần túy như quy tắc kinh doanh.
Truyền thuyết Bước sóng

@LegendLpm bạn đang mâu thuẫn với chính mình.
Ixx

18

Tôi nghĩ rằng có một sự hiểu lầm nhỏ. Chương trình chức năng thuần túy có nhà nước. Sự khác biệt là cách trạng thái đó được mô hình hóa. Trong lập trình hàm thuần túy, trạng thái được thao tác bởi các hàm lấy một số trạng thái và trả về trạng thái tiếp theo. Trình tự thông qua các trạng thái sau đó đạt được bằng cách chuyển trạng thái thông qua một chuỗi các hàm thuần túy.

Ngay cả trạng thái đột biến toàn cầu cũng có thể được mô hình hóa theo cách này. Ví dụ, trong Haskell, một chương trình là một chức năng từ Thế giới đến Thế giới. Đó là, bạn vượt qua trong toàn bộ vũ trụ , và chương trình trả lại một vũ trụ mới. Tuy nhiên, trong thực tế, bạn chỉ cần vượt qua các phần của vũ trụ mà chương trình của bạn thực sự quan tâm. Và các chương trình thực sự trả về một chuỗi các hành động đóng vai trò là hướng dẫn cho môi trường hoạt động mà chương trình chạy.

Bạn muốn thấy điều này được giải thích về mặt lập trình mệnh lệnh. OK, chúng ta hãy xem xét một số chương trình mệnh lệnh thực sự đơn giản trong một ngôn ngữ chức năng.

Xem xét mã này:

int x = 1;
int y = x + 1;
x = x + y;
return x;

Mã mệnh lệnh khá chuẩn bog. Không làm bất cứ điều gì thú vị, nhưng đó là OK để minh họa. Tôi nghĩ bạn sẽ đồng ý rằng có nhà nước liên quan ở đây. Giá trị của biến x thay đổi theo thời gian. Bây giờ, hãy thay đổi ký hiệu một chút bằng cách phát minh ra một cú pháp mới:

let x = 1 in
let y = x + 1 in
let z = x + y in z 

Đặt dấu ngoặc đơn để làm cho nó rõ ràng hơn điều này có nghĩa là gì:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

Vì vậy, bạn thấy, trạng thái được mô hình hóa bằng một chuỗi các biểu thức thuần túy liên kết các biến miễn phí của các biểu thức sau.

Bạn sẽ thấy rằng mô hình này có thể mô hình hóa bất kỳ loại trạng thái nào, thậm chí IO.


Đó có phải là một loại giống như một Monad?
CMCDragonkai

Bạn có xem xét điều này không: A là khai báo ở cấp 1 B là khai báo ở cấp 2, nó coi A là mệnh lệnh. C là khai báo ở cấp 3, nó coi B là mệnh lệnh. Khi chúng ta tăng lớp trừu tượng, nó luôn coi các ngôn ngữ thấp hơn trên lớp trừu tượng là bắt buộc hơn chính nó.
CMCDragonkai

14

Đây là cách bạn viết mã mà không có trạng thái có thể thay đổi : thay vì đặt trạng thái thay đổi thành các biến có thể thay đổi, bạn đặt nó vào các tham số của hàm. Và thay vì viết các vòng lặp, bạn viết các hàm đệ quy. Vì vậy, ví dụ mã bắt buộc này:

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

trở thành mã chức năng này (cú pháp giống như lược đồ):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

hoặc mã Haskellish này

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

Đối với lý do tại sao các lập trình viên chức năng thích làm điều này (mà bạn không yêu cầu), càng nhiều phần chương trình của bạn không trạng thái, càng có nhiều cách để đặt các phần lại với nhau mà không có bất kỳ điều gì bị phá vỡ . Sức mạnh của mô hình không trạng thái không nằm ở trạng thái không trạng thái (hoặc độ tinh khiết) mỗi se , mà khả năng nó mang lại cho bạn để viết các chức năng mạnh mẽ, có thể tái sử dụng và kết hợp chúng.

Bạn có thể tìm thấy một hướng dẫn tốt với rất nhiều ví dụ trong bài viết Tại sao các vấn đề lập trình chức năng của John Hughes .


13

Đó chỉ là những cách khác nhau để làm điều tương tự.

Hãy xem xét một ví dụ đơn giản như thêm các số 3, 5 và 10. Hãy tưởng tượng suy nghĩ về việc làm điều đó bằng cách trước tiên thay đổi giá trị của 3 bằng cách thêm 5 vào số đó, sau đó thêm 10 vào "3", sau đó xuất giá trị hiện tại của " 3 "(18). Điều này có vẻ vô lý một cách rõ ràng, nhưng về bản chất là cách mà lập trình mệnh lệnh dựa trên nhà nước thường được thực hiện. Thật vậy, bạn có thể có nhiều "3" khác nhau có giá trị 3, nhưng khác nhau. Tất cả điều này có vẻ kỳ lạ, bởi vì chúng tôi đã ăn sâu với ý tưởng, khá vô cùng hợp lý, rằng những con số là bất biến.

Bây giờ hãy nghĩ về việc thêm 3, 5 và 10 khi bạn lấy các giá trị là bất biến. Bạn thêm 3 và 5 để tạo ra một giá trị khác, 8, sau đó bạn thêm 10 vào giá trị đó để tạo ra một giá trị khác, 18.

Đây là những cách tương đương để làm điều tương tự. Tất cả các thông tin cần thiết tồn tại trong cả hai phương pháp, nhưng ở các dạng khác nhau. Trong một thông tin tồn tại dưới dạng trạng thái và trong các quy tắc để thay đổi trạng thái. Mặt khác, thông tin tồn tại trong dữ liệu bất biến và định nghĩa chức năng.


10

Tôi đến trễ để thảo luận, nhưng tôi muốn thêm một vài điểm cho những người đang vật lộn với lập trình chức năng.

  1. Các ngôn ngữ chức năng duy trì các cập nhật trạng thái chính xác giống như các ngôn ngữ bắt buộc nhưng chúng thực hiện điều đó bằng cách chuyển trạng thái cập nhật cho các lệnh gọi hàm tiếp theo . Dưới đây là một ví dụ rất đơn giản về việc đi xuống một dòng số. Tiểu bang của bạn là vị trí hiện tại của bạn.

Đầu tiên là cách bắt buộc (trong mã giả)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

Bây giờ cách chức năng (trong mã giả). Tôi đang dựa rất nhiều vào toán tử ternary vì tôi muốn mọi người từ các nền tảng bắt buộc thực sự có thể đọc được mã này. Vì vậy, nếu bạn không sử dụng toán tử ternary nhiều (tôi luôn tránh nó trong những ngày bắt buộc) thì đây là cách nó hoạt động.

predicate ? if-true-expression : if-false-expression

Bạn có thể xâu chuỗi biểu thức ternary bằng cách đặt biểu thức ternary mới thay cho biểu thức false

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

Vì vậy, trong tâm trí đó, đây là phiên bản chức năng.

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

Đây là một ví dụ tầm thường. Nếu điều này làm mọi người di chuyển trong thế giới trò chơi, bạn sẽ phải đưa ra các tác dụng phụ như vẽ vị trí hiện tại của đối tượng trên màn hình và đưa ra một chút độ trễ trong mỗi cuộc gọi dựa trên tốc độ di chuyển của đối tượng. Nhưng bạn vẫn sẽ không cần trạng thái có thể thay đổi.

  1. Bài học là các ngôn ngữ chức năng "biến đổi" trạng thái bằng cách gọi hàm với các tham số khác nhau. Rõ ràng điều này không thực sự làm thay đổi bất kỳ biến số nào, nhưng đó là cách bạn có được hiệu ứng tương tự. Điều này có nghĩa là bạn sẽ phải làm quen với việc suy nghĩ đệ quy nếu bạn muốn lập trình chức năng.

  2. Học cách suy nghĩ đệ quy không khó, nhưng nó cần cả thực hành và bộ công cụ. Phần nhỏ đó trong cuốn sách "Tìm hiểu Java" nơi họ đã sử dụng đệ quy để tính giai thừa không cắt nó. Bạn cần một bộ công cụ gồm các kỹ năng như tạo các quy trình lặp ra khỏi đệ quy (đây là lý do tại sao đệ quy đuôi là cần thiết cho ngôn ngữ chức năng), tiếp tục, bất biến, v.v. Bạn sẽ không lập trình OO mà không học về sửa đổi truy cập, giao diện, v.v. để lập trình chức năng.

Đề nghị của tôi là thực hiện Little Schemer (lưu ý rằng tôi nói "làm" chứ không phải "đọc") và sau đó thực hiện tất cả các bài tập trong SICP. Khi bạn hoàn thành, bạn sẽ có một bộ não khác so với khi bạn bắt đầu.


8

Trên thực tế khá dễ dàng để có một cái gì đó trông giống như trạng thái có thể thay đổi ngay cả trong các ngôn ngữ không có trạng thái có thể thay đổi.

Hãy xem xét một chức năng với loại s -> (a, s). Dịch từ cú pháp Haskell, nó có nghĩa là một hàm lấy một tham số loại " s" và trả về một cặp giá trị, loại " a" và " s". Nếu slà loại trạng thái của chúng ta, hàm này sẽ lấy một trạng thái và trả về trạng thái mới và có thể là một giá trị (bạn luôn có thể trả về "đơn vị" aka (), tương đương với " void" trong C / C ++, là " a" kiểu). Nếu bạn xâu chuỗi một số cuộc gọi của các hàm với các kiểu như thế này (nhận trạng thái được trả về từ một chức năng và chuyển nó sang chức năng tiếp theo), bạn có trạng thái "có thể thay đổi" (thực tế bạn đang ở trong mỗi chức năng tạo trạng thái mới và từ bỏ trạng thái cũ ).

Có thể dễ hiểu hơn nếu bạn tưởng tượng trạng thái có thể thay đổi là "không gian" nơi chương trình của bạn đang thực thi và sau đó nghĩ về chiều thời gian. Tại t1 tức thì, "không gian" ở trong một điều kiện nhất định (ví dụ: một số vị trí bộ nhớ có giá trị 5). Tại một t2 tức thì sau đó, nó ở một điều kiện khác (ví dụ: vị trí bộ nhớ hiện có giá trị 10). Mỗi "lát" thời gian này là một trạng thái và nó là bất biến (bạn không thể quay ngược thời gian để thay đổi chúng). Vì vậy, từ quan điểm này, bạn đã đi từ không thời gian đầy đủ với một mũi tên thời gian (trạng thái có thể thay đổi của bạn) sang một tập hợp các lát không thời gian (một số trạng thái bất biến) và chương trình của bạn chỉ coi mỗi lát là một giá trị và tính toán từng lát trong số họ như là một chức năng được áp dụng cho trước đó.

OK, có lẽ điều đó không dễ hiểu hơn :-)

Có vẻ như không hợp lý khi đại diện rõ ràng cho toàn bộ trạng thái chương trình là một giá trị, chỉ được tạo để loại bỏ ngay sau đó (ngay sau khi một trạng thái mới được tạo). Đối với một số thuật toán có thể là tự nhiên, nhưng khi nó không có, có một mẹo khác. Thay vì trạng thái thực, bạn có thể sử dụng trạng thái giả không khác gì một điểm đánh dấu (hãy gọi loại trạng thái giả này State#). Trạng thái giả mạo này tồn tại từ quan điểm của ngôn ngữ và được truyền giống như bất kỳ giá trị nào khác, nhưng trình biên dịch hoàn toàn bỏ qua nó khi tạo mã máy. Nó chỉ phục vụ để đánh dấu trình tự thực hiện.

Ví dụ, giả sử trình biên dịch cung cấp cho chúng ta các hàm sau:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

Dịch từ các khai báo giống như Haskell này, readRefnhận được một cái gì đó giống với con trỏ hoặc tay cầm thành giá trị của loại " a" và trạng thái giả và trả về giá trị của loại " a" được chỉ ra bởi tham số đầu tiên và trạng thái giả mới. writeReflà tương tự, nhưng thay đổi giá trị chỉ vào thay thế.

Nếu bạn gọi readRefvà sau đó chuyển qua trạng thái giả được trả về writeRef(có thể với các lệnh gọi khác đến các hàm không liên quan ở giữa; các giá trị trạng thái này tạo ra một "chuỗi" các lệnh gọi hàm), nó sẽ trả về giá trị được ghi. Bạn có thể gọi writeReflại với cùng một con trỏ / tay cầm và nó sẽ ghi vào cùng một vị trí bộ nhớ - nhưng, về mặt khái niệm, nó đang trả về trạng thái (giả) mới, trạng thái (giả) vẫn không thể thay đổi (một trạng thái mới đã được "tạo "). Trình biên dịch sẽ gọi các hàm theo thứ tự nó sẽ phải gọi chúng nếu có một biến trạng thái thực phải được tính toán, nhưng trạng thái duy nhất có trạng thái đầy đủ (có thể thay đổi) của phần cứng thực.

(Những người biết Haskell sẽ nhận thấy tôi đã đơn giản hóa mọi thứ rất nhiều và có nhiều chi tiết quan trọng. Đối với những người muốn xem thêm chi tiết, hãy xem Control.Monad.Statetừ mtl, và tại ST sIO(hay ST RealWorld) các đơn nguyên.)

Bạn có thể tự hỏi tại sao làm điều đó theo cách vòng vo như vậy (thay vì chỉ đơn giản là có trạng thái có thể thay đổi trong ngôn ngữ). Ưu điểm thực sự là bạn đã reified trạng thái của chương trình của bạn. Những gì trước đây là ẩn (trạng thái chương trình của bạn là toàn cầu, cho phép những thứ như hành động ở khoảng cách xa ) hiện rõ ràng. Các chức năng không nhận và trả lại trạng thái có thể sửa đổi nó hoặc bị ảnh hưởng bởi nó; chúng là "tinh khiết". Thậm chí tốt hơn, bạn có thể có các luồng trạng thái riêng biệt và với một chút phép thuật loại, chúng có thể được sử dụng để nhúng một phép tính bắt buộc trong một phép tính thuần túy, mà không làm cho nó không trong sạch ( STđơn nguyên trong Haskell là thứ thường được sử dụng cho thủ thuật này; các State#tôi đã đề cập ở trên là trong thực tế GHC của State# s, được sử dụng bởi việc thực hiện của STIO đơn nguyên).


7

Lập trình chức năng tránh trạng thái và nhấn mạnhchức năng. Không bao giờ có bất kỳ thứ gì như không có trạng thái, mặc dù trạng thái đó thực sự có thể là thứ gì đó bất biến hoặc bị cuốn vào kiến ​​trúc của những gì bạn đang làm việc. Hãy xem xét sự khác biệt giữa một máy chủ web tĩnh chỉ tải các tệp ra khỏi hệ thống tệp so với chương trình thực hiện khối Rubik. Cái trước sẽ được thực hiện theo các chức năng được thiết kế để biến một yêu cầu thành một yêu cầu đường dẫn tệp thành một phản hồi từ nội dung của tệp đó. Hầu như không cần trạng thái nào ngoài một chút cấu hình ('trạng thái' của hệ thống tệp thực sự nằm ngoài phạm vi của chương trình. Chương trình hoạt động theo cùng một cách bất kể các tệp đang ở trạng thái nào). Tuy nhiên, sau này, bạn cần mô hình hóa khối và chương trình của bạn thực hiện cách các hoạt động trên khối đó thay đổi trạng thái của nó.


Khi tôi chống chức năng nhiều hơn, tôi tự hỏi làm thế nào nó có thể tốt khi một thứ như ổ cứng có thể thay đổi được. Tất cả các lớp c # của tôi đều có trạng thái có thể thay đổi và rất có thể mô phỏng logic một ổ đĩa cứng hoặc bất kỳ thiết bị nào khác. Trong khi đó với chức năng có sự không phù hợp giữa các mô hình và các máy thực tế mà chúng đang tạo mô hình. Sau khi đào sâu hơn vào chức năng, tôi đã nhận ra những lợi ích bạn có thể có thể vượt xa vấn đề đó một chút công bằng. Và nếu về mặt vật lý có thể phát minh ra một ổ cứng tạo ra một bản sao của chính nó thì nó thực sự hữu ích (giống như nhật ký đã làm).
Truyền thuyết Bước sóng

5

Ngoài những câu trả lời tuyệt vời mà những người khác đang đưa ra, hãy nghĩ về các lớp IntegerStringtrong Java. Thể hiện của các lớp này là bất biến, nhưng điều đó không làm cho các lớp trở nên vô dụng chỉ vì các thể hiện của chúng không thể thay đổi. Sự bất biến mang đến cho bạn sự an toàn. Bạn biết nếu bạn sử dụng một thể hiện String hoặc Integer làm khóa cho a Map, khóa không thể thay đổi. So sánh điều này với Datelớp trong Java:

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

Bạn đã âm thầm thay đổi một chìa khóa trong bản đồ của bạn! Làm việc với các đối tượng bất biến, như trong Lập trình hàm, sạch sẽ hơn rất nhiều. Lý do dễ dàng hơn về những gì tác dụng phụ xảy ra - không có gì! Điều này có nghĩa là nó dễ dàng hơn cho lập trình viên, và cũng dễ dàng hơn cho trình tối ưu hóa.


2
Tôi hiểu điều đó, nhưng nó không trả lời câu hỏi của tôi. Hãy nhớ rằng một chương trình máy tính là một mô hình của một số sự kiện hoặc quá trình trong thế giới thực, nếu bạn không thể thay đổi giá trị của mình, thì làm thế nào để bạn mô hình hóa một cái gì đó thay đổi?
Mason Wheeler

Chà, bạn chắc chắn có thể làm những điều hữu ích với các lớp Integer và String. Nó không giống như sự bất biến của họ có nghĩa là bạn không thể có trạng thái đột biến.
Eddie

@Mason Wheeler - Bằng cách hiểu rằng một vật và trạng thái của nó là hai "thứ" khác nhau. Những gì pacman không thay đổi từ thời điểm A sang thời gian B. Trường hợp pacman không thay đổi. Khi bạn chuyển từ thời điểm A sang thời gian B, bạn sẽ nhận được một sự kết hợp mới của trạng thái pacman + ... đó là cùng một pacman, trạng thái khác nhau. Không thay đổi trạng thái ... trạng thái khác nhau.
RHSeeger

4

Đối với các ứng dụng tương tác cao như trò chơi, Lập trình phản ứng chức năng là người bạn của bạn: nếu bạn có thể hình thành các thuộc tính của thế giới trò chơi của mình dưới dạng các giá trị thay đổi theo thời gian (và / hoặc luồng sự kiện), bạn đã sẵn sàng! Các công thức này đôi khi thậm chí còn tự nhiên và tiết lộ hơn so với việc thay đổi trạng thái, ví dụ đối với một quả bóng chuyển động, bạn có thể trực tiếp sử dụng luật nổi tiếng x = v * t . Và những gì tốt hơn, các quy tắc của trò chơi được viết theo cách như vậy sáng tác tốt hơn so với trừu tượng hướng đối tượng. Ví dụ, trong trường hợp này, tốc độ của quả bóng cũng có thể là giá trị thay đổi theo thời gian, phụ thuộc vào luồng sự kiện bao gồm các va chạm của quả bóng. Để xem xét thiết kế cụ thể hơn, xem Làm trò chơi trong Elm .


4

3

Đó là cách FORTRAN sẽ hoạt động mà không có khối GIAO DỊCH: Bạn sẽ viết các phương thức có các giá trị bạn truyền vào và các biến cục bộ. Đó là nó.

Lập trình hướng đối tượng mang lại cho chúng ta trạng thái và hành vi cùng nhau, nhưng đó là một ý tưởng mới khi lần đầu tiên tôi bắt gặp nó từ C ++ vào năm 1994.

Geez, tôi là một lập trình viên chức năng khi tôi là một kỹ sư cơ khí và tôi không biết điều đó!


2
Tôi không đồng ý rằng đây là thứ bạn có thể ghim trên OO. Các ngôn ngữ trước OO khuyến khích trạng thái ghép và thuật toán. OO chỉ cung cấp một cách tốt hơn để quản lý nó.
Jason Baker

"Khuyến khích" - có lẽ. OO làm cho nó trở thành một phần rõ ràng của ngôn ngữ. Bạn có thể đóng gói và ẩn thông tin trong C, nhưng tôi nói rằng các ngôn ngữ OO làm cho nó dễ dàng hơn nhiều.
duffymo

2

Hãy nhớ rằng: ngôn ngữ chức năng là Turing hoàn chỉnh. Do đó, bất kỳ tác vụ hữu ích nào bạn sẽ thực hiện bằng ngôn ngữ bắt buộc đều có thể được thực hiện bằng ngôn ngữ chức năng. Tuy nhiên, vào cuối ngày, tôi nghĩ có điều gì đó để nói về cách tiếp cận lai. Các ngôn ngữ như F # và Clojure (và tôi chắc chắn những người khác) khuyến khích thiết kế không trạng thái, nhưng cho phép khả năng biến đổi khi cần thiết.


Chỉ vì hai ngôn ngữ Turing hoàn thành không có nghĩa là chúng có thể thực hiện cùng một nhiệm vụ. Điều đó có nghĩa là họ có thể thực hiện cùng một tính toán. Brainfuck đã hoàn tất Turing, nhưng tôi khá chắc chắn rằng nó không thể giao tiếp qua ngăn xếp TCP.
RHSeeger

2
Chắc chắn nó có thể. Với cùng quyền truy cập vào phần cứng như C, nó có thể. Điều đó không có nghĩa là nó sẽ thực tế, nhưng khả năng là có.
Jason Baker

2

Bạn không thể có một ngôn ngữ chức năng thuần túy hữu ích. Sẽ luôn có một mức độ biến đổi mà bạn phải đối phó, IO là một ví dụ.

Hãy nghĩ về các ngôn ngữ chức năng như là một công cụ khác mà bạn sử dụng. Nó tốt cho những thứ nhất định, nhưng không phải những thứ khác. Ví dụ về trò chơi mà bạn đưa ra có thể không phải là cách tốt nhất để sử dụng ngôn ngữ chức năng, ít nhất màn hình sẽ có trạng thái có thể thay đổi mà bạn không thể làm gì với FP. Cách bạn nghĩ về vấn đề và loại vấn đề bạn giải quyết với FP sẽ khác với những vấn đề bạn đã quen với lập trình mệnh lệnh.



-3

Điều này rất đơn giản. Bạn có thể sử dụng bao nhiêu biến số bạn muốn trong lập trình chức năng ... nhưng chỉ khi chúng là các biến cục bộ (chứa bên trong các hàm). Vì vậy, chỉ cần bọc mã của bạn trong các hàm, chuyển các giá trị qua lại giữa các hàm đó (như các tham số đã truyền và các giá trị được trả về) ... và đó là tất cả những gì có trong đó!

Đây là một ví dụ:

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

John, đây là ngôn ngữ gì?
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.