OO 프로그램을 기능적으로 재구성하는 방법은 무엇입니까?


26

기능적인 스타일로 프로그램을 작성하는 방법에 대한 리소스를 찾는 데 어려움을 겪고 있습니다. 온라인에서 논의 할 수있는 가장 고급 주제는 구조적 타이핑을 사용하여 클래스 계층 구조를 줄이는 것이 었습니다. 대부분은 map / fold / reduce / etc를 사용하여 명령형 루프를 대체하는 방법을 처리합니다.

내가 정말로 찾고 싶은 것은 사소하지 않은 프로그램의 OOP 구현, 그 한계 및 기능적 스타일로 리팩토링하는 방법에 대한 심도있는 토론입니다. 알고리즘이나 데이터 구조뿐만 아니라 비디오 게임과 같은 여러 가지 역할과 측면을 가진 것입니다. 그건 그렇고 Tomas Petricek의 Real-World Functional Programming을 읽었지만 더 많이 원합니다.


6
나는 그것이 가능하지 않다고 생각합니다. 모든 것을 다시 디자인하고 다시 작성해야합니다.
Bryan Chen

18
-1,이 게시물은 OOP와 기능적 스타일이 반대라는 잘못된 가정에 의해 편향되어 있습니다. 그것들은 대부분 직교 개념이며, IMHO는 그렇지 않다는 신화입니다. "기능"은 "절차"에 더 반대되며 OOP와 함께 두 스타일을 모두 사용할 수 있습니다.
Doc Brown

11
@DocBrown, OOP는 변경 가능한 상태에 너무 의존합니다. 상태 비 저장 개체는 현재 OOP 디자인 방식에 적합하지 않습니다.
SK-logic

9
@ SK-logic : 키는 상태 비 저장 개체가 아니라 변경할 수없는 개체입니다. 그리고 객체가 변경 가능하더라도 주어진 컨텍스트 내에서 변경되지 않는 한 시스템의 기능 부분에서 종종 사용할 수 있습니다. 또한 객체와 클로저가 상호 교환 가능하다는 것을 알고 있습니다. 따라서 이것은 OOP와 "기능적"이 반대가 아님을 보여줍니다.
Doc Brown

12
@ DocBrown : 나는 언어 구조가 직교라고 생각하지만 사고 방식은 충돌하는 경향이 있습니다. OOP 사람들은 "개체가 무엇이며 어떻게 협업합니까?" 기능적인 사람들은 "내 데이터가 무엇이며 어떻게 변환하고 싶습니까?"라고 묻는 경향이 있습니다. 그것들은 같은 질문이 아니며, 다른 답변으로 이어집니다. 또한 질문을 잘못 읽은 것 같습니다. "OOP drools 및 FP 규칙이 아니라 OOP를 제거하는 방법은 무엇입니까?", "OOP를 얻었고 FP를 얻지 못합니다. OOP 프로그램을 기능적인 것으로 변환 할 수있는 방법이 있습니까? 통찰력? "
Michael Shaw

답변:


31

함수형 프로그래밍의 정의

클로저의 기쁨에 대한 소개 는 다음과 같습니다.

함수형 프로그래밍은 무정형 정의를 갖는 컴퓨팅 용어 중 하나입니다. 100 명의 프로그래머에게 그들의 정의를 요청하면 100 개의 다른 답변을받을 것입니다 ...

함수형 프로그래밍은 함수의 적용 및 구성을 용이하게합니다. 언어가 기능적으로 간주 되려면 함수 개념이 일류 여야합니다. 일류 함수는 다른 데이터와 마찬가지로 저장, 전달 및 반환 될 수 있습니다. 이 핵심 개념 이외에도, FP 정의에는 순도, 불변성, 재귀, 게으름 및 참조 투명성이 포함될 수 있습니다.

Scala 2 판에서의 프로그래밍 p. 10의 정의는 다음과 같습니다.

함수형 프로그래밍은 두 가지 주요 아이디어로 안내됩니다. 첫 번째 아이디어는 함수가 일류 값이라는 것입니다 ... 함수를 다른 함수에 인수로 전달하거나 함수의 결과로 반환하거나 변수에 저장할 수 있습니다 ...

함수형 프로그래밍의 두 번째 주요 아이디어는 프로그램 작업에서 데이터를 변경하지 않고 입력 값을 출력 값에 매핑해야한다는 것입니다.

우리가 첫 번째 정의를 받아 들인다면, 코드를 "기능적으로"만들기 위해해야 ​​할 일은 루프를 안쪽으로 돌리는 것입니다. 두 번째 정의에는 불변성이 포함됩니다.

퍼스트 클래스 기능

현재 버스 오브젝트에서 승객 목록을 가져 와서 버스 요금에 따라 각 승객의 은행 계좌를 줄인다고 가정 해보십시오. 이와 동일한 동작을 수행하는 기능적인 방법은 버스에 메소드를 두는 것입니다. 한 인수의 함수를 취하는 forEachPassenger라고도합니다. 그런 다음 버스는 승객을 반복 처리하지만 가장 잘 수행되며 승차 요금을 청구하는 고객 코드가 기능을 수행하여 forEachPassenger로 전달됩니다. 짜잔! 함수형 프로그래밍을 사용하고 있습니다.

피할 수 없는:

for (Passenger p : Bus.getPassengers()) {
    p.debit(fare);
}

기능적 (스칼라에서 익명 함수 또는 "람다"사용) :

myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })

더 달콤한 스칼라 버전 :

myBus = myBus.forEachPassenger(_.debit(fare))

일류가 아닌 함수

만약 당신의 언어가 일류 함수를 지원하지 않는다면, 이것은 매우 추악해질 수 있습니다. Java 7 이하에서는 다음과 같은 "기능 객체"인터페이스를 제공해야합니다.

// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
    public void accept(T t);
}

그런 다음 Bus 클래스는 내부 반복자를 제공합니다.

public void forEachPassenger(Consumer<Passenger> c) {
    for (Passenger p : passengers) {
        c.accept(p);
    }
}

마지막으로 익명 함수 객체를 버스에 전달합니다.

// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
    @Override
    public void accept(final Passenger p) {
        p.debit(fare);
    }
}

Java 8에서는 로컬 변수를 익명 함수의 범위로 캡처 할 수 있지만 이전 버전에서는 이러한 변수를 final로 선언해야합니다. 이 문제를 해결하려면 MutableReference 래퍼 클래스를 만들어야합니다. 위 코드에 루프 카운터를 추가 할 수있는 정수 별 클래스는 다음과 같습니다.

public static class MutableIntWrapper {
    private int i;
    private MutableIntWrapper(int in) { i = in; }
    public static MutableIntWrapper ofZero() {
        return new MutableIntWrapper(0);
    }
    public int value() { return i; }
    public void increment() { i++; }
}

final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
    @Override
    public void accept(final Passenger p) {
        p.debit(fare);
        count.increment();
    }
}

System.out.println(count.value());

이러한 추악함에도 내부 반복자를 제공하여 프로그램 전체에 퍼져있는 루프에서 복잡하고 반복되는 논리를 제거하는 것이 유리합니다.

이 못생긴 점은 Java 8에서 수정되었지만 1 급 함수 내에서 확인 된 예외를 처리하는 것은 여전히 ​​정확하지 않으며 Java는 여전히 모든 컬렉션에서 변경 가능성을 가정합니다. 이는 종종 FP와 관련된 다른 목표를 가져옵니다.

불변성

Josh Bloch의 항목 13은 "불변성을 선호합니다"입니다. 일반적인 쓰레기 이야기와 반대로 OOP는 불변의 물건으로 수행 할 수 있으므로 그렇게하는 것이 훨씬 좋습니다. 예를 들어, Java의 문자열은 변경할 수 없습니다. StringBuffer, OTOH는 변경 불가능한 문자열을 빌드하기 위해 변경 가능해야합니다. 버퍼 작업과 같은 일부 작업에는 본질적으로 변경이 필요합니다.

청정

각 함수는 최소한 메모 가능해야합니다. 동일한 입력 매개 변수를 제공하고 (실제 인수 외에 입력이 없어야하는 경우) 전역 상태 변경과 같은 "부작용"을 유발하지 않고 매번 동일한 출력을 생성해야합니다. / O 또는 예외 발생

Functional Programming에서 "일을하기 위해서는 보통 악이 필요합니다." 100 % 순도는 일반적으로 목표가 아닙니다. 부작용을 최소화하는 것입니다.

결론

실제로 위의 모든 아이디어 중에서 불변성은 OOP 또는 FP 등 내 코드를 단순화하기위한 실용적인 응용 프로그램 측면에서 가장 큰 승리였습니다. 반복자에게 함수를 전달하는 것이 두 번째로 큰 승리입니다. 자바 8 주면서 문서는 왜 최선의 설명이 있습니다. 재귀는 트리 처리에 좋습니다. 게으름을 사용하면 무한한 컬렉션으로 작업 할 수 있습니다.

JVM이 마음에 들면 Scala와 Clojure를 살펴 보는 것이 좋습니다. 둘 다 함수형 프로그래밍에 대한 통찰력있는 해석입니다. 스칼라는 C와 비슷한 문법을 ​​사용하지만 형식이 안전하지만 C와 마찬가지로 Haskell과 공통으로 많은 구문을 가지고 있습니다. Clojure는 형식이 안전하지 않으며 Lisp입니다. 최근에 하나의 특정 리팩토링 문제와 관련하여 Java, Scala 및 Clojure비교를 게시했습니다 . Logan Campbell의 Game of Life를 사용한 비교 에는 Haskell과 Typed Clojure도 포함됩니다.

추신

지미 호파 (Jimmy Hoffa)는 나의 버스 클래스가 변경 가능하다고 지적했다. 원본을 수정하는 대신이 질문에 대한 리팩터링의 종류를 정확하게 보여줄 것이라고 생각합니다. 이는 버스의 각 방법을 공장으로 만들어 새 버스를 생성하고 승객의 각 방법을 공장에서 새 승객을 생성하도록하여 해결할 수 있습니다. 따라서 모든 것에 반환 유형을 추가했습니다. 이는 Consumer 인터페이스 대신 Java 8의 java.util.function.Function을 복사한다는 것을 의미합니다.

public interface Function<T,R> {
    public R apply(T t);
    // Note: I'm leaving out Java 8's compose() method here for simplicity
}

그런 다음 버스에서 :

public Bus mapPassengers(Function<Passenger,Passenger> c) {
    // I have to use a mutable collection internally because Java
    // does not have immutable collections that return modified copies
    // of themselves the way the Clojure and Scala collections do.
    List<Passenger> newPassengers = new ArrayList(passengers.size());
    for (Passenger p : passengers) {
        newPassengers.add(c.apply(p));
    }
    return Bus.of(driver, Collections.unmodifiableList(passengers));
}

마지막으로, 익명 함수 객체는 수정 된 사물 상태 (새로운 승객이있는 새로운 버스)를 반환합니다. 이것은 p.debit ()가 원래보다 돈이 적은 새로운 불변 ​​승객을 반환한다고 가정합니다.

Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
    @Override
    public Passenger apply(final Passenger p) {
        return p.debit(fare);
    }
}

바라건대 이제 명령형 언어를 얼마나 기능적으로 만들고 싶은지 스스로 결정하고 기능적 언어를 사용하여 프로젝트를 재 설계하는 것이 더 나은지 결정할 수 있기를 바랍니다. Scala 또는 Clojure에서 컬렉션 및 기타 API는 기능적 프로그래밍을 쉽게하도록 설계되었습니다. 둘 다 Java interop이 매우 뛰어나므로 언어를 혼합하고 일치시킬 수 있습니다. 실제로 Java 상호 운용성을 위해 Scala는 첫 번째 클래스 함수를 Java 8 기능 인터페이스와 거의 호환되는 익명 클래스로 컴파일합니다. Scala in Depth sect 의 세부 정보를 읽을 수 있습니다 . 1.3.2 .


이 답변의 노력, 조직 및 명확한 의사 소통에 감사드립니다. 그러나 일부 기술에는 약간의 문제가 있습니다. 상단 근처에 언급 된 키 중 하나는 함수의 구성입니다. 이는 객체 내부의 함수를 크게 캡슐화하는 것이 목적을 달성하지 못하는 이유로 되돌아갑니다. 함수가 객체 내부에있는 경우 해당 객체에 대해 작동해야합니다. 그것이 그 객체에 작용하면 내부를 변경해야합니다. 이제 모든 사람이 참조 투명성 또는 불변성을 요구하지는 않지만 용서할 것입니다. 그러나 객체를 제자리에 변경하면 더 이상 반환 할 필요가 없습니다.
Jimmy Hoffa

그리고 함수가 값을 반환하지 않으면, 갑자기 함수를 다른 함수로 구성 할 수 없으며, 기능 구성의 모든 추상화를 잃게됩니다. 함수가 객체를 제 위치에서 변경 한 다음 객체를 반환하도록 할 수는 있지만이 작업을 수행하는 경우 함수를 사용하여 객체를 매개 변수로 사용하여 부모 객체의 경계에서 벗어날 수 있습니까? 부모 객체에서 해방되면 다른 유형에서도 작동 할 수 있습니다 .이 유형은 누락 된 FP의 또 다른 중요한 부분입니다. 당신의 forEachPasenger는 ... 승객에 대해 작동합니다
지미 호파에게

1
매핑하고 줄일 대상을 추상화하고 이러한 함수가 객체를 포함하지 않는 이유는 파라 메트릭 다형성을 통해 무수한 유형에 사용할 수 있기 때문입니다. FP를 실제로 정의하고 가치를 갖도록하는 OOP 언어에서는 찾을 수없는 이러한 다양한 추상화의 혼동입니다. 게으름, 참조 투명성, 불변성 또는 HM 유형 시스템이 FP를 작성하는 데 필요한 것은 아닙니다. 이러한 것들은 함수가 일반적으로 유형에 대해 추상화 할 수있는 기능 구성에 적합한 언어를 작성하는 데 따른 부작용입니다.
Jimmy Hoffa

@JimmyHoffa 당신은 나의 모범을 매우 공평하게 비난했습니다. Java8 Consumer 인터페이스를 통해 변경 가능성에 매료되었습니다. 또한 FP의 chouser / fogus 정의에는 불변성이 포함되어 있지 않으며 나중에 Odersky / Spoon / Venners 정의를 추가했습니다. 원래 예를 남겼지 만 하단의 "PS"섹션에 새로운 불변 ​​버전을 추가했습니다. 못 생겼어 그러나 원본의 내부를 변경하지 않고 새로운 객체를 생성하기 위해 객체에 작용하는 기능을 보여줍니다. 좋은 의견!
GlenPeterson

1
이 대화는 화이트 보드에서 계속됩니다 : chat.stackexchange.com/transcript/message/11702383#11702383
GlenPeterson

12

나는 이것을 "완료"하는 개인적인 경험이 있습니다. 결국, 순전히 기능적인 것을 생각해 내지 못했지만, 내가 좋아하는 것을 생각해 냈습니다. 내가 한 방법은 다음과 같습니다.

  • 모든 외부 상태를 함수의 매개 변수로 변환하십시오. EG : 객체의 메소드가 수정 x되면 x을 호출 하는 대신 메소드가 전달되도록하십시오 this.x.
  • 객체에서 동작을 제거하십시오.
    1. 개체의 데이터를 공개적으로 액세스 할 수 있도록합니다
    2. 모든 메소드를 오브젝트가 호출하는 함수로 변환하십시오.
    3. 객체를 호출하는 클라이언트 코드가 객체 데이터를 전달하여 새 함수를 호출하도록합니다. EG : 변환 x.methodThatModifiesTheFooVar()fooFn(x.foo)
    4. 객체에서 원래 방법을 제거
  • 당신이 할 수 주문 기능이 좋아하는 더 많은 반복 루프로 교체 map, reduce, filter, 등

변경 가능한 상태를 제거 할 수 없습니다. 내 언어 (JavaScript)에서는 너무 비 관용적이었습니다. 그러나, 모든 상태가 전달 및 / 또는 반환하여, 모든 기능을 테스트 할 수 있습니다. 상태를 설정하는 데 너무 오래 걸리거나 종속성을 분리하려면 프로덕션 코드를 먼저 수정해야하는 OOP와 다릅니다.

또한 정의에 대해 틀릴 수 있지만 함수가 참조 적으로 투명하다고 생각합니다. 함수는 동일한 입력으로 동일한 효과를 갖습니다.

편집하다

여기 에서 볼 수 있듯이 JavaScript로 불변의 객체를 만들 수는 없습니다. 부지런하고 코드를 호출하는 사람을 제어하는 ​​경우 현재 객체를 변경하지 않고 항상 새 객체를 작성하여 수행 할 수 있습니다. 노력할 가치가 없었습니다.

그러나 Java 를 사용하는 경우 이러한 기술 을 사용하여 클래스를 변경할 수 없습니다.


+1 정확히 무엇을하려고하는지에 따라, 이것은 단순히 "리팩토링"을 넘어서는 디자인 변경없이 실제로 갈 수있는 한 가능할 것입니다.
Evicatos

@Evicatos : JavaScript가 불변 상태를 더 잘 지원한다면 Clojure와 같은 동적 기능 언어에서 얻을 수있는 것처럼 솔루션이 기능적이라고 생각합니다. 리팩토링 이외의 것을 필요로하는 것의 예는 무엇입니까?
Daniel Kaplan

변경 가능한 상태를 제거하면 자격이 있다고 생각합니다. 나는 그것이 언어에 대한 더 나은 지원의 문제라고 생각하지 않습니다. 나는 가변에서 불변으로 갈 때 기본적으로 항상 재 작성을 구성하는 근본적인 아키텍처 변경이 필요하다고 생각합니다. 그래도 리팩토링 정의에 따라 Ymmv.
Evicatos

@Evicatos 내 편집 내용보기
Daniel Kaplan

1
@tieTYT 그렇습니다. JS가 그렇게 변경 가능하다는 것은 슬픈 일이지만 적어도 Clojure는 JavaScript로 컴파일 할 수 있습니다. github.com/clojure/clojurescript
GlenPeterson

3

프로그램을 완전히 리팩토링하는 것이 실제로 불가능하다고 생각합니다. 올바른 패러다임에서 다시 디자인하고 다시 구현해야합니다.

코드 리팩토링이 "기존 코드를 재구성하고 외부 동작을 변경하지 않고 내부 구조를 변경하는 기술"로 정의 된 것을 보았습니다.

특정 기능을보다 기능적으로 만들 수는 있지만 그 핵심에는 여전히 객체 지향 프로그램이 있습니다. 작은 비트와 조각을 변경하여 다른 패러다임에 적응시킬 수는 없습니다.


첫 번째 좋은 점은 참조 투명성을 위해 노력하는 것입니다. 이 기능을 사용하면 함수형 프로그래밍의 이점 중 ~ 50 %를 얻을 수 있습니다.
Daniel Gratzer

3

나는이 일련의 기사들이 정확히 당신이 원하는 것이라고 생각한다 :

순전히 기능적인 레트로 게임

http://prog21.dadgum.com/23.html 1 부

http://prog21.dadgum.com/24.html 2 부

http://prog21.dadgum.com/25.html 3 부

http://prog21.dadgum.com/26.html 4 부

http://prog21.dadgum.com/37.html 후속 조치

요약은 다음과 같습니다.

저자는 부작용이있는 메인 루프 (부작용은 어딘가에 발생해야합니까?)를 제안하고 대부분의 함수는 게임 상태를 어떻게 변경했는지에 대한 작은 불변 레코드를 반환합니다.

물론 실제 프로그램을 작성할 때는 가장 도움이되는 각 스타일을 사용하여 여러 프로그래밍 스타일을 혼합하고 일치시킵니다. 그러나 가장 기능적 / 불변의 방식으로 프로그램을 작성하고 전역 변수 만 사용하여 가장 스파게티 방식으로 작성하는 것이 좋은 학습 경험입니다 :-) (생산이 아닌 실험으로하십시오)


2

OOP와 FP에는 코드 구성에 대한 두 가지 상반된 접근 방식이 있으므로 모든 코드를 안쪽으로 뒤집어 야합니다.

OOP는 유형 (클래스)을 중심으로 코드를 구성합니다. 다른 클래스는 동일한 작업을 수행 할 수 있습니다 (서명이 동일한 메소드). 결과적으로 작업 집합이 많이 변경되지 않고 새 유형을 자주 추가 할 수있는 경우 OOP가 더 적합합니다. 예를 들어, 각 위젯의 고정 방법들을 갖고있는 GUI 라이브러리를 고려 ( hide(), show(), paint(), move(), 등) 그러나 라이브러리 확장으로 새로운 위젯이 추가 될 수있다. OOP에서는 새로운 인터페이스를 쉽게 추가 할 수 있습니다 (주어진 인터페이스) : 새로운 클래스를 추가하고 모든 메소드 (로컬 코드 변경) 만 구현하면됩니다. 반면에 인터페이스에 새 작업 (메소드)을 추가하려면 상속이 작업량을 줄일 수 있음에도 불구하고 해당 인터페이스를 구현하는 모든 클래스를 변경해야 할 수 있습니다.

FP는 작업 (기능) 주위에 코드를 구성합니다. 각 기능은 다른 유형으로 다른 방식으로 처리 할 수있는 일부 작업을 구현합니다. 이것은 일반적으로 패턴 일치 또는 다른 메커니즘을 통해 유형을 디스패치함으로써 달성됩니다. 결과적으로, 유형 집합이 안정적이고 새로운 작업이 더 자주 추가 될 때 FP가 더 적합합니다. 고정 된 이미지 형식 세트 (GIF, JPEG 등)와 구현하려는 일부 알고리즘을 예로 들어 보겠습니다. 각 알고리즘은 이미지 유형에 따라 다르게 동작하는 기능으로 구현할 수 있습니다. 새로운 기능 (로컬 코드 변경) 만 구현하면되기 때문에 새로운 알고리즘을 추가하는 것은 쉽습니다. 새 형식 (유형)을 추가하려면 지금까지 구현 한 모든 기능을 수정하여 지원하지 않아야합니다 (로컬이 아닌 변경).

결론 : OOP와 FP는 코드를 구성하는 방식이 근본적으로 다릅니다. OOP 디자인을 FP 디자인으로 변경하면이를 반영하기 위해 모든 코드를 변경해야합니다. 그래도 흥미로운 운동이 될 수 있습니다. mikemay가 인용 한 SICP 서적에 대한 이 강의 노트 , 특히 슬라이드 13.1.5-13.1.10을 참조하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.