DelphiCon 의 2020 시리즈 중, Functional Programming With Delphi - Nick Hodges 의 한글 요약본입니다.
- 프로그래밍 방식에 대한 새로운 관점을 가질 수 있는 세션입니다.
- 함수형 프로그래밍은 지금까지 일반적으로 해온 명령형 프로그래밍보다 더 간결 명료하고, 멀티 쓰레드에서도 안전한 코드를 실현합니다.
-
발표자 (Nick Hodges)는 델파이 개발자라면 한번쯤 들어본 저명한 델파이 전문가입니다.
-
저서
- 델파이로 코딩하기 (Coding in Delphi)
- 델파이로 코딩하기 속편 (More Coding in Delphi)
- 델파이에서 의존관계 인젝션 (Dependency Injection in Delphi)
-
저서
- 원본 비디오에는 아래 요약보다 자세한 데모와 설명이 있습니다.
목차
상태의 실패(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;
위와 같이 ,함수로만 구성되고, 상태 변화가 없고, 함수가 파라미터로 전달되고, 이해하기 쉽고, 훨씬 단순한 코드 예문을 보았다.
이런 방식을 사용하면 델파이에서 함수형 프로그래밍을 할 수 있다.
요약
-
순수 함수는 부작용이 없다.
- 같은 입력을 받으면 항상 같은 출력을 한다.
-
함수형 프로그래밍은 상태 변경을 하지 않기 때문에 멀티 쓰레드 환경에서도 안전하다.
- 할당 연산자를 사용하지 않는다.
- 함수형 프로그래밍은 상태를 공유하지 않는다.
Recommended Comments
There are no comments to display.