Jump to content
과거의 기술자료(읽기 전용): https://tech.devgear.co.kr ×
과거의 기술자료(읽기 전용): https://tech.devgear.co.kr
  • [DelphiCon 요약] 델파이로 함수형 프로그래밍하기


    Kori
     공유하기

    << DelphiCon 2020 목록으로 이동

    DelphiCon 의 2020 시리즈 중, Functional Programming With Delphi - Nick Hodges 의 한글 요약본입니다.  

    • 프로그래밍 방식에 대한 새로운 관점을 가질 수 있는 세션입니다.
    • 함수형 프로그래밍은 지금까지 일반적으로 해온 명령형 프로그래밍보다 더 간결 명료하고, 멀티 쓰레드에서도 안전한 코드를 실현합니다.
    • 발표자 (Nick Hodges)는 델파이 개발자라면 한번쯤 들어본 저명한 델파이 전문가입니다.
    • 원본 비디오에는 아래 요약보다 자세한 데모와 설명이 있습니다. 

    목차


    상태의 실패(The Failure of State)

    함수형 프로그래밍 강연에서 Robert C. Martin는 "상태는 변하고, 이런 변화는 프로그램에 영향을 끼친다"고 설명한다.

    • 변수의 값은 변한다. 변수 값의 변화를 추적할 수 없으면 코드를 이해하기 어렵다.
    • 메소드 호출 사이의 관계를 파악하지 못하면 코드의 흐름을 따라가기가 어렵다.
    • 특히, 멀티 쓰레드를 사용하는 경우, 상태가 변화가 발생하는 코드를 이해하거나 디버깅하기는 더 어렵다.

    불변 (Immutable) 타입을 사용하자

    • 불변 클래스는 (생성자에서) 한번만 값을 검증하면 된다. 이후에는 변경되지 않기 때문이다.
      • 예를 들어, 이메일에 값이 들어있는지 아닌지 등을 확인할 때 생성자 호출 코드만 보면 된다.
    • 불변 클래스는 한번 만들어지면 없어질 때까지 상태가 변하지 않으므로 상태 변경을 걱정할 필요가 없다.
    • 서로 다른 쓰레드에서 함께 사용되어도 안전하다.
    • 코드 읽기가 더 쉽다.
      • 상태 변화를 추적하기 위해 메소드를 하나씩 추적할 필요가 없다.

    가변 클래스와 불변 클래스 예시

    // [1] 일반적인 클래스 타입 (내부 데이터 변경 허용, 멀티 쓰레드에서 안전하지 않음)
    type
      TMutablePerson = class
      private
        FName: string;
        Email: string;
        procedure SetName(const Value: string);
        procedure SetEmail(const Value: string);
      public
        constructor Create(const aName: string; const aEmail: string);
        property Name: string read Name write SetName; //프로퍼티에서 쓰기가 가능하므로 이름을 바꿀 수 있다
        property Email: string read Email write SetEmail; //프로퍼티에서 쓰기가 가능하므로 이메일을 바꿀 수 있다
      end;
    
    // [2] 불변 클래스 타입 (내부 데이터 변경 불가. 멀티 쓰레드에서도 안전)
    type
      TImmutablePerson = class
      private
        FName: string;
        Email: string;
      public
        constructor Create(const aName: string; const aEmail: string);
        property Name: string read Name; //생성된 후에는 외부에서 읽기만 가능
        property Email: string read Email; //생성된 후에는 외부에서 읽기만 가능
      end;
    
    // [3] 불변 클래스 타입 (내부 데이터 변경을 허용하지만, 실제로는 새 인스턴스를 제공하므로 멀티 쓰레드에서도 안전)
    type
      TPerson = class
      private
        FName: string;
        Email: string;
      public
        constructor Create(const aName: string; const aEmail: string);
        function ChangeName(const Value: string): TPerson; //데이터가 변하지만, 실제로 새 인스턴스를 반환
        function ChangeEmail(const Value: string): TPerson; //데이터가 변하지만, 실제로 새 인스턴스를 반환
        property Name: string read Name; //생성된 후에는 외부에서 읽기만 가능
        property Email: string read Email; //생성된 후에는 외부에서 읽기만 가능
      end;
    
    function TPerson.ChangeEmail (const aEmail: string) : TPerson;
    begin
      Result := TPerson.Create(FName, aEmail); //기존 Person의 이메일이 바뀌는 대신 새 Person이 생긴다.
      // 참고로, 델파이에는 가비지 컬렉션이 없으므로, 기존의 오브젝트는 제거하여 메모리 누수를 방지할 필요가 있다.
    end;

    함수형 프로그래밍과 명령형 프로그래밍

    함수형(Functional) 프로그래밍이란?

    • 함수형 프로그래밍은 크게 주목받고 있다.
      • Haskell, Scala, Clojure, F#, Erlang, Groovy 등이 모두 함수형 프로그래밍 언어이다.
    • 함수형 프로그래밍으로 만든 프로그램에는 오로지 함수만 있다. 즉 모든 것이 함수이다.
    • 급부상 하는 이유
      • 저명한 프로그래머들이 함수형 프로그레밍을 하고 있다.
      • (순수 함수형 프로그래밍은) 멀티 쓰레드에서 안전하다.
      • 더 간결하고, 더 작성하기 쉽다. (간단한 함수 호출 코드로 기존의 긴 코드를 교체할 수 있다)
      • 테스트와 디버깅이 더 쉽다. (상태 변화를 추적할 필요가 없기 때문에)

    명령형(Imperative) 프로그래밍이란?

    • 컴퓨터 프로그램
      • 컴퓨터가 수행할 작업이 정의된 명령 세트
      • 컴퓨터에게 작업 지시를 하면 컴퓨터는 지시받은 대로 수행한다.
    • 우리가 코드를 작성하고 생각하는 익숙한 방식은 이른바 “명령형” 프로그래밍은
      • 어떻게(HOW) 수행할 것인지를 정해주는 방식이다.
      • “명령형” 프로그래밍은 상태를 변경하는 방식이다.

    함수형(Functional) 프로그래밍과 (우리에게 익숙한) 명령형(Imperative) 프로그래밍의 차이점

      명령형 (Imperative) 프로그래밍 함수형(Functional) 프로그래밍

    코드의 목적

    어떻게 (HOW) 하는 지를 정의

    무엇을 (WHAT)을 하는 지를 정의

    코드 작성

    순서대로 명령을 실행하는 "과정" 중심

    표현식의 "결과" 중심

    문제 해결 방식(예시)

    목록에서 다음 고객을 선택하고, 
    만약 청구할 금액이 있으면, 청구서를 발행한다.

    만약 목록에 고객이 더 있으면, 첫단계로 가서 반복한다.

    목록에서 청구할 금액이 있는 모든 고객에게 청구서를 발행한다.

    함수형 프로그래밍 이해하기

    먼저, 고정 관념에서 완전히 벗어날 필요가 있다.

    • “객체 지향은 움직이는 부분을 캡슐화함으로써 코드를 이해할 수 있도록 한다. 함수형 프로그래밍은 부분의 움직임을 최소화함으로써 코드를 이해할 수 있도록 한다” - 마이클 페더스 (Michael Feathers)
    • 디버깅을 해본 개발자라면, 움직이는 부분들이 너무 많고, 코드를 이해하려면 이 많은 움직임을 힘들게 쫓아다녀야 한다는 점을 잘 안다. 움직이는 부분이 최소화 된다면 훨씬 단순해 질 수 있다.

    함수형 프로그래밍: 메소드/함수의 요건

    • 함수형 프로그래밍에서 메소드/함수는
      • 참조 투명성이 확보된다. (즉, 참조하는 쪽에서 볼 때 ‘항상’ 같은 결과를 확신할 수 있다)
      • 즉, Method Signature가 언제나 정직하다. (즉, 인자가 같으면 항상 그 결과도 같다)
      • 예를 들어 메소드에 A를 전달하면 결과는 항상 B이다. (B가 아닌 결과는 절대 나올 수 없다)
    • [퀴즈] Assert.True (f(x), f(x))는 항상 ‘참’인가?
      • 항상 ‘참’이라고 보장할 수 없다. 각 f(x)의 결과가 다를 수 있고, 그러면 ‘거짓’일 수도 있기 때문이다.
      • 항상 ‘참’이라고 보장하려면, f(x)의 결과가 항상 같다고 보장되어야 한다. 즉 f(x)는 “참조 투명성”이 확보된 순수한 함수여서 상태가 전혀 변하지 않아야 한다.

    참조 투명성이 확보된 함수와 그렇지 않은 함수의 차이

    // 호출되는 시점에 따라 결과가 달라지는 함수
    function GetElapseTime (const aThen: TDateTime): TTimeSpan;
    begin
      Result := Now - aThen; //같은 값을 받아도 호출되는 시점에 따라, Now의 값이 다르기 때문에, 결과도 달라진다.
    end;
    
    // 호출되는 시점과 관계없이 항상 결과가 같은 함수
    function sGetElapseTime (const aThen, aNow: TDateTime): TTimeSpan;
    begin
      Result := aNow - aThen; //받은 값으로만 계산하므로, 같은 값을 전달하면 결과도 항상 같다.
    end;

    함수형 프로그래밍: 함수의 특징

    • 함수형 언어의 빌딩 블록이다.
    • 완전히 캡슐화되어 있다. (일종의 블랙박스이다)
    • 사전에 확실히 정해진다(순수하다)
      • 참조 투명성이 보장된다
      • 즉, 인자가 같으면 결과도 항상 같다.
      • 상태 변경을 하지 않는다. 부작용이 없다. 전역 상태값을 참조하지 않는다.
    • 교환 가능하다. 즉, 함수의 실행 순서가 바뀌어도, 결과가 바뀌지 않는다.
      • 명령형 프로그래밍에서는 함수 간의 호출 순서가 중요해서 주석을 남기기도 하는데 그럴 필요 없다.

    함수형 프로그래밍: 함수 (매우 중요!)

    함수형 프로그래밍에서 함수는 가장 중요한 지위를 가진다.

    • 추상화의 기본 단위
    • 함수 자체가 인자(Parameter)가 되어 전달될 수 있다.
    • 함수 자체가 결과값으로 반환될 수 있다.
    • 결과를 해당 함수로 교체할 수 있다.
    • 함수는 다른 언어 생성자가 보일 수 있는 곳이면 어디든지 보인다. (함수형 프로그래밍 언어에서 함수는 일종의 루트 타입이다)
    • 부작용이 없다: 멀티 쓰레드에서 안전하고, 항상 결과가 정확하다.

    함수형 프로그램밍: 완성된 프로그램의 특징

    • 더 단순하다 (코드가 짧고, 변수의 상태 변화를 걱정하거나 확인할 필요가 없다)
    • 코드 작성과 유지가 더 쉽다. (오직 함수만 만들어 쓰고 관리하면 된다)
    • 시간적 결합 (Temporal Coupling) 문제가 없으므로, 호출 순서를 고민하지 않아도 부작용이 없다.
    • 멀티 쓰레드에서 동시성 문제를 걱정하지 않아도 된다.
    • 변수 값 변화를 확인하느라 애쓰지 않아도 된다.

    함수형 프로그래밍 방식으로 생각하기

    함수형 프로그래밍을 위한 새로운 생각

    • 상태 변경이 아니라 불변 상태를 생각하자
      • 즉, 변수나 오브젝트의 조건이나 상태를 확인하면서 지시하지 말고, 변하지 않는 것을 정의하고 조합하자.
    • 단계가 아니라 결과를 생각하자
      • 루프 없이 가능한가? 물론 가능하다. (아래 예문 참조)
    • 구조가 아니라 조합을 생각하자
    • 명령이 아니라 선언을 생각하자
      • 원하는 것이 무엇인지를 작성하자. 어떻게 실행하는 지를 명령하지 말자.

    함수형 프로그래밍에서 하지 말아야 할 것

    • 변수값 변경 (X)
    • 할당 연산자 (X): 할당문은 상태를 바꾸는 것이 목적이다.
    • 루프 사용 (X):  (IEnumerable로 대체하는 아래 예문 참조)
    • 데이터 구조 변경 (X)
    • 부작용이 있는 코드 (X)

    생각할 점!

    • GoTo문 사용 중지 운동 당시 많은 개발자들이 당황했었다. 하지만, 지금 우리 대부분은 GoTo 문 없이 코딩을 하고 있다!

    타협점

    • WriteLn: 대부분의 프로그램은 사용자의 입력을 받을 필요가 있다.
    • 사용자와 상호 작용을 위한 타협: 대부분의 프로그램은 결과 표시 등 사용자와 상호 작용을 한다.

    예문: 델파이 구현한 함수형 프로그래밍

    델파이로 함수형 프로그래밍을 하기 위해 필요한 것

    • 함수형으로 생각하기
    • IEnumerable 사용 (오픈 소스 Spring4D 에서 제공)
    • 익명 메소드 사용
    • for 루프를 쓰지 않기

    1. 정해진 갯수까지 제곱값 구하기

    // [1] 일반적인 델파이 코드
    procedure PrintSquare1 (const aInteger: Integer);
    var
      i: Integer; 
    begin
      for i := 1 to aInteger do // i의 값이 계속 바뀐다. 
      begin
        WriteLn(i,  - ‘, i * i);
      end;
    end;
    
    // [2] 상태 변경을 피하기 위해 재귀 호출 사용
    // (할당문을 사용하는 상태 변경은 피했지만, 재귀 호출은 메모리 부하가 크고 매우 조심해서 사용해야 한다는 점은 다들 안다)
    procedure PrintSquare2 (const aInteger: Integer);
    begin
      if aInteger > 0 then 
      begin 
        PrintSquare2(aInteger - 1); //상태 변경으로 볼 수 있지만, 할당문은 아니다. 
        WriteLn(aInteger,  - ‘, aInteger * aInteger); 
      end;
    end;
    
    // [3] TEnumerable (오픈 소스 Spring4D)와 익명 메소드 활용
    uses Spring.Collections: //Spring4D 
     
    
    procedure PrintSquare3 (const aInteger: Integer); 
    begin
      if aInteger > 0 then
      begin TEnumerable.Range(1, aInteger).ForEach(procedure(const aInt: Integer) //익명 메소드 
                                                   begin 
                                                     WriteLn(aInt,  - ‘, aInt * aInt); 
                                                   end; 
    end;
    
    // [4] 함수를 반환하는 함수형 작성
    type
      TPrintSquaresProc = reference to procedure (const aInteger: integer);
     
                                                   
    procedure PrintSquare4: TPrintSquaresProc; 
    begin
      Result := procedure(const aInteger: integer) 
                begin
                  PrintSquare3(aInteger);
                end;
    end;
    
    // [5] 함수를 인자로 받는 순수한 함수형 작성
    // 제곱을 어떻게 하는 지를 전혀 명령하지 않는다. 그냥 "제곱을 해라" 라고만 한다.
    type
      TPrintSquaresProc = reference to procedure (const aInteger: integer);
     
                                                   
    procedure PrintSquare5(const aInteger: integer; aProc: TPrintSquaresProc);
    begin
      aProc(aInteger);
    end;
     
    
    //알맞은 함수만 인자로 전달하면 제곱 뿐만 아니라 무엇이든 해당 함수에 의한 출력을 수 있다.                                              
    //여기서는 PrintSquare4 함수를 전달하여 제곱을 출력한다. 
    PrintSquare5 (10, PrintSquare4());

     

    2. 과잉수 (abundant number, 약수의 값이 자기 자신보다 큰 숫자) 구하기

    (역자 주: 이 요약에서는 일반적인 방식의 코드 설명 부분은 생략) 당연히, 꽤 길고 복잡하고 루프와 할당문이 많이 사용된다. 당연히 코드 작성도 오래걸리고, 변수의 값을 일일이 추적하면서 디버깅하는 것도 오래 걸린다.

    // 함수형 코드
    uses
      Spring.Collections,
      uNumberClassificationTypes;
    
    type
      TSomewhatFunctionalNumberClassifier = class
      public // TSomewhatFunctionalNumberClassifiers는 클래스 함수로만 구성 
        class function IsFactor(aNumber: Integer; aPotentialFactor: Integer): Boolean;
        class function Factors(aNumber: Integer): ISet;
        class function IsAbundant(aNumber: Integer): Boolean; static;
        class function IsDeficient(aNumber: Integer): Boolean; static;
        class function IsPerfect(aNumber: Integer): Boolean; static;
    end;
    
    
    function SomewhatFunctionalNumberClassifier(aNumber: Integer): TNumberClassifier;
    begin
      if TSomewhatFunctionalNumberClassifier.IsPerfect(aNumber) then
      begin
        Result := Perfect
      end else
      begin
        if TSomewhatFunctionalNumberClassifier.IsAbundant(aNumber) then
        begin
          Result := Abundant
        end else
        begin
          Result := Deficient
        end;
      end;
    end;
    
    // [1] 이 함수만 유일하게 상태가 변하고 루프를 사용한다. 이것은 뒤에서 다시 한번 더 함수형으로 개선하겠다.
    class function TSomewhatFunctionalNumberClassifier.Factors(aNumber: Integer): ISet;
    var
      LFactores : ISet;
      i : Integer;
    begin
      LFactores := Collections.CreateSet;
      for i := 1 to Round(Sqrt(aNumber)) do // 상태가 변하는 유일한 지점
      begin
        if IsFactor(aNumber, i) then
        begin
          LFactores.Add(i);
          LFactores.Add(aNumber div i);
        end;
      end;
      Result := LFactores;
    end;
    
    // 위 [1]을 더 개선한 코드 (TEnumerable.Range와 익명 메소드 활용)
    class function TSomewhatFunctionalNumberClassifier.Factors(aNumber: Integer): ISet;
    begin
      Result := TEnumerable.Range(1, aNumber).Where( function(const aInteger: Integer) : Boolean
                                                     begin
                                                       Result := IsFactor(aNumber, aInteger);
                                                     end);
    end;
    
    class function TSomewhatFunctionalNumberClassifier.IsFactor(aNumber: Integer): Boolean;
    begin
      Result := aNumber mod aPotentialFactor = 0;
    end;
    
    class function TSomewhatFunctionalNumberClassifier.IsPerfect(aNumber: Integer): Boolean;
    begin
      Result := Factors(aNumber).Sum - aNumber = aNumber;
    end;
    
    class function TSomewhatFunctionalNumberClassifier.IsAbundant(aNumber: Integer): Boolean;
    begin
      Result := Factors(aNumber).Sum - aNumber > aNumber;
    end;
    
    class function TSomewhatFunctionalNumberClassifier.IsDeficient(aNumber: Integer): Boolean;
    begin
      Result := Factors(aNumber).Sum - aNumber < aNumber;
    end;

    위와 같이 ,함수로만 구성되고, 상태 변화가 없고, 함수가 파라미터로 전달되고, 이해하기 쉽고, 훨씬 단순한 코드 예문을 보았다.
    이런 방식을 사용하면 델파이에서 함수형 프로그래밍을 할 수 있다.

    요약

    • 순수 함수는 부작용이 없다.
      • 같은 입력을 받으면 항상 같은 출력을 한다.
    • 함수형 프로그래밍은 상태 변경을 하지 않기 때문에 멀티 쓰레드 환경에서도 안전하다.
      • 할당 연산자를 사용하지 않는다.
      • 함수형 프로그래밍은 상태를 공유하지 않는다.

     

    << DelphiCon 2020 목록으로 이동

     

     공유하기


    User Feedback

    Recommended Comments

    There are no comments to display.


×
×
  • Create New...

중요한 정보

이용약관 개인정보보호정책 이용규칙 이 사이트가 더 잘 작동하기 위해 방문자의 컴퓨터에 쿠키가 배치됩니다. 쿠키 설정 변경에서 원하는 설정을 할 수 있습니다. 변경하지 않으면 쿠키를 허용하는 것으로 이해합니다.