เวอร์ชั่นสั้น:
ในการที่จะทำให้รูปแบบการมอบหมายเดี่ยวทำงานได้อย่างน่าเชื่อถือใน Java คุณจะต้อง (1) โครงสร้างพื้นฐานที่ไม่เปลี่ยนรูปได้ง่ายและ (2) คอมไพเลอร์ - หรือการสนับสนุนระดับรันไทม์สำหรับการกำจัดการโทรหาง
เราสามารถเขียนโครงสร้างพื้นฐานได้มากมายและเราสามารถจัดเรียงสิ่งต่าง ๆ เพื่อพยายามหลีกเลี่ยงการเติมสแต็ค แต่ตราบใดที่การโทรแต่ละครั้งใช้กรอบสแต็กจะมีการ จำกัด จำนวนการเรียกซ้ำที่คุณสามารถทำได้ ทำให้ iterables เล็กและ / หรือขี้เกียจและคุณไม่ควรมีปัญหาสำคัญ อย่างน้อยที่สุดปัญหาที่คุณพบไม่จำเป็นต้องส่งคืนผลลัพธ์ล้านรายการในครั้งเดียว :)
นอกจากนี้โปรดทราบว่าเนื่องจากโปรแกรมจะต้องมีผลต่อการเปลี่ยนแปลงที่มองเห็นได้จริงเพื่อให้มีมูลค่าในการทำงานคุณจึงไม่สามารถทำให้ทุกอย่างเปลี่ยนแปลงไม่ได้ อย่างไรก็ตามคุณสามารถเก็บสิ่งของส่วนใหญ่ของคุณไว้ไม่เปลี่ยนรูปได้โดยใช้ชุดย่อยเล็ก ๆ ที่จำเป็น (เช่นลำธาร) ที่จุดสำคัญบางจุดที่ทางเลือกจะเป็นภาระมากเกินไป
รุ่นยาว:
พูดง่ายๆก็คือโปรแกรม Java ไม่สามารถหลีกเลี่ยงตัวแปรทั้งหมดหากต้องการทำสิ่งที่ควรค่าแก่การทำ คุณสามารถมีพวกเขาและทำให้ จำกัด การความไม่แน่นอนในระดับมาก แต่การออกแบบมากของภาษาและ API - พร้อมกับความจำเป็นในการในที่สุดก็เปลี่ยนระบบพื้นฐาน - ให้ไปไม่เปลี่ยนไม่ได้ทั้งหมด
Java ได้รับการออกแบบตั้งแต่เริ่มต้นเป็นความจำเป็น , เชิงวัตถุภาษา
- ภาษาที่มีความหมายมักขึ้นอยู่กับตัวแปรที่ไม่แน่นอนบางชนิด พวกเขามีแนวโน้มที่จะสนับสนุนการทำซ้ำมากกว่าการเรียกซ้ำตัวอย่างเช่นและการสร้างซ้ำเกือบทั้งหมด - แม้
while (true)
และfor (;;)
! - ขึ้นอยู่กับตัวแปรอย่างเต็มที่บางที่เปลี่ยนจากการทำซ้ำเป็นซ้ำ
- ภาษาเชิงวัตถุจะมองเห็นทุกโปรแกรมเป็นกราฟของวัตถุที่ส่งข้อความถึงกันและในเกือบทุกกรณีตอบสนองต่อข้อความเหล่านั้นโดยการปิดบังบางสิ่ง
ผลลัพธ์สุดท้ายของการตัดสินใจออกแบบคือไม่มีตัวแปรที่ไม่สามารถเปลี่ยนแปลงได้ Java ไม่มีทางที่จะเปลี่ยนสถานะของสิ่งใด - แม้แต่สิ่งที่เรียบง่ายเหมือนกับการพิมพ์ "Hello world!" ไปที่หน้าจอเกี่ยวข้องกับกระแสข้อมูลขาออกซึ่งเกี่ยวข้องกับการผสานไบต์ในบัฟเฟอร์ที่ไม่แน่นอน
ดังนั้นเพื่อวัตถุประสงค์เชิงปฏิบัติทั้งหมดเราถูก จำกัด ให้กำจัดตัวแปรจากรหัสของเราเอง ตกลงเราสามารถทำได้ เกือบจะ โดยพื้นฐานสิ่งที่เราต้องการคือแทนที่การวนซ้ำเกือบทั้งหมดด้วยการเรียกซ้ำและการกลายพันธุ์ทั้งหมดด้วยการเรียกซ้ำเรียกคืนค่าที่เปลี่ยนแปลง อย่างนั้น ...
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()));
}
โดยพื้นฐานแล้วเราสร้างรายการเชื่อมโยงที่แต่ละโหนดเป็นรายการในตัวเอง แต่ละรายการมี "หัว" (ค่าปัจจุบัน) และ "หาง" (รายการย่อยที่เหลือ) ภาษาที่ใช้งานได้ส่วนใหญ่ทำอะไรที่คล้ายกันกับเรื่องนี้ การดำเนินการ "ถัดไป" เพียงแค่ส่งกลับหางซึ่งโดยทั่วไปจะถูกส่งผ่านไปยังระดับถัดไปในกองของการโทรซ้ำ
ตอนนี้เป็นรุ่นที่มีการดัดแปลงอย่างมากของสิ่งนี้ แต่มันก็ดีพอที่จะแสดงให้เห็นถึงปัญหาร้ายแรงด้วยวิธีนี้ใน Java พิจารณารหัสนี้:
public function doStuff() {
final Ints integers = ...somehow assemble list of 20 million ints...;
final Ints result = take(25, squares_of(integers));
...
}
แม้ว่าเราต้องการ 25 ints สำหรับผลลัพธ์ แต่squares_of
ก็ไม่ทราบ มันจะคืนค่ากำลังสองของจำนวนทุกintegers
ค่า การเรียกซ้ำ 20 ล้านระดับทำให้เกิดปัญหาใหญ่ใน Java
ดูว่าภาษาที่ใช้งานได้ตามปกติที่คุณทำอย่างไร้สาระเช่นนี้มีคุณสมบัติที่เรียกว่า "การกำจัดการโทรหาง" สิ่งที่หมายถึงคือเมื่อคอมไพเลอร์เห็นการกระทำล่าสุดของรหัสที่จะเรียกตัวเอง (และส่งกลับผลลัพธ์ถ้าฟังก์ชั่นที่ไม่ใช่โมฆะ) มันใช้สแต็คเฟรมของการโทรปัจจุบันแทนการตั้งค่าใหม่และ "กระโดด" แทน ของ "การโทร" (ดังนั้นพื้นที่สแต็กที่ใช้ยังคงไม่เปลี่ยนแปลง) ในระยะสั้นมันไปประมาณ 90% ของวิธีการเปลี่ยนการเรียกซ้ำหางเป็นการวนซ้ำ มันสามารถจัดการกับบรรดาพันล้าน ints โดยไม่ล้นกอง (ในที่สุดมันก็ยังคงมีหน่วยความจำไม่เพียงพอ แต่การรวบรวมรายชื่อ ints พันล้านหน่วยกำลังจะทำให้คุณสับสนในหน่วยความจำในระบบ 32 บิต)
Java ไม่ได้ทำเช่นนั้นในกรณีส่วนใหญ่ (ขึ้นอยู่กับคอมไพเลอร์และรันไทม์ แต่การนำไปปฏิบัติของออราเคิลนั้นไม่ได้ทำ) การเรียกใช้ฟังก์ชันเรียกซ้ำแต่ละครั้งจะทำให้หน่วยความจำมีค่ามากขึ้น ใช้มากเกินไปและคุณจะได้รับสแต็คล้น ล้นสแต็คทั้งหมด แต่รับประกันความตายของโปรแกรม ดังนั้นเราต้องแน่ใจว่าจะไม่ทำเช่นนั้น
หนึ่งวิธีแก้ปัญหา ... การประเมินผลที่ขี้เกียจ เรายังคงมีข้อ จำกัด ของสแต็ก แต่สามารถเชื่อมโยงกับปัจจัยที่เราควบคุมได้มากกว่า เราไม่ต้องคำนวณ ints หนึ่งล้านแค่คืน 25 :)
ดังนั้นเรามาสร้างโครงสร้างพื้นฐานการประเมินผลที่ขี้เกียจกับเรา (รหัสนี้ได้รับการทดสอบในขณะที่กลับ แต่ฉันได้แก้ไขมันค่อนข้างน้อยแล้วอ่านความคิดไม่ใช่ข้อผิดพลาดทางไวยากรณ์ :))
// 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()); }
};
}
(โปรดทราบว่าหากสิ่งนี้สามารถใช้งานได้จริงใน Java โค้ดอย่างน้อยค่อนข้างคล้ายกับที่กล่าวมาจะเป็นส่วนหนึ่งของ API)
ขณะนี้มีโครงสร้างพื้นฐานอยู่แล้วค่อนข้างง่ายที่จะเขียนโค้ดที่ไม่ต้องการตัวแปรที่ไม่แน่นอนและอย่างน้อยก็มีความเสถียรสำหรับอินพุตจำนวนน้อย
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);
}
ส่วนใหญ่ใช้งานได้ แต่ก็ยังค่อนข้างมีแนวโน้มที่จะสแต็คล้น ลองใช้take
ints 2 พันล้านและดำเนินการกับมัน : P ในที่สุดมันจะส่งข้อยกเว้นอย่างน้อยก็จนกว่า RAM 64+ GB จะกลายเป็นมาตรฐาน ปัญหาคือจำนวนหน่วยความจำของโปรแกรมที่สำรองไว้สำหรับสแต็กของมันไม่ใหญ่มาก โดยทั่วไปแล้วจะอยู่ระหว่าง 1 ถึง 8 MiB (คุณสามารถขอที่ใหญ่กว่าได้ แต่มันไม่สำคัญว่าคุณจะขอเท่าไหร่ - คุณโทรหาtake(1000000000, someInfiniteSequence)
คุณจะได้รับการยกเว้น) โชคดีที่มีการประเมินที่ขี้เกียจจุดอ่อนอยู่ในพื้นที่ที่เราสามารถควบคุมได้ดีกว่า . take()
เราก็จะต้องระมัดระวังเกี่ยวกับเราเท่าไหร่
มันจะยังคงมีปัญหามากมายเพิ่มขึ้นเนื่องจากการใช้สแต็กของเราเพิ่มขึ้นเป็นเส้นตรง การโทรแต่ละครั้งจะจัดการองค์ประกอบหนึ่งและส่งผ่านส่วนที่เหลือไปยังอีกสายหนึ่ง ตอนนี้ที่ฉันคิดเกี่ยวกับมันแม้ว่ามีเคล็ดลับหนึ่งที่เราสามารถดึงซึ่งอาจได้รับเรา headroom ค่อนข้างมาก: เปลี่ยนสายการโทรเป็นต้นไม้สาย ลองพิจารณาอะไรทำนองนี้:
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);
}
workWith
แบ่งงานออกเป็นสองส่วนโดยแบ่งงานออกเป็นสองส่วน เนื่องจากการโทรแต่ละครั้งจะลดขนาดของรายการการทำงานลงครึ่งหนึ่งมากกว่าหนึ่งรายการสิ่งนี้จึงควรปรับขนาดลอการิทึมมากกว่าเชิงเส้น
ปัญหาคือฟังก์ชั่นนี้ต้องการอินพุต - และด้วยรายการที่เชื่อมโยงการรับความยาวต้องผ่านรายการทั้งหมด ที่แก้ไขได้อย่างง่ายดายแม้ว่า; เพียงแค่ไม่สนใจวิธีการหลายรายการที่มี :) รหัสข้างต้นจะทำงานกับสิ่งที่ชอบInteger.MAX_VALUE
นับเป็นโมฆะหยุดการประมวลผลต่อไป การนับนั้นส่วนใหญ่มีดังนั้นเราจึงมีตัวฐานที่แน่นหนา หากคุณคาดว่าจะมีมากกว่าInteger.MAX_VALUE
รายการในรายการคุณสามารถตรวจสอบworkWith
ค่าส่งคืนของ - มันควรจะเป็นโมฆะในตอนท้าย มิฉะนั้นเรียกเก็บเงิน
โปรดจำไว้ว่านี่เป็นองค์ประกอบหลายอย่างที่คุณบอก มันไม่ขี้เกียจ มันทำมันทันที คุณต้องการทำสิ่งนั้นเพื่อการกระทำนั่นคือสิ่งของที่มีจุดประสงค์เดียวคือใช้กับทุกองค์ประกอบในรายการ อย่างที่ฉันคิดตอนนี้ฉันคิดว่าลำดับจะซับซ้อนน้อยกว่ามากถ้าเก็บไว้เป็นเส้นตรง ไม่ควรเป็นปัญหาเนื่องจากลำดับไม่ได้เรียกตัวเองว่ามันเป็นเพียงแค่สร้างวัตถุที่เรียกพวกเขาอีกครั้ง