Cách viết các chương trình Java hữu ích mà không cần sử dụng các biến có thể thay đổi


12

Tôi đã đọc một bài viết về lập trình chức năng nơi nhà văn tuyên bố

(take 25 (squares-of (integers)))

Lưu ý rằng nó không có biến. Thật vậy, nó không có gì nhiều hơn ba chức năng và một hằng số. Hãy thử viết bình phương số nguyên trong Java mà không sử dụng biến. Ồ, có lẽ có một cách để làm điều đó, nhưng nó chắc chắn không phải là tự nhiên, và nó sẽ không đọc độc đáo như chương trình của tôi ở trên.

Có thể đạt được điều này trong Java? Giả sử bạn được yêu cầu in bình phương của 15 số nguyên đầu tiên, bạn có thể viết một vòng lặp for hoặc while mà không sử dụng biến không?

Thông báo mod

Câu hỏi này không phải là một cuộc thi golf mã. Chúng tôi đang tìm kiếm câu trả lời giải thích các khái niệm liên quan (lý tưởng mà không lặp lại câu trả lời trước đó), và không chỉ cho một đoạn mã khác.


19
Ví dụ chức năng của bạn không sử dụng các biến ở bên trong, nhưng ngôn ngữ thực hiện tất cả các biến phía sau hậu trường. Bạn đã ủy thác hiệu quả những phần khó chịu cho người mà bạn tin rằng đã thực hiện đúng.
Blrfl

12
@Blrfl: Đối số "đằng sau hậu trường" sẽ giết chết tất cả các cuộc tranh luận dựa trên ngôn ngữ, vì mọi đoạn mã cuối cùng được dịch sang mã máy x86. Mã x86 không hướng đối tượng, không thủ tục, không chức năng, không phải bất cứ điều gì, nhưng các danh mục này là các thẻ có giá trị cho các ngôn ngữ lập trình. Nhìn vào ngôn ngữ, không phải việc thực hiện.
thiton

10
@thiton Không đồng ý. Điều Blrfl đang nói là các hàm đó có thể sử dụng các biến được viết bằng cùng một ngôn ngữ lập trình . Không cần phải đi cấp thấp ở đây. Đoạn mã chỉ đơn thuần là sử dụng các chức năng thư viện. Bạn có thể dễ dàng viết cùng một mã trong Java, xem: squaresOf(integers()).take(25)(viết các hàm đó được để lại như một bài tập cho người đọc; khó khăn nằm ở tập vô hạn integers(), nhưng đó là một vấn đề đối với Java vì đánh giá háo hức của nó, không liên quan gì các biến)
Andres F.

6
Câu nói đó thật khó hiểu và gây hiểu lầm, không có phép thuật ở đó, chỉ là cú pháp cú pháp .
yannis

2
@thiton Tôi khuyên bạn nên tìm hiểu thêm về các ngôn ngữ FP, tuy nhiên, đoạn mã không hỗ trợ (hoặc từ chối) khẳng định rằng "các biến" không cần thiết (theo đó tôi giả sử bạn có nghĩa là "biến có thể thay đổi", bởi vì khác loại là phổ biến trong FP). Đoạn mã chỉ hiển thị các chức năng thư viện cũng có thể đã được triển khai trong Java, loại bỏ các vấn đề lười biếng / háo hức không chính thức ở đây.
Andres F.

Câu trả lời:


31

Có thể thực hiện một ví dụ như vậy trong Java mà không cần sử dụng các bản cập nhật phá hoại không? Đúng. Tuy nhiên, như @Thiton và bài báo đã đề cập, nó sẽ xấu (tùy theo khẩu vị của một người). Một cách là sử dụng đệ quy; đây là một ví dụ Haskell có chức năng tương tự:

unfoldr      :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b  =
  case f b of
   Just (a,new_b) -> a : unfoldr f new_b
   Nothing        -> []  

Lưu ý 1) thiếu đột biến, 2) sử dụng đệ quy và 3) thiếu các vòng lặp. Điểm cuối cùng rất quan trọng - các ngôn ngữ chức năng không cần các cấu trúc lặp được xây dựng trong ngôn ngữ, vì đệ quy có thể được sử dụng cho hầu hết các trường hợp (tất cả?) Trong đó các vòng lặp được sử dụng trong Java. Đây là một loạt các bài báo nổi tiếng cho thấy các cuộc gọi chức năng biểu cảm đáng kinh ngạc như thế nào.


Tôi thấy bài viết không hài lòng và muốn đưa ra một vài điểm bổ sung:

Bài viết đó là một lời giải thích rất nghèo nàn và khó hiểu về lập trình chức năng và lợi ích của nó. Tôi rất muốn giới thiệu các nguồn khác để tìm hiểu về lập trình chức năng.

Phần khó hiểu nhất về bài viết này là nó không đề cập đến việc có hai cách sử dụng cho các câu lệnh gán trong Java (và hầu hết các ngôn ngữ chính khác):

  1. ràng buộc một giá trị với một tên: final int MAX_SIZE = 100;

  2. cập nhật phá hoại: int a = 3; a += 1; a++;

Lập trình chức năng tránh cái thứ hai, nhưng bao gồm cái thứ nhất (ví dụ: let-expressions, tham số chức năng, defineitions cấp cao nhất ) . Đây là một điểm rất quan trọng để nắm bắt, bởi vì nếu không bài viết chỉ có vẻ ngớ ngẩn và có thể để lại cho bạn tự hỏi, là gì take, squares-ofintegersnếu không biến?

Ngoài ra, ví dụ là vô nghĩa. Nó không hiển thị các triển khai của take, squares-ofhoặc integers. Đối với tất cả những gì chúng ta biết, chúng được thực hiện bằng cách sử dụng các biến có thể thay đổi. Như @Martin đã nói, bạn có thể viết ví dụ này một cách tầm thường trong Java.

Một lần nữa, tôi khuyên bạn nên tránh bài viết này và những người khác thích nó nếu bạn thực sự muốn tìm hiểu về lập trình chức năng. Nó dường như được viết nhiều hơn với mục tiêu gây sốc và xúc phạm hơn là dạy các khái niệm và nguyên tắc cơ bản. Thay vào đó, tại sao không kiểm tra một trong những bài báo yêu thích mọi thời đại của tôi , bởi John Hughes. Hughes cố gắng giải quyết một số vấn đề tương tự mà bài báo đã đề cập (mặc dù Hughes không nói về sự tương tranh / song song); đây là một lời trêu ghẹo:

Bài viết này là một nỗ lực để chứng minh cho cộng đồng lớn hơn của các lập trình viên (không chức năng) ý nghĩa của lập trình chức năng, và cũng để giúp các lập trình viên chức năng khai thác tối đa lợi thế của mình bằng cách làm rõ những lợi thế đó là gì.

[...]

Chúng ta sẽ tranh luận trong phần còn lại của bài viết này rằng các ngôn ngữ chức năng cung cấp hai loại keo mới, rất quan trọng. Chúng tôi sẽ đưa ra một số ví dụ về các chương trình có thể được mô đun hóa theo những cách mới và do đó có thể đơn giản hóa. Đây là chìa khóa cho sức mạnh của lập trình chức năng - nó cho phép cải thiện mô đun hóa. Đó cũng là mục tiêu mà các lập trình viên chức năng phải phấn đấu - các mô-đun nhỏ hơn và đơn giản hơn và tổng quát hơn, được dán cùng với các keo mới mà chúng tôi sẽ mô tả.


10
+1 cho "Tôi khuyên bạn nên tránh bài viết này và những người khác thích nó nếu bạn thực sự muốn tìm hiểu về lập trình chức năng. Nó dường như được viết nhiều hơn với mục tiêu gây sốc và xúc phạm hơn là dạy các khái niệm và nguyên tắc cơ bản."

3
Một nửa lý do mọi người không làm FP là vì họ không nghe / học bất cứ điều gì về nó ở uni, và nửa còn lại là vì khi họ nhìn vào đó, họ tìm thấy những bài báo khiến họ không hiểu và nghĩ tất cả chỉ vì sự huyền ảo chơi về chứ không phải là một cách tiếp cận suy nghĩ hợp lý với lợi ích. +1 để cung cấp nguồn thông tin tốt hơn
Jimmy Hoffa

3
Đặt câu trả lời của bạn cho câu hỏi ở đầu tuyệt đối nếu bạn muốn câu hỏi trực tiếp hơn và có thể câu hỏi này sẽ được mở sau đó (với câu trả lời trực tiếp tập trung vào câu hỏi)
Jimmy Hoffa

2
Xin lỗi cho nitpick, nhưng tôi không hiểu tại sao bạn lại chọn mã haskell này. Tôi đã đọc LYAH và ví dụ của bạn rất khó để tôi mò mẫm . Tôi cũng không thấy mối quan hệ với câu hỏi ban đầu. Tại sao bạn không sử dụng take 25 (map (^2) [1..])làm ví dụ?
Daniel Kaplan

2
@tieTYT câu hỏi hay - cảm ơn bạn đã chỉ ra điều này. Lý do tôi sử dụng ví dụ đó là vì nó cho thấy cách tạo danh sách các số bằng cách sử dụng đệ quy và tránh các biến có thể thay đổi. Ý định của tôi là để OP thấy mã đó và suy nghĩ về cách làm một cái gì đó tương tự trong Java. Để giải quyết đoạn mã của bạn, là [1..]gì? Đó là một tính năng thú vị được tích hợp trong Haskell, nhưng không minh họa các khái niệm đằng sau việc tạo ra một danh sách như vậy. Tôi chắc chắn các trường hợp của Enumlớp (mà cú pháp yêu cầu) cũng hữu ích, nhưng quá lười để tìm thấy chúng. Như vậy , unfoldr. :)

27

Bạn sẽ không. Các biến là cốt lõi của lập trình mệnh lệnh, và nếu bạn cố gắng lập trình một cách bắt buộc mà không sử dụng các biến, bạn chỉ khiến mọi người đau đớn. Trong các mô hình lập trình khác nhau, các phong cách là khác nhau và các khái niệm khác nhau tạo thành cơ sở của bạn. Một biến trong Java, khi được sử dụng tốt với phạm vi nhỏ, không có gì xấu. Yêu cầu chương trình Java không có biến cũng giống như yêu cầu chương trình Haskell không có chức năng, vì vậy bạn không yêu cầu chương trình đó và bạn không để mình bị lừa xem chương trình cấp bách là kém hơn vì nó sử dụng biến.

Vì vậy, cách Java sẽ là:

for (int i = 1; i <= 25; ++i) {
    System.out.println(i*i);
}

và đừng để bản thân bị lừa viết nó theo bất kỳ cách phức tạp nào do sự ghét bỏ các biến.


5
"Hận thù của các biến"? Ooookay ... Bạn đã đọc gì về Lập trình hàm? Những ngôn ngữ bạn đã thử? Hướng dẫn nào?
Andres F.

8
@AndresF.: Hơn hai năm học tập tại Haskell. Tôi không nói rằng FP là xấu. Tuy nhiên, có nhiều xu hướng trong nhiều cuộc thảo luận về FP-vs-IP (chẳng hạn như bài viết được liên kết) lên án việc sử dụng các thực thể có tên được gán lại (biến AKA) và lên án mà không có lý do hoặc dữ liệu chính đáng. Sự lên án vô lý là sự thù hận trong cuốn sách của tôi. Và hận thù làm cho mã thực sự xấu.
thiton

10
"Thù hận biến" là nguyên nhân đơn giản hóa en.wikipedia.org/wiki/Fallacy_of_the_single_cause có rất nhiều lợi ích cho lập trình không quốc tịch mà thậm chí có thể có trong Java, mặc dù tôi đồng ý với câu trả lời của bạn mà trong Java chi phí sẽ là quá cao về độ phức tạp để chương trình và không thành ngữ. Tôi vẫn sẽ không đi xung quanh vẫy tay với ý tưởng rằng lập trình phi trạng thái là tốt và trạng thái là xấu như một số phản ứng cảm xúc chứ không phải là một lý do, mọi người nghĩ ra lập trường do kinh nghiệm.
Jimmy Hoffa

2
Phù hợp với những gì @JimmyHoffa nói, tôi sẽ giới thiệu bạn với John Carmack về chủ đề lập trình kiểu chức năng bằng các ngôn ngữ bắt buộc (C ++ trong trường hợp của anh ấy) ( altdevblogaday.com/2012/04/26/feftal-programming-in-c ).
Steven Evers

5
Sự lên án vô lý không phải là sự thù hận, và tránh trạng thái đột biến không phải là không có lý.
Michael Shaw

21

Đơn giản nhất tôi có thể làm với đệ quy là một hàm với một tham số. Nó không phải là Java-esque, nhưng nó hoạt động:

public class squares
{
    public static void main(String[] args)
    {
        squares(15);
    }

    private static void squares(int x)
    {
        if (x>0)
        {
            System.out.println(x*x);
            squares(x-1);
        }
    }
}

3
+1 để cố gắng trả lời câu hỏi bằng một ví dụ Java.
KChaloux

Tôi muốn tải xuống phần này để trình bày kiểu golf (xem thông báo Mod ) nhưng không thể tự nhấn mũi tên xuống vì mã này hoàn toàn khớp với các câu trong câu trả lời yêu thích của tôi : "1) thiếu đột biến, 2) việc sử dụng đệ quy và 3) thiếu các vòng lặp "
gnat

3
@gnat: Câu trả lời này đã được đăng trước thông báo Mod. Tôi đã không đi theo phong cách tuyệt vời, tôi sẽ đơn giản và đáp ứng câu hỏi ban đầu của OP; để minh họa rằng nó có thể làm những việc như vậy trong Java.
Thất vọngWithFormsDesigner

@FrustratedWithFormsDesigner chắc chắn; điều này sẽ không ngăn tôi khỏi DVing (vì bạn phải có khả năng chỉnh sửa để tuân thủ) - đó là trận đấu hoàn hảo nổi bật đã làm nên điều kỳ diệu. Làm tốt lắm, thực sự hoàn thành tốt, khá giáo dục - cảm ơn bạn
gnat

16

Trong ví dụ chức năng của bạn, chúng tôi không thấy cách thức squares-oftakechức năng được thực hiện. Tôi không phải là chuyên gia Java, nhưng tôi khá chắc chắn rằng chúng ta có thể viết các hàm đó để kích hoạt một câu lệnh như thế này ...

squares_of(integers).take(25);

Điều đó không quá khác biệt.


6
Nitpick: squares-ofkhông phải là một tên hợp lệ trong Java ( squares_ofmặc dù). Nhưng mặt khác, điểm tốt cho thấy ví dụ của bài viết là kém.

Tôi nghi ngờ rằng bài viết này integerlười biếng tạo ra các số nguyên và takehàm chọn 25 squared-ofsố integer. Nói tóm lại, bạn nên có một integerchức năng để lười biếng sản xuất số nguyên đến vô cùng.
OnesimusUnbound

Thật là một chút điên rồ khi gọi một cái gì đó giống như (integer)một hàm - một hàm vẫn là một cái gì đó ánh xạ một đối số thành một giá trị. Hóa ra đó (integer)không phải là một chức năng, mà chỉ là một giá trị. Người ta thậm chí có thể đi xa để nói rằng đó integerlà một biến bị ràng buộc với một chuỗi số vô hạn.
Ingo

6

Trong Java, bạn có thể làm điều này (đặc biệt là phần danh sách vô hạn) với các trình vòng lặp. Trong mẫu mã sau đây, số lượng được cung cấp cho hàm Taketạo có thể cao tùy ý.

class Example {
    public static void main(String[] a) {
        Numbers test = new Take(25, new SquaresOf(new Integers()));
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Hoặc với các phương pháp nhà máy có chuỗi:

class Example {
    public static void main(String[] a) {
        Numbers test = Numbers.integers().squares().take(23);
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Trong trường hợp SquaresOf, TakeIntegersmở rộngNumbers

abstract class Numbers implements Iterator<Integer> {
    public static Numbers integers() {
        return new Integers();
    }

    public Numbers squares() {
        return new SquaresOf(this);
    }

    public Numbers take(int c) {
        return new Take(c, this);
    }
    public void remove() {}
}

1
Điều này cho thấy sự vượt trội của mô hình OO so với chức năng; với thiết kế OO phù hợp, bạn có thể bắt chước mô hình chức năng nhưng bạn không thể bắt chước mô hình OO theo kiểu chức năng.
m3th0dman

3
@ m3th0dman: Với thiết kế OO phù hợp, bạn có thể bắt chước một nửa giả định FP, giống như bất kỳ ngôn ngữ nào có chuỗi, danh sách và / hoặc từ điển có thể bắt chước một nửa OO. Sự tương đương Turing của các ngôn ngữ có mục đích chung có nghĩa là đã nỗ lực đủ, bất kỳ ngôn ngữ nào cũng có thể mô phỏng các tính năng của bất kỳ ngôn ngữ nào khác.
cHao

Lưu ý rằng các trình vòng lặp kiểu Java như trong while (test.hasNext()) System.out.println(test.next())sẽ là không có trong FP; iterators vốn đã có thể thay đổi.
cHao

1
@cHao Tôi hầu như không tin rằng đóng gói thực sự hoặc đa hình có thể được bắt chước; còn Java (trong ví dụ này) không thể thực sự bắt chước một ngôn ngữ chức năng vì sự đánh giá háo hức nghiêm ngặt. Tôi cũng tin rằng các trình vòng lặp có thể được viết theo cách đệ quy.
m3th0dman

@ m3th0dman: Đa hình sẽ không khó để bắt chước; thậm chí C và ngôn ngữ lắp ráp có thể làm điều đó. Chỉ cần làm cho phương thức trở thành một trường trong đối tượng hoặc một mô tả lớp / vtable. Và đóng gói trong ý nghĩa ẩn dữ liệu là không cần thiết nghiêm ngặt; một nửa ngôn ngữ ngoài kia không cung cấp nó, khi đối tượng của bạn là bất biến, điều đó không quan trọng bằng việc mọi người có thể nhìn thấy ruột của nó hay không. tất cả những gì cần thiết là gói dữ liệu , mà các trường phương thức đã nói ở trên có thể dễ dàng cung cấp.
cHao

6

Phiên bản ngắn:

Để làm cho kiểu chuyển nhượng đơn hoạt động đáng tin cậy trong Java, bạn cần (1) một số loại cơ sở hạ tầng thân thiện không thay đổi và (2) trình biên dịch hoặc hỗ trợ mức thời gian chạy để loại bỏ cuộc gọi đuôi.

Chúng tôi có thể viết nhiều cơ sở hạ tầng, và chúng tôi có thể sắp xếp mọi thứ để cố gắng tránh lấp đầy ngăn xếp. Nhưng miễn là mỗi cuộc gọi mất một khung stack, sẽ có giới hạn về mức độ đệ quy bạn có thể làm. Giữ iterables của bạn nhỏ và / hoặc lười biếng, và bạn không nên có vấn đề lớn. Ít nhất là hầu hết các vấn đề bạn sẽ gặp phải không yêu cầu trả lại một triệu kết quả cùng một lúc. :)

Cũng lưu ý, vì chương trình phải thực sự ảnh hưởng đến những thay đổi có thể nhìn thấy để có giá trị chạy, bạn không thể biến mọi thứ thành bất biến. Tuy nhiên, bạn có thể giữ cho phần lớn các công cụ của riêng bạn không thay đổi, bằng cách sử dụng một tập hợp nhỏ các biến đổi thiết yếu (ví dụ như luồng) chỉ tại một số điểm chính mà các lựa chọn thay thế sẽ quá khó chịu.


Phiên bản dài:

Nói một cách đơn giản, một chương trình Java hoàn toàn không thể tránh các biến nếu nó muốn làm bất cứ điều gì đáng làm. Bạn có thể chứa chúng, và do đó hạn chế khả năng biến đổi ở một mức độ lớn, nhưng chính thiết kế ngôn ngữ và API - cùng với nhu cầu thay đổi hệ thống cơ bản - khiến cho tính bất biến hoàn toàn không thể thực hiện được.

Java được thiết kế từ đầu như một ngôn ngữ bắt buộc , hướng đối tượng .

  • Các ngôn ngữ bắt buộc hầu như luôn phụ thuộc vào các biến có thể thay đổi của một số loại. Họ có xu hướng thích lặp đi lặp lại hơn đệ quy, ví dụ, và gần như tất cả các cấu trúc lặp - thậm chí while (true)for (;;)! - hoàn toàn phụ thuộc vào một biến ở đâu đó thay đổi từ lần lặp sang lần lặp.
  • Các ngôn ngữ hướng đối tượng hình dung khá nhiều chương trình như một biểu đồ của các đối tượng gửi tin nhắn cho nhau và trong hầu hết các trường hợp, trả lời các tin nhắn đó bằng cách thay đổi một cái gì đó.

Kết quả cuối cùng của những quyết định thiết kế đó là không có các biến có thể thay đổi, Java không có cách nào để thay đổi trạng thái của bất cứ thứ gì - thậm chí là một thứ đơn giản như in "Hello world!" đến màn hình liên quan đến một luồng đầu ra, bao gồm việc gắn các byte vào một bộ đệm có thể thay đổi .

Vì vậy, đối với tất cả các mục đích thực tế, chúng tôi giới hạn trong việc trục xuất các biến khỏi mã của chính chúng tôi . OK, chúng ta có thể làm điều đó. Hầu hết. Về cơ bản những gì chúng ta cần là thay thế gần như tất cả các lần lặp bằng đệ quy và tất cả các đột biến bằng các cuộc gọi đệ quy trả về giá trị đã thay đổi. như vậy ...

class Ints {
     final int value;
     final Ints tail;

     public Ints(int value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints next() { return this.tail; }
     public int value() { return this.value; }
}

public Ints take(int count, Ints input) {
    if (count == 0 || input == null) return null;
    return new Ints(input.value(), take(count - 1, input.next()));
}    

public Ints squares_of(Ints input) {
    if (input == null) return null;
    int i = input.value();
    return new Ints(i * i, squares_of(input.next()));
}

Về cơ bản, chúng tôi xây dựng một danh sách được liên kết, trong đó mỗi nút là một danh sách. Mỗi danh sách có một "đầu" (giá trị hiện tại) và "đuôi" (danh sách con còn lại). Hầu hết các ngôn ngữ chức năng làm một cái gì đó giống như điều này, bởi vì nó rất phù hợp với tính bất biến hiệu quả. Một hoạt động "tiếp theo" chỉ trả về đuôi, thường được chuyển sang cấp độ tiếp theo trong một chồng các cuộc gọi đệ quy.

Bây giờ, đây là một phiên bản cực kỳ đơn giản của công cụ này. Nhưng nó đủ tốt để chứng minh một vấn đề nghiêm trọng với cách tiếp cận này trong Java. Xem xét mã này:

public function doStuff() {
    final Ints integers = ...somehow assemble list of 20 million ints...;
    final Ints result = take(25, squares_of(integers));
    ...
}

Mặc dù chúng tôi chỉ cần 25 ints cho kết quả, squares_ofnhưng không biết điều đó. Nó sẽ trả về bình phương của mỗi số trong integers. Đệ quy sâu 20 triệu cấp gây ra các vấn đề khá lớn trong Java.

Hãy xem, các ngôn ngữ chức năng mà bạn thường làm như thế này, có một tính năng gọi là "loại bỏ cuộc gọi đuôi". Điều đó có nghĩa là, khi trình biên dịch thấy hành động cuối cùng của mã là tự gọi (và trả về kết quả nếu hàm không trống), nó sử dụng khung ngăn xếp của cuộc gọi hiện tại thay vì thiết lập một cái mới và thay vào đó là "nhảy" của một "cuộc gọi" (vì vậy không gian ngăn xếp được sử dụng không đổi). Nói tóm lại, nó đi khoảng 90% theo hướng chuyển đổi đệ quy đuôi thành phép lặp. Nó có thể đối phó với hàng tỷ int mà không tràn ra ngăn xếp. (Cuối cùng nó vẫn hết bộ nhớ, nhưng việc tập hợp danh sách một tỷ int sẽ khiến bạn rối tung lên theo trí nhớ trên hệ thống 32 bit.)

Java không làm điều đó, trong hầu hết các trường hợp. (Nó phụ thuộc vào trình biên dịch và thời gian chạy, nhưng việc triển khai của Oracle không thực hiện được.) Mỗi ​​lệnh gọi đến một hàm đệ quy sẽ ngốn hết bộ nhớ của khung stack. Sử dụng quá nhiều, và bạn nhận được một ngăn xếp tràn. Tràn đầy tất cả nhưng đảm bảo cái chết của chương trình. Vì vậy, chúng tôi phải đảm bảo không làm điều đó.

Một cách giải quyết ... lười đánh giá. Chúng tôi vẫn có các giới hạn ngăn xếp, nhưng chúng có thể được gắn với các yếu tố chúng tôi có quyền kiểm soát nhiều hơn. Chúng ta không phải tính một triệu int chỉ để trả về 25. :)

Vì vậy, hãy xây dựng cho chúng tôi một số cơ sở hạ tầng đánh giá lười biếng. (Mã này đã được kiểm tra một thời gian trước, nhưng tôi đã sửa đổi nó khá nhiều kể từ đó; đọc ý tưởng, không phải lỗi cú pháp. :))

// Represents something that can give us instances of OutType.
// We can basically treat this class like a list.
interface Source<OutType> {
     public Source<OutType> next();
     public OutType value();
}

// Represents an operation that turns an InType into an OutType.
// Note, these can be the same type.  We're just flexible like that.
interface Transform<InType, OutType> {
    public OutType appliedTo(InType input);
}

// Represents an action (as opposed to a function) that can run on
// every element of a sequence.
abstract class Action<InType> {
    abstract void doWith(final InType input);
    public void doWithEach(final Source<InType> input) {
        if (input == null) return;
        doWith(input.value());
        doWithEach(input.next());
    }
}

// A list of Integers.
class Ints implements Source<Integer> {
     final Integer value;
     final Ints tail;
     public Ints(Integer value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints(Source<Integer> input) {
         this.value = input.value();
         this.tail = new Ints(input.next());
     }
     public Source<Integer> next() { return this.tail; }
     public Integer value() { return this.value; }
     public static Ints fromArray(Integer[] input) {
         return fromArray(input, 0, input.length);
     }
     public static Ints fromArray(Integer[] input, int start, int end) {
         if (end == start || input == null) return null;
         return new Ints(input[start], fromArray(input, start + 1, end));
     }
}

// An example of the spiff we get by splitting the "iterator" interface
// off.  These ints are effectively generated on the fly, as opposed to
// us having to build a huge list.  This saves huge amounts of memory
// and CPU time, for the rather common case where the whole sequence
// isn't needed.
class Range implements Source<Integer> {
    final int start, end;
    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
    public Integer value() { return start; }
    public Source<Integer> next() {
        if (start >= end) return null;
        return new Range(start + 1, end);
    }
}

// This takes each InType of a sequence and turns it into an OutType.
// This *takes* a Transform, rather than just *implementing* Transform,
// because the transforms applied are likely to be specified inline.
// If we just let people override `value()`, we wouldn't easily know what type
// to return, and returning our own type would lose the transform method.
static class Mapper<InType, OutType> implements Source<OutType> {
    private final Source<InType> input;
    private final Transform<InType, OutType> transform;

    public Mapper(Transform<InType, OutType> transform, Source<InType> input) {
        this.transform = transform;
        this.input = input;
    }

    public Source<OutType> next() {
         return new Mapper<InType, OutType>(transform, input.next());
    }
    public OutType value() {
         return transform.appliedTo(input.value());
    }
}

// ...

public <T> Source<T> take(int count, Source<T> input) {
    if (count <= 0 || input == null) return null;
    return new Source<T>() {
        public T value() { return input.value(); }
        public Source<T> next() { return take(count - 1, input.next()); }
    };
}

(Hãy nhớ rằng nếu điều này thực sự khả thi trong Java, mã ít nhất giống như ở trên sẽ là một phần của API.)

Bây giờ, với một cơ sở hạ tầng sẵn có, việc viết mã không cần các biến có thể thay đổi và ít nhất là ổn định cho số lượng đầu vào nhỏ hơn.

public Source<Integer> squares_of(Source<Integer> input) {
     final Transform<Integer, Integer> square = new Transform<Integer, Integer>() {
         public Integer appliedTo(final Integer i) { return i * i; }
     };
     return new Mapper<>(square, input);
}


public void example() {
    final Source<Integer> integers = new Range(0, 1000000000);

    // and, as for the author's "bet you can't do this"...
    final Source<Integer> squares = take(25, squares_of(integers));

    // Just to make sure we got it right :P
    final Action<Integer> printAction = new Action<Integer>() {
        public void doWith(Integer input) { System.out.println(input); }
    };
    printAction.doWithEach(squares);
}

Điều này chủ yếu hoạt động, nhưng nó vẫn có phần dễ bị chồng chất. Hãy thử takeing 2 tỷ ints và thực hiện một số hành động trên chúng. : P Cuối cùng sẽ đưa ra một ngoại lệ, ít nhất là cho đến khi hơn 64 GB RAM trở thành tiêu chuẩn. Vấn đề là, dung lượng bộ nhớ của chương trình dành riêng cho ngăn xếp của nó không lớn. Nó thường nằm trong khoảng từ 1 đến 8 MiB. (Bạn có thể yêu cầu lớn hơn, nhưng không quan trọng bạn yêu cầu bao nhiêu - bạn gọi take(1000000000, someInfiniteSequence), bạn sẽ có một ngoại lệ.) May mắn thay, với sự đánh giá lười biếng, điểm yếu nằm ở khu vực chúng ta có thể kiểm soát tốt hơn . Chúng ta chỉ cần cẩn thận về số lượng chúng ta take().

Nó vẫn sẽ có nhiều vấn đề mở rộng, bởi vì việc sử dụng ngăn xếp của chúng tôi tăng tuyến tính. Mỗi cuộc gọi xử lý một yếu tố và chuyển phần còn lại sang cuộc gọi khác. Tuy nhiên, bây giờ tôi nghĩ về nó, có một mẹo chúng ta có thể rút ra có thể giúp chúng ta có thêm một chút khoảng trống: biến chuỗi cuộc gọi thành một cây các cuộc gọi. Hãy xem xét một cái gì đó như thế này:

public <T> void doSomethingWith(T input) { /* magic happens here */ }
public <T> Source<T> workWith(Source<T> input, int count) {
    if (count < 0 || input == null) return null;
    if (count == 0) return input;
    if (count == 1) {
        doSomethingWith(input.value());
        return input.next();
    }
    return (workWith(workWith(input, count/2), count - count/2);
}

workWithvề cơ bản chia công việc thành hai nửa và gán mỗi nửa cho một cuộc gọi khác cho chính nó. Vì mỗi cuộc gọi làm giảm kích thước của danh sách làm việc xuống một nửa thay vì một, nên điều này sẽ mở rộng quy mô logarit thay vì tuyến tính.

Vấn đề là, chức năng này muốn có một đầu vào - và với một danh sách được liên kết, việc lấy chiều dài đòi hỏi phải đi qua toàn bộ danh sách. Điều đó dễ dàng giải quyết, mặc dù; chỉ đơn giản là không quan tâm có bao nhiêu mục. :) Đoạn mã trên sẽ hoạt động với thứ gì đó giống Integer.MAX_VALUEnhư số đếm, vì null sẽ dừng quá trình xử lý. Số lượng chủ yếu là ở đó vì vậy chúng tôi có một trường hợp cơ sở vững chắc. Nếu bạn dự đoán có nhiều hơn Integer.MAX_VALUEcác mục trong danh sách, thì bạn có thể kiểm tra workWithgiá trị trả về - cuối cùng nó sẽ không có giá trị. Nếu không, tái diễn.

Hãy ghi nhớ, điều này chạm đến nhiều yếu tố như bạn nói với nó. Nó không lười biếng; nó làm việc của nó ngay lập tức Bạn chỉ muốn làm điều đó cho các hành động - nghĩa là, điều mà mục đích duy nhất của nó là áp dụng chính nó cho mọi yếu tố trong danh sách. Như tôi đang nghĩ về nó ngay bây giờ, đối với tôi, dường như các chuỗi sẽ ít phức tạp hơn nếu được giữ tuyến tính; không nên là một vấn đề, vì các chuỗi không tự gọi mình bằng mọi cách - họ chỉ tạo các đối tượng gọi lại chúng.


3

Trước đây tôi đã cố gắng tạo một trình thông dịch cho một ngôn ngữ giống như ngôn ngữ trong Java, (một vài năm trước và tất cả các mã đã bị mất như trong CVS tại sourceforge) và các trình lặp sử dụng Java là một chút dài dòng cho lập trình chức năng trong danh sách.

Đây là một cái gì đó dựa trên giao diện trình tự, chỉ có hai thao tác bạn cần để nhận giá trị hiện tại và bắt đầu chuỗi ở phần tử tiếp theo. Chúng được đặt tên đầu và đuôi sau các chức năng trong sơ đồ.

Điều quan trọng là sử dụng một cái gì đó như Seqhoặc Iteratorgiao diện vì nó có nghĩa là danh sách được tạo ra một cách lười biếng. Các Iteratorgiao diện không thể là một đối tượng bất biến, vì vậy được ít phù hợp với chức năng lập trình - nếu bạn không thể nói nếu giá trị bạn vượt qua vào một chức năng đã được thay đổi bởi nó, bạn sẽ mất một trong những ưu điểm nổi bật của chương trình chức năng.

Rõ ràng integersnên là một danh sách của tất cả các số nguyên, vì vậy tôi đã bắt đầu từ 0 và thay vào đó trả về số dương và số âm.

Có hai phiên bản hình vuông - một phiên bản tạo một chuỗi tùy chỉnh, phiên bản còn lại sử dụng map'hàm' - Java 7 không có lambdas nên tôi đã sử dụng một giao diện - và lần lượt áp dụng nó cho từng phần tử.

Điểm quan trọng của square ( int x )hàm là chỉ cần loại bỏ nhu cầu gọi head()hai lần - thông thường tôi sẽ thực hiện điều này bằng cách đặt giá trị vào một biến cuối cùng, nhưng thêm hàm này có nghĩa là không có biến nào trong chương trình, chỉ có các tham số hàm.

Sự dài dòng của Java đối với loại lập trình này đã khiến tôi viết phiên bản thứ hai của trình thông dịch của mình bằng C99.

public class Squares {
    interface Seq<T> {
        T head();
        Seq<T> tail();
    }

    public static void main (String...args) {
        print ( take (25, integers ) );
        print ( take (25, squaresOf ( integers ) ) );
        print ( take (25, squaresOfUsingMap ( integers ) ) );
    }

    static Seq<Integer> CreateIntSeq ( final int n) {
        return new Seq<Integer> () {
            public Integer head () {
                return n;
            }
            public Seq<Integer> tail () {
                return n > 0 ? CreateIntSeq ( -n ) : CreateIntSeq ( 1 - n );
            }
        };
    }

    public static final Seq<Integer> integers = CreateIntSeq(0);

    public static Seq<Integer> squaresOf ( final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return square ( source.head() );
            }
            public Seq<Integer> tail () {
                return squaresOf ( source.tail() );
            }
        };
    }

    // mapping a function over a list rather than implementing squaring of each element
    interface Fun<T> {
        T apply ( T value );
    }

    public static Seq<Integer> squaresOfUsingMap ( final Seq<Integer> source ) {
        return map ( new Fun<Integer> () {
            public Integer apply ( final Integer value ) {
                return square ( value );
            }
        }, source );
    }

    public static <T> Seq<T> map ( final Fun<T> fun, final Seq<T> source ) {
        return new Seq<T> () {
            public T head () {
                return fun.apply ( source.head() );
            }
            public Seq<T> tail () {
                return map ( fun, source.tail() );
            }
        };
    }

    public static Seq<Integer> take ( final int count,  final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return source.head();
            }
            public Seq<Integer> tail () {
                return count > 0 ? take ( count - 1, source.tail() ) : nil;
            }
        };
    }

    public static int square ( final int x ) {
        return x * x;
    }

    public static final Seq<Integer> nil = new Seq<Integer> () {
        public Integer head () {
            throw new RuntimeException();
        }
        public Seq<Integer> tail () {
            return this;
        }
    };

    public static <T> void print ( final Seq<T> seq ) {
        printPartSeq ( "[", seq.head(), seq.tail() );
    }

    private static <T> void printPartSeq ( final String prefix, final T value, final Seq<T> seq ) {
        if ( seq == nil) {
            System.out.println("]");
        } else {
            System.out.print(prefix);
            System.out.print(value);
            printPartSeq ( ",", seq.head(), seq.tail() );
        }
    }
}

3

Làm thế nào để viết các chương trình Java hữu ích mà không cần sử dụng các biến có thể thay đổi.

Về lý thuyết, bạn có thể thực hiện bất kỳ thứ gì trong Java bằng cách sử dụng đệ quy và không có biến nào có thể thay đổi.

Trong thực tế:

  • Ngôn ngữ Java không được thiết kế cho việc này. Nhiều cấu trúc được thiết kế cho đột biến, và khó sử dụng nếu không có nó. (Chẳng hạn, bạn không thể khởi tạo một mảng Java có độ dài thay đổi mà không bị đột biến.)

  • Ditto cho các thư viện. Và nếu bạn giới hạn bản thân trong các lớp thư viện không sử dụng đột biến dưới vỏ bọc, điều đó còn khó hơn. (Bạn thậm chí không thể sử dụng Chuỗi ... hãy xem cách hashcodetriển khai.)

  • Việc triển khai Java chính không hỗ trợ tối ưu hóa cuộc gọi đuôi. Điều đó có nghĩa là các phiên bản đệ quy của thuật toán có xu hướng không gian ngăn xếp "đói". Và vì các ngăn xếp luồng Java không phát triển, bạn cần phải phân bổ các ngăn xếp lớn ... hoặc rủi ro StackOverflowError.

Kết hợp ba điều này và Java không thực sự là một lựa chọn khả thi để viết các chương trình hữu ích (tức là không tầm thường) mà không có các biến có thể thay đổi.

(Nhưng này, điều đó ổn. Có những ngôn ngữ lập trình khác có sẵn cho JVM, một số ngôn ngữ hỗ trợ lập trình chức năng.)


2

Khi chúng tôi đang tìm kiếm một ví dụ về các khái niệm, tôi muốn nói rằng hãy đặt Java sang một bên và tìm một cài đặt khác nhưng quen thuộc để tìm phiên bản quen thuộc của các khái niệm. Các ống UNIX khá giống với chuỗi chức năng lười biếng.

cat /dev/zero | tr '\0' '\n' | cat -n | awk '{ print $0 * $0 }' | head 25

Trong Linux, điều này có nghĩa là, cung cấp cho tôi các byte được tạo thành từ sai chứ không phải bit thật, cho đến khi tôi mất cảm giác ngon miệng; thay đổi từng byte đó thành một ký tự dòng mới; đánh số từng dòng do đó tạo ra; và sản xuất bình phương của số đó. Hơn nữa, tôi có cảm giác thèm ăn 25 dòng và không hơn.

Tôi khẳng định rằng một lập trình viên sẽ không được khuyên nên viết một đường ống Linux theo cách đó. Đó là kịch bản shell Linux tương đối bình thường.

Tôi khẳng định rằng một lập trình viên sẽ không được khuyên nên thử viết điều tương tự tương tự trong Java. Lý do là bảo trì phần mềm là một yếu tố chính trong chi phí trọn đời của các dự án phần mềm. Chúng tôi không muốn làm rối loạn lập trình viên tiếp theo bằng cách trình bày những gì rõ ràng là một chương trình Java nhưng thực sự được viết bằng ngôn ngữ một lần tùy chỉnh bằng cách sao chép công phu chức năng đã tồn tại trong nền tảng Java.

Mặt khác, tôi khẳng định rằng lập trình viên tiếp theo có thể chấp nhận hơn nếu một số gói "Java" của chúng tôi thực sự là các gói Máy ảo Java được viết bằng một trong các ngôn ngữ chức năng hoặc đối tượng / chức năng như Clojure và Scala. Chúng được thiết kế để được mã hóa bằng cách kết hợp các hàm với nhau và được gọi từ Java theo cách thông thường của các lệnh gọi phương thức Java.

Sau đó, một lần nữa, nó vẫn có thể là một ý tưởng tốt cho một lập trình viên Java lấy cảm hứng từ lập trình chức năng, ở những nơi.

Gần đây, kỹ thuật yêu thích của tôi là sử dụng biến trả về bất biến, chưa được khởi tạo và một lối thoát duy nhất để, như một số trình biên dịch ngôn ngữ chức năng thực hiện, Java kiểm tra xem có vấn đề gì xảy ra trong phần thân của hàm không, tôi luôn cung cấp một và chỉ một giá trị trả về. Thí dụ:

int f(final int n) {
    final int result; // not initialized here!
    if (n < 0) {
        result = -n;
    } else if (n < 1) {
        result = 0;
    } else {
        result = n - 1;
    }
    // If I would leave off the "else" clause,
    // Java would fail to compile complaining that
    // "result" is possibly uninitialized.
    return result;
}


Tôi chắc chắn khoảng 70% Java đã thực hiện kiểm tra giá trị trả về. Bạn sẽ nhận được một lỗi về "tuyên bố trả lại bị thiếu" nếu điều khiển có thể rơi vào cuối hàm không trống.
cHao

Quan điểm của tôi: Nếu bạn viết mã vì int result = -n; if (n < 1) { result = 0 } return result;nó biên dịch tốt và trình biên dịch không biết liệu bạn có định làm cho nó tương đương với hàm trong ví dụ của tôi không. Có thể ví dụ đó quá đơn giản để làm cho kỹ thuật này có vẻ hữu ích, nhưng trong một hàm có nhiều nhánh tôi cảm thấy thật tuyệt khi làm rõ rằng kết quả được gán chính xác một lần bất kể con đường nào được theo.
minopret

if (n < 1) return 0; else return -n;Tuy nhiên, nếu bạn nói , bạn sẽ không gặp vấn đề gì ... và bên cạnh đó đơn giản hơn. :) Dường như với tôi rằng trong trường hợp đó, quy tắc "một lần trả lại" thực sự giúp gây ra vấn đề không biết khi nào giá trị trả về của bạn được đặt; mặt khác, bạn chỉ có thể trả về nó và Java sẽ có thể xác định nhiều hơn khi các đường dẫn khác có thể không trả về giá trị, vì bạn không còn chia nhỏ cách tính giá trị từ việc trả lại thực tế của nó.
cHao

Hoặc, cho ví dụ câu trả lời của bạn , if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;.
cHao

Tôi vừa quyết định rằng tôi không muốn dành thêm bất kỳ khoảnh khắc nào trong đời để bảo vệ quy tắc OnlyOneReturn trong Java. Ra nó đi. Khi và nếu tôi nghĩ về một thực hành mã hóa Java mà tôi cảm thấy muốn bảo vệ nó bị ảnh hưởng bởi các thực tiễn lập trình chức năng, tôi sẽ đưa ra một thay thế cho ví dụ đó. Cho đến lúc đó, không có ví dụ.
minopret

0

Cách dễ nhất để tìm ra điều đó sẽ được cung cấp sau đây cho trình biên dịch Frege và xem mã java được tạo:

module Main where

result = take 25 (map sqr [1..]) where sqr x = x*x

Sau vài ngày tôi thấy suy nghĩ của mình trở lại câu trả lời này. Sau tất cả các phần gợi ý của tôi là triển khai các phần lập trình chức năng trong Scala. Nếu chúng tôi xem xét áp dụng Scala ở những điểm mà chúng tôi thực sự có Haskell trong tâm trí (và tôi nghĩ tôi không phải là một blog duy nhất.zlemma.com/2013/02/20/ ,), ít nhất chúng ta có nên xem xét Frege không?
minopret

@minopret Đây thực sự là Frege thích hợp - những người đã biết và yêu Haskell và vẫn cần JVM. Tôi tự tin một ngày nào đó Frege sẽ đủ trưởng thành để có được sự cân nhắc nghiêm túc ít nhất.
Ingo
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.