짧은 버전 :
Java에서 단일 할당 스타일을 안정적으로 작동 시키려면 (1) 불변 친화적 인 인프라가 필요하며 (2) 테일 콜 제거를위한 컴파일러 또는 런타임 수준의 지원이 필요합니다.
많은 인프라를 작성할 수 있으며 스택을 채우지 않도록 할 수 있습니다. 그러나 각 호출이 스택 프레임을 사용하는 한 재귀의 양에 제한이 있습니다. iterables를 작거나 게으르게 유지하면 큰 문제가 없어야합니다. 최소한 대부분의 문제는 한 번에 백만 개의 결과를 반환하지 않아도됩니다. :)
또한 프로그램은 실행 가치를 얻기 위해 실제로 눈에 띄는 변화에 영향을 미치므로 모든 것을 변경할 수는 없습니다 . 그러나 대안이 너무 번거로운 특정 핵심 지점에서만 필수 가변 항목 (예 : 스트림)의 작은 하위 집합을 사용하여 자신의 물건 대부분을 불변으로 유지할 수 있습니다.
긴 버전 :
간단히 말해서, Java 프로그램은 가치있는 일을 원한다면 변수를 완전히 피할 수 없습니다. 그것들 을 포함 시킬 수 있고, 따라서 가변성을 크게 제한 할 수 있지만, 언어와 API의 디자인은 근본적으로 기본 시스템을 변경해야 할 필요성 때문에 불변성을 실현할 수 없게 만듭니다.
자바는 등 처음부터 설계되었습니다 필수적 , 객체 지향 언어입니다.
- 명령형 언어는 거의 항상 어떤 종류의 가변 변수에 의존합니다. 그들은 예를 들어, 재귀를 통해 반복을 선호하는 경향이 있고, 거의 모든 반복적 인 구조 -도
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()));
}
기본적으로, 우리는 각 노드 자체가리스트 인 링크 된리스트를 만듭니다. 각 목록에는 "head"(현재 값)와 "tail"(나머지 하위 목록)이 있습니다. 대부분의 기능적 언어는 효율적인 불변성에 매우 적합하기 때문에 이와 유사한 기능을 수행합니다. "다음"오퍼레이션은 테일을 리턴하며 일반적으로 재귀 호출 스택에서 다음 레벨로 전달됩니다.
자, 이것은이 것들의 지나치게 단순화 된 버전입니다. 그러나 Java 에서이 접근법에 대한 심각한 문제를 보여주기에 충분합니다. 이 코드를 고려하십시오.
public function doStuff() {
final Ints integers = ...somehow assemble list of 20 million ints...;
final Ints result = take(25, squares_of(integers));
...
}
결과에 25 개의 정수만 있으면되지만 squares_of
이를 모릅니다. 의 모든 숫자의 제곱을 반환합니다 integers
. 2 천만 레벨의 재귀는 Java에서 상당히 큰 문제를 일으 킵니다.
일반적으로 이와 같은 기능을 수행하는 기능 언어에는 "tail call elimination"이라는 기능이 있습니다. 즉, 컴파일러가 코드의 마지막 동작이 자신을 호출하고 함수가 무효가 아닌 경우 결과를 반환하는 것을 볼 때 새 호출을 설정하는 대신 현재 호출의 스택 프레임을 사용하고 대신 "점프"를 수행합니다. "호출"(따라서 사용 된 스택 공간은 일정하게 유지됨). 간단히 말해서, 꼬리 재귀를 반복으로 바꾸는 방향의 약 90 %입니다. 스택을 오버플로하지 않고도 수십억 개의 정수를 처리 할 수 있습니다. (결국 여전히 메모리가 부족하지만 32 억 시스템에서 10 억 정수 목록을 작성하면 메모리가 엉망이됩니다.)
대부분의 경우 Java는 그렇게하지 않습니다. (컴파일러와 런타임에 따라 다르지만 오라클의 구현에서는 그렇지 않습니다.) 재귀 함수에 대한 각 호출은 스택 프레임의 메모리를 차지합니다. 너무 많이 사용하면 스택 오버플로가 발생합니다. 스택 오버플로는 프로그램의 죽음을 보장합니다. 그래서 우리는 그렇게하지 않아야합니다.
하나의 반 해결 방법 ... 게으른 평가. 우리는 여전히 스택 제한이 있지만 더 많은 제어 권한을 가진 요소에 묶여있을 수 있습니다. 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
20 억 정수를 시도 하고 그들에 대한 조치를 취하십시오. : P 64GB 이상의 RAM이 표준이 될 때까지 결국 예외가 발생합니다. 문제는 스택을 위해 예약 된 프로그램 메모리의 양이 그렇게 크지 않다는 것입니다. 일반적으로 1 ~ 8 MiB입니다. (당신은 더 큰 요청할 수 있지만, 모든 훨씬 얼마나 당신이 물어 중요하지 않습니다 - 당신이 전화를 take(1000000000, someInfiniteSequence)
하면 됩니다 . 예외가) 지역에서 우리가 더 나은 수 다행히 게으른 평가와 함께, 약한 자리가 제어 . 우리는 단지 우리가 얼마나 많은지주의해야합니다 take()
.
스택 사용량이 선형으로 증가하기 때문에 여전히 확장에 많은 문제가 있습니다. 각 호출은 한 요소를 처리하고 나머지는 다른 호출로 전달합니다. 이제 생각해 보니, 우리가 끌어낼 수있는 한 가지 트릭이 있습니다. 이로 인해 약간 더 많은 헤드 룸이 생길 수 있습니다. 통화 체인을 통화 트리 로 전환하십시오 . 다음과 같은 것을 고려하십시오.
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
기본적으로 작업을 두 개의 반으로 나누고 각 반을 다른 호출에 할당합니다. 각 호출은 작업 목록의 크기를 1이 아니라 절반으로 줄이므로 선형이 아니라 로그로 확장해야합니다.
문제는이 함수가 입력을 원한다는 것입니다. 링크 된리스트에서 길이를 얻으려면 전체리스트를 순회해야합니다. 그래도 쉽게 해결됩니다. 단순히 상관 없어 얼마나 많은 항목. :) 위의 코드는 Integer.MAX_VALUE
null 과 같이 처리를 중지하기 때문에 카운트 와 같은 것으로 작동 합니다. 카운트는 대부분 거기에 있으므로 우리는 견고한 기본 사례를 갖습니다. Integer.MAX_VALUE
목록에 여러 개 이상의 항목 이있을 것으로 예상되면 workWith
의 반환 값을 확인할 수 있습니다 . 끝에 null이 있어야합니다. 그렇지 않으면 재귀하십시오.
명심하십시오, 이것은 당신이 말한만큼 많은 요소에 영향을 미칩니다. 게으르지 않습니다. 그것은 즉시 그 일을합니다. 당신은 행동 , 즉 목록의 모든 요소에 자신을 적용하는 것이 유일한 목적 을 위해서만하고 싶습니다 . 지금 생각하고 있듯이 선형으로 유지하면 시퀀스가 훨씬 덜 복잡해 보입니다. 시퀀스는 어쨌든 스스로를 호출하지 않기 때문에 문제가되지 않아야합니다. 시퀀스를 다시 호출하는 객체를 생성하기 만합니다.