- 원본 비디오 시청: https://delphicon.embarcadero.com/talks/how-well-do-you-know-the-delphi-language/
- DelphiCon 전체 보기 (현재 무료, 향후 유료 전환 예상): https://delphicon.embarcadero.com/replays/
- 데브기어의 DelphiCon 소개 페이지로 가기: https://devgear.co.kr/archives/3692
델파이로 함수형 프로그래밍하기 (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)가 되어 전달될 수 있다.
- 함수 자체가 결과값으로 반환될 수 있다.
- 결과를 해당 함수로 교체할 수 있다.
- 함수는 다른 언어 생성자가 보일 수 있는 곳이면 어디든지 보인다. (함수형 프로그래밍 언어에서 함수는 일종의 루트 타입이다)
- 부작용이 없다: 멀티 쓰레드에서 안전하고, 항상 결과가 정확하다.
함수형 프로그램이란?
- 더 단순하다 (코드가 짧고, 변수의 상태 변화를 걱정하거나 확인할 필요가 없다)
- 코드 작성과 유지가 더 쉽다. (오직 함수만 만들어 쓰고 관리하면 된다)
- 일시적인 임시 연결이나 부작용이 없다. (즉, 호출 순서를 지키느라 애쓰지 않아도 된다)
- 멀티 쓰레드에서 컨커런시를 걱정하지 않아도 된다.
- 변수 값 변화를 확인하느라 애쓰지 않아도 된다.
함수형 프로그래밍 방식으로 생각하기
- 상태 변경이 아니라 불변 상태를 생각하자
- 즉, 변수나 오브젝트의 조건이나 상태를 확인하면서 지시하지 말고, 변하지 않는 것을 가지고 코딩하자.
- 단계가 아니라 결과를 생각하자
- 루프 없이 가능한가? 물론 가능하다. (아래 예문 참조)
- 구조가 아니라 조합을 생각하자
- 명령이 아니라 선언을 생각하자
- 원하는 것이 무엇인지를 작성하자. 어떻게 실행하는 지를 명령하지 말자.
함수형 프로그래밍에서 하지 말아야 할 것
- 변수값 변경 금지
- 할당 연산자를 쓰지 말자 (할당문은 상태를 바꾼다.)
- 루프 사용 금지 ( IEnumerable
로 대체하는 아래 예문 참조) 데이터 구조 변경 금지 부작용 금지
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
이상으로 ,함수로만 구성되고, 상태 변화가 없고, 함수가 파라미터로 전달되고, 이해하기 쉽고, 훨씬 단순한 코드 예문을 보았다.
이런 방식을 사용하면 델파이에서 함수형 프로그래밍을 할 수 있다.
요약
- 순수 함수의 조건은:
- 같은 입력을 받으면 항상 같은 출력을 한다.
- 부작용이 없다.
- 함수형 프로그래밍은 상태 변경을 하지 않는다.
- (할당 연산자는 상태를 변경하기 때문에) 할당 연산자를 사용하지 않는다.
- 상태 변경은 멀티 쓰레드 환경에서는 특히 문제가 된다.
- 함수형 프로그래밍은 상태를 공유하지 않는다.
- 원본 비디오 시청: https://delphicon.embarcadero.com/talks/how-well-do-you-know-the-delphi-language/
- DelphiCon 전체 보기 (현재 무료, 향후 유료 전환 예상): https://delphicon.embarcadero.com/replays/
- 데브기어의 DelphiCon 소개 페이지로 가기: https://devgear.co.kr/archives/3692