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ả.