델파이로 함수형 프로그래밍하기 (Functional Programming With Delphi) 를 요약했습니다. (이 요약 번역은 원본 비디오와 내용이 일부 다르거나, Q&A등 일부 생략되었을 수 있습니다.)

 

이 짧은 함수형 프로그래밍 세션을 통해 프로그래밍 방식에 대한 새로운 관점을 가져보세요. 함수형 프로그래밍은 지금까지 일반적으로 해온 명령형 프로그래밍보다 더 간결 명료하고, 멀티 쓰레드에서도 안전한 코드를 실현합니다.

  • 함수형 프로그래밍의 배경
  • 불변 (Immutable) 타입을 사용하자
  • 함수형(Functional) 프로그래밍이란?
  • 컴퓨터 프로그램이란?
  • (우리에게 익숙한) 명령형 (Imperative) 프로그래밍과의 차이점
  • 함수형 프로그래밍을 하려면 고정 관념에서 완전히 벗어날 필요가 있다.
  • 메소드의 특징
  • 함수형 프로그래밍에서의 함수의 특성은
  • 함수형 프로그래밍에서의 함수는? (매우 중요!)
  • 함수형 프로그램이란?
  • 함수형 프로그래밍 방식으로 생각하기
  • 함수형 프로그래밍에서 하지 말아야 할 것
  • 타협점
  • 델파이 예문 (1.정해진 갯수까지 제곱값 구하기, 2.과잉수 구하기
  • 요약

발표자 (Nick Hodges)는 델파이 개발자라면 한번쯤 들어본 저명한 델파이 전문가입니다. (웹페이지: https://codingindelphi.com/blog/ )

 

발표자가 저술한 도서: https://www.codingindelphi.com

  • 델파이로 코딩하기 (Coding in Delphi)
  • 델파이로 코딩하기 속편 (More Coding in Delphi)
  • 델파이에서 의존관계 인젝션 (Dependency Injection in Delphi)

원본 비디오 시청: https://delphicon.embarcadero.com/talks/how-well-do-you-know-the-delphi-language/

 

함수형 프로그래밍의 배경

Robert C. Martin의 함수형 프로그래밍 강연: https://www.youtube.com/watch?v=7Zlp9rKHGD4

상태의 실패: 상태(State)는 변한다. 변수가 언제든 변하기 때문에 다음과 같은 결과가 생긴다.

  • 변수의 데이터 변경 여부를 알지 못하면 코드를 이해하기가 어렵다.
  • 메소드 호출 관계를 파악하지 못하면 코드의 흐름을 따라가기가 어렵다.
  • 멀티 쓰레드를 활용한다면 코드를 이해하거나 디버깅하기가 더 어려워진다.

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

https://enterprisecraftsmanship.com/posts/functional-c-immutability/

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

아래 코드에서 TMutablePerson은 멀티 쓰레드에서 안전하지 않다. 하지만, TImmutablePerson과 TPerson은 멀티 쓰레드에서 안전하다.

// [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 등이 모두 함수형 프로그래밍 언어이다.
  • 뜨고 있는 이유
    • 저명한 프로그래머들이 함수형 프로그레밍을 하고 있다.
    • (순수 함수형 프로그래밍은) 멀티 쓰레드에서 안전하다.
    • 더 간결하고, 더 작성하기 쉽다. (간단한 함수 호출 코드로 기존의 긴 코드를 교체할 수 있다)
    • 테스트와 디버깅이 더 쉽다. (상태 변화를 추적할 필요가 없기 때문에)
  • 함수형 프로그래밍이란: 오로지 함수로만 구성된 코드와 프로그램

컴퓨터 프로그램이란?

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

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

 

명령형 (Imperative) 프로그래밍

함수형(Functional) 프로그래밍

코드의 목적

어떻게 (HOW) 하는 지정

무엇을 (WHAT) 하는 지 지정

코드 작성 방식강조

명령을 순서대로 실행하는 방식

표현식의 결과를 강조하는 방식

문제 해결 방식(예시)

목록에서 다음 고객을 선택하고,

만약 청구할 금액이 있으면, 청구서를 발행한다.

만약 목록에 고객이 있으면, 첫단계로 간다.

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

 

함수형 프로그래밍을 하려면 고정 관념에서 완전히 벗어날 필요가 있다.

“객체 지향은 움직이는 각 부분을 캡슐화함으로써 코드를 이해할 수 있도록 만들었다. 함수형 프로그래밍은 각 부분의 움직임을 최소화함으로써 코드를 이해할 수 있도록 만든다” - 마이클 페더스 (Michael Feathers)

 

디버깅을 해본 개발자라면, 움직이는 부분들이 너무 많고, 코드를 이해하려면 이 많은 움직임을 힘들게 쫓아다녀야 한다는 점을 잘 안다. 움직이는 부분이 최소화 된다면 훨씬 단순해 질 수 있다.

 

메소드의 특징

  • (함수형 프로그래밍에서 메소드의) 참조 투명성이 확보된다.
  • (함수형 프로그래밍 함수는) Method Signature (메소드의 인자과 결과)가 언제나 정직하다.
    • 예를 들어 메소드에 A를 전달하면 결과는 항상 B이다. (B가 아닌 결과는 결코 나오지 않는다)
  • [퀴즈] Assert.True (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)가 되어 전달될 수 있다.
  • 함수 자체가 결과값으로 반환될 수 있다.
  • 결과를 해당 함수로 교체할 수 있다.
  • 함수는 다른 언어 생성자가 보일 수 있는 곳이면 어디든지 보인다. (함수형 프로그래밍 언어에서 함수는 일종의 루트 타입이다)
  • 부작용이 없다: 멀티 쓰레드에서 안전하고, 항상 결과가 정확하다.

함수형 프로그램이란?

  • 더 단순하다 (코드가 짧고, 변수의 상태 변화를 걱정하거나 확인할 필요가 없다)
  • 코드 작성과 유지가 더 쉽다. (오직 함수만 만들어 쓰고 관리하면 된다)
  • 일시적인 임시 연결이나 부작용이 없다. (즉, 호출 순서를 지키느라 애쓰지 않아도 된다)
  • 멀티 쓰레드에서 컨커런시를 걱정하지 않아도 된다.
  • 변수 값 변화를 확인하느라 애쓰지 않아도 된다.

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

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

DelphiCon-FP1Dos.png

 

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

  • 변수값 변경 금지
  • 할당 연산자를 쓰지 말자 (할당문은 상태를 바꾼다.)
  • 루프 사용 금지 ( IEnumerable로 대체하는 아래 예문 참조)
  • 데이터 구조 변경 금지
  • 부작용 금지

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

DelphiCon-FP2Donts.png

 

타협점

  • 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.Foctors(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.Foctors(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 요약] 코드사이트 로깅 실전 활용 기법 (Real-world CodeSite Logging Techniques) 관리자 2021.01.19 22115
공지 [UX Summit 요약] 오른쪽 클릭은 옳다 (Right Click is Right) 관리자 2020.11.16 20659
공지 [10.4 시드니] What's NEW! 신기능 자세히 보기 관리자 2020.05.27 22701
공지 RAD스튜디오(델파이,C++빌더) - 고객 사례 목록 관리자 2018.10.23 28462
공지 [데브기어 컨설팅] 모바일 앱 & 업그레이드 마이그레이션 [1] 관리자 2017.02.06 29722
공지 [전체 목록] 이 달의 기술자료 & 기술레터 관리자 2017.02.06 25071
공지 RAD스튜디오(델파이, C++빌더) - 시작하기 [1] 관리자 2015.06.30 46008
공지 RAD스튜디오(델파이,C++빌더) - 모바일 앱 개발 사례 (2020년 11월 업데이트 됨) 험프리 2014.01.16 181771
1342 [DelphiCon 요약] 델파이로 웹 다루기 (Powering the Web with Delphi) file 관리자 2021.02.16 1379
1341 [고객 사례- POS, C++빌더] YG-POS - 소상공인을 위한 무료 소프트웨어 관리자 2021.02.08 745
1340 [고객 사례- 금융, 델파이] 주식, 화폐 등 금융 정보 분석 소프트웨어 - 게임스탑, 비트코인, 아마존, 구글 등 관리자 2021.02.04 688
1339 ‘장기 활용 가능성’이 가장 뛰어난 프레임워크는? (델파이 VS. WPF VS. ELECTRON) 관리자 2021.02.01 499
1338 [델파이 웹개발] 델파이로 풀스택 웹 개발하기 - uniGUI 활용 (Full Stack Web Development with uniGUI for Delphi) file 관리자 2021.02.01 4432
1337 버전별 업데이트된 주요 기능들 (C++빌더6 / 델파이7부터 최신 버전까지!) 관리자 2021.01.29 408
1336 이 달의 기술자료 - 2021년 02월 file 험프리 2021.01.26 560
1335 [샘플 프로젝트] 심박 측정기-델파이/C++빌더와 IOT 연동하기 관리자 2021.01.25 528
1334 C++ 유니코드 문자열 리터럴(Unicode String Literal) 활용 방법 관리자 2021.01.22 549
1333 C++빌더 마이그레이션, C++ 프로젝트를 간편하게 업데이트 하는 방법 관리자 2021.01.19 354
1332 [고객 사례- 델파이, 게임] 체스 오프닝 위저드 - 60,000여명의 체스 플레이어가 실제로 사용하는 앱 관리자 2021.01.18 781
1331 생산성을 끌어올려줄 겟잇(GetIt)의 최신 컴포넌트들 관리자 2021.01.18 596
1330 [DelphiCon 요약] High DPI 고해상도를 VCL에서 활용하기 (Leveraging High DPI in VCL Applications) 관리자 2021.01.13 789
1329 [DelphiCon 요약] Spring4D 소개 - 델파이 개발을 한수준 높이기 (Introduction to Spring4D - Taking Delphi Development to the Next Level) 관리자 2021.01.08 1224
1328 새해 목표: LEARN, TEACH, REPEAT. 관리자 2021.01.05 350
» [DelphiCon 요약] 델파이로 함수형 프로그래밍하기 (Functional Programming With Delphi) 관리자 2020.12.29 754
1326 [DelphiCon 요약] 델파이 고성능 구현 (High Performance Delphi) 관리자 2020.12.27 965
1325 [TOP 10] 2020년 하반기, 개발자가 사랑한 기술자료는? 관리자 2020.12.24 382
1324 이 달의 기술자료 - 2021년 01월 file 험프리 2020.12.24 505