델파이로 함수형 프로그래밍하기 (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 732055
공지 [UX Summit 요약] 오른쪽 클릭은 옳다 (Right Click is Right) 관리자 2020.11.16 729742
공지 [10.4 시드니] What's NEW! 신기능 자세히 보기 관리자 2020.05.27 731802
공지 RAD스튜디오(델파이,C++빌더) - 고객 사례 목록 관리자 2018.10.23 735213
공지 [데브기어 컨설팅] 모바일 앱 & 업그레이드 마이그레이션 [1] 관리자 2017.02.06 735553
공지 [전체 목록] 이 달의 기술자료 & 기술레터 관리자 2017.02.06 731428
공지 RAD스튜디오(델파이, C++빌더) - 시작하기 [1] 관리자 2015.06.30 742943
공지 RAD스튜디오(델파이,C++빌더) - 모바일 앱 개발 사례 (2020년 11월 업데이트 됨) 험프리 2014.01.16 937241
1395 N 윈도우와 맥 개발 시작을 위한 파이어몽키 코스북: 무료 다운로드 제공(385페이지) 관리자 2013.04.05 153175
1394 ComPort(시리얼 통신) 컴포넌트 설치안내 [11] file 험프리 2013.12.04 127309
1393 [REST API] REST 기반 파일 업로드와 다운로드 구현하기 험프리 2020.08.31 91399
1392 델파이 튜토리얼 자습서 이용 안내 관리자 2014.09.01 73411
1391 이 달의 기술자료 - 2014년 11월 험프리 2014.10.13 56431
1390 이 달의 기술자료 - 2014년 6월 file 험프리 2014.06.05 52255
1389 Find the O/S Language Type c2design 2014.07.30 49913
1388 RAD Studio Resource Center 박병일 2012.01.26 47914
1387 CD-ROM 열고 닫기 박병일 2011.12.22 45382
1386 [Android] 폰번호 가져오기 [1] 타락천사 2014.09.05 39615
1385 이 달의 기술자료 - 2014년 12월 file 험프리 2014.11.26 34584
1384 RAD Studio XE6 Update1 발표 [1] Humphery 2014.06.20 30378