가변 변수를 사용하지 않고 유용한 Java 프로그램을 작성하는 방법


12

작가가 언급 한 함수형 프로그래밍에 관한 기사를 읽고있었습니다.

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

변수가 없습니다. 실제로, 그것은 단지 3 개의 기능과 하나의 상수를 갖지 않습니다. 변수를 사용하지 않고 Java로 정수 제곱을 작성하십시오. 오, 아마 그것을 할 수있는 방법이있을 것입니다.

Java로 이것을 달성 할 수 있습니까? 처음 15 개의 정수의 제곱을 인쇄해야한다고 가정하면 변수를 사용하지 않고 for 또는 while 루프를 작성할 수 있습니까?

모드 통지

이 질문은 코드 골프 대회 가 아닙니다 . 우리는 단지 다른 코드 조각이 아니라 관련된 개념을 설명하는 답변을 찾고 있습니다 (이상적인 초기 답변을 반복하지 않고).


19
함수 예제 내부에서 변수를 사용하지만 언어는 모든 변수를 장면 뒤에서 수행합니다. 불쾌한 부분을 올바르게 수행했다고 생각하는 사람에게 효과적으로 위임했습니다.
Blrfl

12
@Blrfl : 모든 코드 조각이 궁극적으로 x86 머신 코드로 변환되므로 "장면 뒤"인수는 모든 언어 기반 토론을 중단시킵니다. x86 코드는 객체 지향적이지 않고 절차 적이거나 기능적이지 않으며 아무것도 아닙니다. 그러나이 범주는 프로그래밍 언어에 유용한 태그입니다. 구현이 아닌 언어를보십시오.
thiton

10
@thiton Disagreed. Blrfl의 말에 따르면,이 함수들은 아마도 같은 프로그래밍 언어로 작성된 변수를 사용할 것입니다 . 여기서 저수준으로 갈 필요가 없습니다. 스 니펫은 단지 라이브러리 함수를 사용하고 있습니다. : 당신은 쉽게 자바에서 동일한 코드를 작성할 수 있습니다 참조 squaresOf(integers()).take(25)에 대한 무한 집합의 어려움 거짓말을 (그 함수를 작성하는 것은 독자들에게 연습 문제로 남겨 integers(),하지만, 때문에 열망 평가의와는 아무 자바에 대한 문제 없다 변수)
Andres F.

6
그 인용문은 혼란스럽고 오해의 소지가 있으며 거기에는 마법이 없으며 구문 설탕 만 있습니다.
yannis

2
@thiton FP 언어에 대해 더 많이 배울 것을 제안하지만 그럼에도 불구하고 코드 스 니펫은 "변수"가 필요하지 않다는 주장을 지원하지 않습니다 (또는 거부합니다). FP에서 종류가 일반적입니다). 이 스 니펫은 Java로 구현할 수있는 라이브러리 함수를 보여줍니다.
Andres F.

답변:


31

파괴적인 업데이트를 사용하지 않고 Java에서 이러한 예제를 구현할 수 있습니까? 예. 그러나 @Thiton과 기사 자체에서 언급했듯이 미운 것입니다 (개인의 취향에 따라 다름). 한 가지 방법은 재귀를 사용하는 것입니다. 다음은 비슷한 것을 하는 Haskell 예제입니다.

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

주 1) 돌연변이의 부재, 2) 재귀의 사용 및 3) 루프의 부재. 마지막 요점은 매우 중요합니다. 함수 언어는 루프가 Java에 사용되는 대부분의 경우에 사용될 수 있기 때문에 언어에 내장 된 반복 구문이 필요하지 않습니다. 다음은 믿을 수 없을 정도로 표현적인 함수 호출이 가능한 방법을 보여주는 잘 알려진 일련의 논문입니다.


기사가 만족스럽지 않고 몇 가지 추가 사항을 만들고 싶습니다.

이 기사는 함수형 프로그래밍과 그 이점에 대한 매우 가난하고 혼란스러운 설명입니다. 함수형 프로그래밍에 대한 다른 학습 자료 를 강력히 추천 합니다.

이 기사에서 가장 혼란스러운 부분은 Java (및 대부분의 다른 주류 언어)에서 할당 문에 두 가지 용도가 있다는 언급은 없다는 것입니다.

  1. 값을 이름에 바인딩 : final int MAX_SIZE = 100;

  2. 파괴적인 업데이트 : int a = 3; a += 1; a++;

함수형 프로그래밍은 두 번째를 피하지만 첫 번째를 포용합니다 (예 : -expressions let, 함수 매개 변수, 최상위 defineitions) . 그렇지 않으면 문서는 단지 바보 같다 궁금 당신을 떠날 수있는, 무엇 때문에, 이해에 매우 중요한 포인트입니다 take, squares-of그리고 integers변수하지 않을 경우?

또한이 예는 의미가 없습니다. 그것은의 구현을 표시하지 않습니다 take, squares-of또는 integers. 우리가 아는 모든 것은 가변 변수를 사용하여 구현됩니다. @Martin이 말했듯 이이 예제를 Java로 간단하게 작성할 수 있습니다.

함수 프로그래밍에 대해 배우고 싶다면이 기사와 다른 기사를 피하는 것이 좋습니다. 그것은 개념과 기본을 가르치기보다는 충격과 불쾌감을 목표로 더 쓰여진 것 같습니다. 대신 John Hughes 의 필자가 가장 좋아하는 논문 중 하나를 확인하십시오 . 휴즈는이 기사에서 다루었던 것과 동일한 문제 중 일부를 다루려고합니다 (휴즈는 동시성 / 병렬화에 대해 이야기하지는 않지만). 티저는 다음과 같습니다.

이 백서는 더 큰 (비 기능적) 프로그래머 커뮤니티에 기능적 프로그래밍의 중요성을 보여주고 기능적 프로그래머가 그 장점이 무엇인지 명확하게함으로써 그 장점을 최대한 활용할 수 있도록 돕기위한 시도입니다.

[...]

이 백서의 나머지 부분에서 기능적 언어는 두 가지 매우 중요한 종류의 접착제를 제공한다고 주장합니다. 우리는 새로운 방식으로 모듈화되어 단순화 될 수있는 프로그램의 예를 제시 할 것이다. 이것이 기능적 프로그래밍 기능의 핵심이며 모듈화를 개선 할 수 있습니다. 또한 기능 프로그래머가 더 작고 더 단순하고 더 일반적인 모듈로 노력해야하는 목표는 우리가 설명 할 새로운 접착제와 함께 접착됩니다.


10
+1 "기능 프로그래밍에 대해 배우고 싶다면이 기사와 다른 기사를 피하는 것이 좋습니다. 개념과 기본을 가르치기보다는 충격과 불쾌감을주기 위해 더 많이 쓰여진 것 같습니다."

3
사람들이 FP를하지 않는 이유의 절반은 유니에서 FP에 대해 아무 것도 듣거나 배우지 않기 때문이며, 다른 절반은 정보를 볼 때 정보를 제공하지 않고 기사를 찾는 기사를 발견하기 때문입니다. 이익을 가지고 생각하는 합리적인 접근 방식이 아닌 게임을합니다. 정보의 좋은 소스를주는 일
지미 호파를

3
질문에 더 직접적이라면 질문에 대한 답을 절대 맨 위에 두십시오. 그러면 질문이 계속 열려있을 것입니다 (직접 질문 중심의 답변으로)
Jimmy Hoffa

2
nitpick 죄송합니다. 왜이 haskell 코드를 선택했는지 이해가되지 않습니다. 나는 LYAH를 읽었고 당신의 모범은 내가 이해하기 어렵다. 또한 원래 질문과의 관계를 보지 못했습니다. 왜 take 25 (map (^2) [1..])예 를 들어서 사용하지 않았 습니까?
Daniel Kaplan

2
@tieTYT 좋은 질문입니다. 지적 해 주셔서 감사합니다. 이 예제를 사용한 이유는 재귀를 사용하여 숫자 목록을 생성하고 변경 가능한 변수를 피하는 방법을 보여주기 때문입니다. 내 의도는 OP가 해당 코드를보고 Java에서 비슷한 것을 수행하는 방법에 대해 생각하는 것이 었습니다. 코드 스 니펫을 해결하기 위해 무엇 [1..]입니까? 하스켈에 내장 된 멋진 기능이지만 이러한 목록을 생성하는 개념을 설명하지는 않습니다. Enum클래스의 인스턴스 (구문에 필요한)도 도움이 될 것이지만 너무 게으르지 않았습니다. 따라서 unfoldr. :)

27

당신은하지 않을 것입니다. 변수는 명령형 프로그래밍의 핵심이며 변수를 사용하지 않고 명령 식으로 프로그래밍하려고하면 모든 사람이 엉덩이에 통증을 유발합니다. 다른 프로그래밍 패러다임에서는 스타일이 다르며 다른 개념이 기반이됩니다. 작은 범위에서 잘 사용될 때 Java의 변수는 나쁘지 않습니다. 변수가없는 Java 프로그램을 요청하는 것은 함수가없는 Haskell 프로그램을 요청하는 것과 같으므로 요청하지 않으며 변수를 사용하기 때문에 명령형 프로그래밍을 열등한 것으로 보지 않아도됩니다.

따라서 Java 방식은 다음과 같습니다.

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

변수의 증오로 인해 더 복잡한 방식으로 작성하도록 속지 마십시오.


5
"가혹한 변수"? Ooookay ... 함수형 프로그래밍에 대해 무엇을 읽었습니까? 어떤 언어를 사용해 보셨습니까? 어느 튜토리얼?
Andres F.

8
@AndresF .: Haskell에서 2 년 이상의 코스워크. 나는 FP가 나쁘다고 말하지 않는다. 그러나 많은 FP-vs-IP 토론 (예 : 링크 된 기사)에서 재 할당 가능한 명명 된 엔터티 (AKA 변수)의 사용을 비난하고 정당한 이유나 데이터없이 비난하는 경향이 있습니다. 내 책에 부당한 비난이 증오합니다. 그리고 증오는 정말 나쁜 코드입니다.
thiton

10
"변수의 증오"는 인과적인 과잉 단순화입니다. en.wikipedia.org/wiki/Fallacy_of_the_single_cause 자바에서 발생할 수있는 상태 비 저장 프로그래밍에는 많은 이점이 있지만, Java에서는 비용이 너무 복잡하다는 답변에 동의합니다 프로그램과 비 관념적입니다. 나는 무국적 프로그래밍이 좋고 상태가 좋다는 생각을 손으로 around 아 먹지 않을 것입니다.
Jimmy Hoffa

2
@JimmyHoffa가 말한 내용과 함께 필자는 명령형 언어 (그의 경우 C ++)의 기능 스타일 프로그래밍 주제에 대해 John Carmack을 언급합니다 ( altdevblogaday.com/2012/04/26/functional-programming-in-c ).
Steven Evers

5
불합리한 정죄는 증오가 아니며 변경 가능한 상태를 피하는 것은 불합리하지 않습니다.
Michael Shaw

21

재귀로 할 수있는 가장 간단한 것은 하나의 매개 변수가있는 함수입니다. Java와 매우 다르지는 않지만 작동합니다.

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
실제로 Java 예제로 질문에 대답하려고 시도한 경우 +1
KChaloux 2019

코드 골프 스타일 프리젠 테이션을 위해 이것을 하향 투표 했지만 ( Mod notice 참조 )이 코드는 내가 좋아하는 답변 에서 작성된 문장과 완벽하게 일치하기 때문에 아래쪽 화살표를 누르도록 강요 할 수 없습니다 : "1) 돌연변이가 없음, 2) 사용 재귀 및 3) 루프 부족 "
gnat

3
@gnat :이 답변은 Mod 공지 전에 게시되었습니다. 나는 훌륭한 스타일을 추구하지 않았고, 단순성을 추구했으며, OP의 원래 질문을 만족 시켰습니다. 이 것을 설명하기위한 것입니다 자바와 같은 일을 할 수.
FrustratedWithFormsDesigner

@FrustratedWithFormsDesigner 확실히; 이것은 DVing에서 멈추지 않을 것입니다 ( 편집 하기 위해 편집 할 수 있어야하기 때문에 )- 마술을 한 놀랍도록 완벽하게 일치 합니다. 잘 했어요, 정말 잘 했어요, 상당히 교육적입니다-고맙습니다
gnat

16

귀하의 기능적 예에서는 squares-oftake기능이 어떻게 구현 되는지 알 수 없습니다 . 나는 자바 전문가는 아니지만, 우리가 이와 같은 문장을 가능하게하는 함수를 작성할 수 있다고 확신합니다 ...

squares_of(integers).take(25);

그렇게 크게 다르지 않습니다.


6
Nitpick : squares-of은 Java에서 유효한 이름이 아닙니다 ( squares_of그렇지만). 그러나 그렇지 않으면 기사의 예가 좋지 않음을 나타내는 좋은 지적입니다.

기사의 integer게으른 정수를 생성하고 take함수가에서 25 개의 squared-of숫자를 선택 한다고 생각합니다 integer. 즉, integer정수를 무한대로 게으르게 생성 하는 함수가 있어야 합니다.
OnesimusUnbound

(integer)함수 와 같은 것을 호출하는 것은 약간의 광기입니다 . 함수는 여전히 인수 를 값에 매핑하는 것입니다 . 그것은 (integer)함수가 아니라 단지 가치 라는 것이 밝혀졌습니다 . 하나는 심지어 말까지 갈 수 integerA는 변수 숫자의 무한 스트 렘에 바인딩됩니다.
Ingo

6

Java에서는 반복자를 사용 하여이 작업을 수행 할 수 있습니다 (무한 목록 부분). 다음 코드 샘플에서 Take생성자에 제공된 수는 임의로 높을 수 있습니다.

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());
    }
}

또는 체인 가능한 팩토리 메소드를 사용하는 경우 :

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

어디 SquaresOf, TakeIntegers확장Numbers

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
이것은 기능적인 것보다 OO 패러다임의 우월성을 보여줍니다. 적절한 OO 디자인을 사용하면 기능적 패러다임을 모방 할 수 있지만 기능적 스타일로 OO 패러다임을 모방 할 수는 없습니다.
m3th0dman

3
@ m3th0dman : 적절한 OO 디자인을 사용하면 문자열, 목록 및 / 또는 사전이있는 언어가 OO를 반으로 모방 할 수있는 것과 마찬가지로 FP를 반으로 모방 할 수 있습니다 . 범용 언어의 튜링 동등성은 충분한 노력을 기울이면 모든 언어가 다른 언어의 기능을 시뮬레이션 할 수 있음을 의미합니다.
cHao

in과 같은 Java 스타일 이터레이터 while (test.hasNext()) System.out.println(test.next())는 FP에서 아니오입니다. 반복자는 본질적으로 변경 가능합니다.
cHao

1
@cHao 나는 진정한 캡슐화 나 다형성이 모방 될 수 있다고 믿지 않는다. 또한 Java (이 예에서)는 엄격한 평가로 인해 기능 언어를 실제로 모방 할 수 없습니다. 또한 반복자는 재귀 적 방식으로 작성할 수 있다고 생각합니다.
m3th0dman 2019

@ m3th0dman : 다형성은 전혀 모방하기 어렵지 않습니다. C와 어셈블리 언어조차도 가능합니다. 메소드를 객체의 필드 또는 클래스 설명자 / vtable로 만드십시오. 데이터 숨김 의미의 캡슐화 가 반드시 필요한 것은 아닙니다. 언어의 절반은 그것을 제공하지 않습니다. 객체가 불변 할 때 사람들이 어쨌든 내장을 볼 수 있는지 여부는 중요하지 않습니다. 전술 한 방법 필드가 쉽게 제공 할 수있는 데이터 래핑 만 있으면된다 .
cHao

6

짧은 버전 :

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);
}

이것은 대부분 작동하지만 여전히 오버플로가 발생하기 쉽습니다. take20 억 정수를 시도 하고 그들에 대한 조치를 취하십시오. : 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_VALUEnull 과 같이 처리를 중지하기 때문에 카운트 와 같은 것으로 작동 합니다. 카운트는 대부분 거기에 있으므로 우리는 견고한 기본 사례를 갖습니다. Integer.MAX_VALUE목록에 여러 개 이상의 항목 이있을 것으로 예상되면 workWith의 반환 값을 확인할 수 있습니다 . 끝에 null이 있어야합니다. 그렇지 않으면 재귀하십시오.

명심하십시오, 이것은 당신이 말한만큼 많은 요소에 영향을 미칩니다. 게으르지 않습니다. 그것은 즉시 그 일을합니다. 당신은 행동 , 즉 목록의 모든 요소에 자신을 적용하는 것이 유일한 목적 을 위해서만하고 싶습니다 . 지금 생각하고 있듯이 선형으로 유지하면 시퀀스가 ​​훨씬 덜 복잡해 보입니다. 시퀀스는 어쨌든 스스로를 호출하지 않기 때문에 문제가되지 않아야합니다. 시퀀스를 다시 호출하는 객체를 생성하기 만합니다.


3

이전에 Java에서 리스프와 유사한 언어에 대한 인터프리터를 만들려고 시도했지만 (몇 년 전 sourceforge의 CVS에서와 같이 모든 코드가 손실되었습니다) Java util 반복자는 함수형 프로그래밍에 대해 조금 장황합니다. 목록에.

다음은 시퀀스 인터페이스를 기반으로 한 것입니다. 현재 인터페이스 값을 얻고 다음 요소에서 시작하는 데 필요한 두 가지 작업 만 있습니다. 이것들은 체계의 기능에 따라 head와 tail로 명명됩니다.

목록이 느리게 생성된다는 것을 의미하기 때문에 Seq또는 Iterator인터페이스와 같은 것을 사용하는 것이 중요합니다 . Iterator인터페이스는 매우 적은 함수형 프로그래밍에 적합, 불변의 객체가 될 수 없다 - 당신이 함수에 전달할 값이 그것에 의해 변경된 경우 알 수없는 경우, 당신은 함수형 프로그래밍의 주요 장점 중 하나 잃게됩니다.

분명히 integers모든 정수 목록이어야하므로 0에서 시작하여 양수와 음수를 교대로 반환했습니다.

두 가지 버전의 사각형이 있습니다. 하나는 사용자 정의 시퀀스를 생성하고 다른 하나 map는 '함수'를 사용합니다 .Java 7에는 람다가 없으므로 인터페이스를 사용하여 차례로 시퀀스의 각 요소에 적용합니다.

square ( int x )함수 의 요점은 head()두 번 호출 할 필요를 제거하는 것입니다. 일반적으로 값을 최종 변수에 넣어서이 작업을 수행했지만이 함수를 추가하면 프로그램에 변수가 없으며 함수 매개 변수 만 있음을 의미합니다.

이런 종류의 프로그래밍에 대한 Java의 자세한 내용으로 인해 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

가변 변수를 사용하지 않고 유용한 Java 프로그램 을 작성하는 방법

이론적으로는 재귀와 가변 변수를 사용하여 Java의 거의 모든 것을 구현할 수 있습니다.

실제로:

  • Java 언어는이를 위해 설계되지 않았습니다. 많은 구성체는 돌연변이를 위해 고안되었으며, 그것 없이는 사용하기 어렵다. 예를 들어, 가변 길이 Java 배열을 변형없이 초기화 할 수 없습니다.

  • 도서관을 위해 Ditto. 그리고 덮개 아래에서 돌연변이를 사용하지 않는 라이브러리 클래스로 자신을 제한하면 훨씬 더 어려워집니다. (String을 사용할 수도 없습니다 ... hashcode구현 방법을 살펴보십시오 .)

  • 주류 Java 구현은 테일 콜 최적화를 지원하지 않습니다. 이는 재귀 버전의 알고리즘이 스택 공간 "배고픈"경향이 있음을 의미합니다. Java 스레드 스택이 커지지 않기 때문에 큰 스택을 미리 할당해야합니다 StackOverflowError.

이 세 가지를 결합하면 Java는 실제로 가변 변수없이 유용한 (즉, 사소한) 프로그램 을 작성하기위한 실용적인 옵션 이 아닙니다 .

(그러나 이것도 괜찮습니다. JVM에 사용 가능한 다른 프로그래밍 언어가 있으며, 그 중 일부는 기능 프로그래밍을 지원합니다.)


2

개념의 예를 찾고있을 때, Java를 제외하고 익숙한 개념 버전을 찾을 수있는 다른 아직 익숙한 설정을 찾아 보자. UNIX 파이프는 체인 지연 기능과 다소 유사합니다.

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

리눅스에서 이것은 내가 식욕을 잃을 때까지 각 바이트가 참 비트가 아닌 거짓으로 구성된 바이트를 제공한다는 것을 의미합니다. 각 바이트를 개행 문자로 변경하십시오. 이렇게 만들어진 각 줄에 번호를 매기십시오. 그 숫자의 제곱을 생성합니다. 또한 나는 25 줄 이상에 대한 식욕을 가지고 있습니다.

나는 프로그래머가 그런 방식으로 리눅스 파이프 라인을 작성하는 것은 좋지 않다고 주장한다. 비교적 일반적인 Linux 셸 스크립팅입니다.

나는 프로그래머가 Java에서 똑같은 것을 작성하려고 시도하는 것이 좋지 않다고 주장한다. 그 이유는 소프트웨어 프로젝트의 수명 비용의 주요 요소 인 소프트웨어 유지 관리 때문입니다. 우리는 표면 상으로 Java 프로그램을 제시함으로써 다음 프로그래머를 혼란스럽게 만들고 싶지는 않지만 실제로 Java 플랫폼에 이미 존재하는 기능을 정교하게 복제함으로써 맞춤형 일회성 언어로 실제로 작성됩니다.

다른 한편으로, 우리의 "자바"패키지 중 일부가 실제로 Clojure 및 Scala와 같은 기능적 또는 객체 / 기능적 언어 중 하나로 작성된 Java Virtual Machine 패키지 인 경우 다음 프로그래머가 더 수용 할 수 있다고 주장합니다. 이들은 함수를 체인으로 연결하여 코딩하고 일반적인 Java 메소드 호출 방식으로 Java에서 호출되도록 설계되었습니다.

그런 다음에도 Java 프로그래머가 여전히 기능적 프로그래밍에서 영감을 얻는 것이 좋습니다.

최근 내가 좋아하는 기술은 변경 불가능하고 초기화되지 않은 반환 변수와 단일 종료를 사용하여 일부 함수형 언어 컴파일러가하는 것처럼 Java는 함수 본문에서 무슨 일이 있어도 항상 하나만 제공한다는 것을 확인합니다. 반환 값. 예:

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;
}


어쨌든 Java가 이미 반환 값 검사를 수행한다고 확신합니다. 제어가 무효가 아닌 함수의 끝에서 떨어질 수있는 경우 "missing return statement"에 대한 오류가 발생합니다.
cHao

내 요점 : int result = -n; if (n < 1) { result = 0 } return result;잘 컴파일 할 때 코드를 작성 하면 컴파일러는 내 예제의 함수와 동등한 지 여부를 알지 못합니다. 어쩌면 그 예제가 기술을 유용하게 보이기에는 너무 간단 할 수도 있지만, 많은 가지가있는 함수에서 어떤 경로를 따라야하는지에 관계없이 결과가 정확히 한 번 할당된다는 것을 분명히하는 것이 좋습니다.
minopret

당신이 말하면 if (n < 1) return 0; else return -n;, 당신은 문제없이 끝납니다 ... 그리고 그것은 더 간단합니다. :)이 경우 "한 번의 반환"규칙은 실제로 반환 값이 언제 설정 되었는지 알지 못하는 문제를 야기 하는 데 도움 이됩니다. 그렇지 않으면 그냥 반환 할 수 있고 Java는 다른 경로가 값을 반환하지 않을 때를 더 잘 결정할 수 있으므로 실제 반환 값과 더 이상 값 계산을 나누지 않습니다.
cHao

또는 답변의 예를 들어 if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;.
cHao

방금 Java에서 OnlyOneReturn 규칙을 지키기 위해 더 이상 인생을 보내고 싶지 않다고 결정했습니다. 간다. 함수형 프로그래밍 방식의 영향을받는 Java 코딩 방식을 생각할 때이 예제를 대체 할 것입니다. 그때까지는 예가 없습니다.
minopret

0

가장 쉬운 방법은 Frege 컴파일러에 다음을 공급 하고 생성 된 Java 코드를 보는 것입니다.

module Main where

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

며칠 후 나는 내 생각이이 답변으로 돌아 오는 것을 발견했다. 내 제안의 모든 부분 후에 스칼라에서 함수형 프로그래밍 부분을 구현하는 것이 었습니다. 우리가 정말 마음에 하스켈했다 어디 우리가 그 장소에서 스칼라 적용을 고려해 (그리고 내가 유일한 사람이 아니에요 생각한다면 blog.zlemma.com/2013/02/20/... 우리는 안된다) 적어도 프레게을 고려?
minopret

@minopret 이것은 Frege가 틈새 시장을 겨냥한 것입니다. Haskell을 알고 사랑하지만 JVM이 필요한 사람들. 나는 언젠가 Frege가 적어도 진지한 고려를 할만큼 충분히 성숙 할 것이라고 확신한다.
Ingo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.