- [Expression Problem] 표현 문제란?
- [Expression Problem] Open classes , Protocol , 그리고 확장 메서드
- [Expression Problem] Type classes
- [Expression Problem] Mutiple Dispatch (Feat Visitor / Multi Method) -> 지금 여기
- [Expression Problem] 객체 대수 (Object algebras) 와 Tagless Final
Mutiple dispatch 가 무엇인가 부터 알아보도록 하자
dispatch 는 대충 함수 호출이라고 보면 되고, 어느 함수가 호출될까를 결정하는것이
- dynamic 하게 dispatch (== 런타임)
- mutiple (모든 파라미터) 에 따라서 dispatch
일반적으로 C++ / Java / C# 같은경우는 일반적으로
- Compile 타임에 Compile 당시 타입에 따라서 디스패치된다.
- virtual 함수의 경우 dynamic 하게 단 하나 (receiver 즉 this) 타입에 따라서 디스패치된다.
Complie 과 Runtime 시점에 디스패치된다는것은 OOP 배울때 자주 배우는 정적 바인딩/ 동적 바인딩 의 차이이다.
Complie 타임 디스패치 / Dynamic single 디스패치
(모든 예를 설명할수있는 C# 으로 예를 들어서 설명하면)
class Charater
{
public void Hit() // virtual 없음에 주의!!!
{
Console.WriteLine("Charater Hit");
}
}
class Healer : Charater
{
public void Hit()
{
Console.WriteLine("Healer Hit");
}
}
Charater a = new Healer();
a.Hit(); // "Charater Hit"
virtual keyword 가 없으면 a 가 실제로 Healer 일 지 라도, 부모의 함수가 불린다. (정적바인딩)
class Charater
{
virtual public void Hit() // virtual 없음에 주의!!!
{
Console.WriteLine("Charater Hit");
}
}
class Healer : Charater
{
override public void Hit()
{
Console.WriteLine("Healer Hit");
}
}
Charater a = new Healer();
a.Hit(); // "Healer Hit"
Healer Hit 가 나오길 원한다면, virutal 키워드를 붙여서 동적(dynamic) 바인딩으로 함수가 연결되도록 해야한다
그럼 Mutiple dispatch 란 무엇인가?
설명하기에 앞서서, function(receiver , arg1, arg2) 는 receiver.function(arg1, arg2) 와 사실상 동일하다는걸 먼저 확인하자
- python 같은 언어는 실제로 완전하게 동일하다 (unbound 식 호출 /bound 식 호출).
- 그렇지 않은 언어라고 할지라도 실제 내부적으로는
receiver.function(arg1 , arg2) -> function ( receiver , arg1, arg2) 꼴의 함수가 존재하고, receiver.function(arg1,arg2) 호출시, receiver 의 this 포인터가 넘거가는 식으로 구현되어있다. 그렇지 않으면 모든 인스턴스별 함수가 메모리에 적제되어야 하기 때문.
[C++] 숨겨진 This pointer 는 어떻게 넘어 가는가~ (tistory.com) 참조
여기서 receiver.function(arg1, arg2) 는 기본적으로 함수가 receiver 에 종속/결합되기 때문에 (OOP 식)
function(receiver , arg1, arg2) 식 표현이 보다 유연하며, 이런 방식으로 예시를 들도록 하겠다. (FP 식)
함수 오버로딩
자 우선 모든 파라미터에 대하여 dispatch 가 되어야 한다고 했다.
우리는 이미 파라미터별로 호출되는 함수가 다른것을 알고있다. 바로 함수 오버로딩이다.
namespace Test
{
public abstract record Character();
public record Healer(int hp) : Character();
public record Knight(int hp) : Character();
public static class Test
{
static void Hit(Healer healer, Knight knight)
{
Console.WriteLine("Healer hit Knight");
}
static void Hit(Healer healer, Healer knight)
{
Console.WriteLine("Healer hit Healer");
}
static void Hit(Knight healer, Healer knight)
{
Console.WriteLine("Knight hit Healer");
}
static void Hit(Knight healer, Knight knight)
{
Console.WriteLine("Knight hit Knight");
}
public static void Main()
{
Knight k = new Knight(100);
Healer h = new Healer(100);
Hit(h, k); // Healer hit Knight
}
}
}
Hit는 자동으로 Hit(Knight healer, Healer knight) 함수가 불리게 된다.
자 그런데, C#은 기본적으로 virtual 함수가 아닌경우, Compile 타임에 바인딩된다고 했다.
따라서 Healer 와 Knight 의 타입을 부모로 변경하면 (실제로 Healer와 Knight 타입임에도 불구하고) 사용이 불가능하다
따라서 이런 함수가 동작하도록 하려면 Character / Character 를 받는 함수를 생성해 주어야한다.
static void Hit(Character c1, Character c2)
{
Console.WriteLine("Character hit Character");
}
public static void Main()
{
Character k = new Knight(100);
Character h = new Healer(100);
Hit(h, k); // Character hit Character
}
다시 말하지만 C#은 함수오버로딩은 Compile 타임에 바인딩 되므로, "Healer hit Knight" 가 아닌 "Character hit Character" 가 출력되는것은 자명하다.
동적바인딩을 위해서는 virutal 키워드를 추가하여 dynamic 바인딩으로 변경하고, 실제 this 포인터의 객체 타입에 따라 함수를 호출시켯다. 그러나 이렇게 Character c1 / Character c2 의 경우에는 이러한 바인딩이 불가능하다.
virtual 함수를 만들기 위하여 c1.Hit(c2) 인 꼴로 변경한다고 할지라도 c2 타입이 컴파일타임에 Character 인 이상 타입 c2 의 실체 타입에 맞는 함수를 호출하는것이 불가능하다.
그럼 어떻게 해야하나? Mutiple dispatch 가 지원되지 않는 언어에서는 크게 2가지 방법으로 해결한다.
- 런타임 검사
- Visitor Pattern
런타임 검사
런타임 검사는 그냥 런타임에 직접 검사하는것이다. 예를 들면 이렇게 짜야된다.
namespace Test
{
public abstract record Character();
public record Healer(int hp) : Character();
public record Knight(int hp) : Character();
public static class Test
{
static void Hit(Healer healer, Knight knight)
{
Console.WriteLine("Healer hit Knight");
}
static void Hit(Healer healer, Healer knight)
{
Console.WriteLine("Healer hit Healer");
}
static void Hit(Knight healer, Healer knight)
{
Console.WriteLine("Knight hit Healer");
}
static void Hit(Knight healer, Knight knight)
{
Console.WriteLine("Knight hit Knight");
}
static void Hit(Character c1, Character c2)
{
switch (c1, c2)
{
case (Healer cf, Knight cs):
Hit(cf, cs);
break;
case (Healer cf, Healer cs):
Hit(cf, cs);
break;
case (Knight cf, Healer cs):
Hit(cf, cs);
break;
case (Knight cf, Knight cs):
Hit(cf, cs);
break;
};
}
public static void Main()
{
Character k = new Knight(100);
Character h = new Healer(100);
Hit(h, k); // Healer hit Knight
}
}
}
어차피 컴파일 타임에는 Hit(Character c2, Character c2) 가 불리니까,
이 불린 함수에서 타입을 직접 체크 하는것이다.
패턴매칭이 좀 지원되는 언어라면 그나마 상황이 낫지만, 아니라면 그냥 if 문으로 죙일 비교해야한다.
죙일 비교하는것도 죙일인데, 코드를 계속 뜯어 고쳐야한다 즉 expression problem 이다.
Visitor
이렇게, 싱글 디스패칭밖에 안되는 OOP 언어에서, 디스패칭을 해주는 놈을 클래스로 만들어 놓는것이 Visitor 이다.
앞서서 OOP 언어에서는 dynamic 한 함수를 만들기 위하여 virutal 키워드를 이용하여 동적바인딩 한다고했다.
또한 Receiver 하나에 대하여만 dynamic 디스패칭이 가능하다고 했다.
그럼 virutal 로 receiver 에 대한 dynamic 을 여러번 하면 되는거 아님가? -> 이게 비지터다
Visitor 패턴에 대한 구현은 포스팅 범위를 넘어가므로 생략하겠다.
위키백과에 따르면
"구조를 수정하지 않고도 실질적으로 새로운 동작을 기존의 객체 구조에 추가 할 수 있게 된다"
다만 Visitor 패턴을 구현해보면 알겠지만, 이게 사용하기 상당히 거지같다..
왜 Visitor ?
근데 왜 비지터를 Expression Problem 에 설명하고있을까? 하면
OOP 식 접근은 타입추가는 쉽지만, 기능추가가 어렵다는게 Expression Problem 이였다.
여기서 함수(동작)를 Visitable 한 클래스로 만들면?
짜잔 위키백과에 따라서 "구조를 수정하지 않고도 실질적으로 새로운 동작을 기존의 객체 구조에 추가할 수 있게 된다"
public interface CharacterVisit
{
//방문자 정의
void VisitHealer(Healer c);
void VisitKnight(Knight c);
}
class Hit : CharacterVisit
{
public void VisitHealer(Healer c)
{
throw new NotImplementedException();
}
public void VisitKnight(Knight c)
{
throw new NotImplementedException();
}
}
// Skill 이 추가되어도 별도 수정 필요없음
class Skill : CharacterVisit
{
public void VisitHealer(Healer c)
{
throw new NotImplementedException();
}
public void VisitKnight(Knight c)
{
throw new NotImplementedException();
}
}
와 Skill (동작) 을 추가할때 기존 코드 수정없이 뚝딱 추가 가능!
Expression Problem 해결! 메데타시 메데타시
이면 좋겠으나 그렇게 호락호락하지않다. 이유를 잘 생각해보자.
OOP 식 접근은 타입추가는 쉽지만, 기능추가가 어렵다.
FP 식 접근은 기능추가는 쉽지만, 타입추가가 어렵다.
Visitor는 기능 추가는 쉽지만, 타입이 추가되면... 어?
public interface CharacterVisit
{
//방문자 정의
void VisitHealer(Healer c);
void VisitKnight(Knight c);
void VisitTanker(Tanker c);
}
CharacterVisit 에 방문할 타입을 추가해야한다 -> 모든 기존 Visitor 에 코드가 수정되어야 한다 -> 기존코드 수정이 발생한다.
즉 기능을 자유롭게 추가하기 위하여 Visitor를 쓰면 OOP 식 접근에서 FP 식 접근으로 변경된것이다.
Mutiple dispatch
모든 파라미터에 실제 타입에 맞게 바인딩 되도록 하는것이 Mutiple dispatch 이다.
C# 에서는 dynamic 이라는 키워드를 통하여 런타임전용 타입을 만드는것이 가능하다, 이는 함수를 사용할때도 가능하다. (잘쓰지는 않지만..)
namespace Test
{
public abstract record Character();
public record Healer(int hp) : Character();
public record Knight(int hp) : Character();
public static class Test
{
static void Hit(Healer healer, Knight knight)
{
Console.WriteLine("Healer hit Knight");
}
static void Hit(Healer healer, Healer knight)
{
Console.WriteLine("Healer hit Healer");
}
static void Hit(Knight healer, Healer knight)
{
Console.WriteLine("Knight hit Healer");
}
static void Hit(Knight healer, Knight knight)
{
Console.WriteLine("Knight hit Knight");
}
public static void Main()
{
Character k = new Knight(100);
Character h = new Healer(100);
Hit(h as dynamic, k as dynamic); // Healer hit Knight
}
}
}
여기서 기능이 추가되면?
//Skill 이 추가됨
static void Skill(Healer healer, Knight knight)
{
Console.WriteLine("Healer skill Knight");
}
public static void Main()
{
Character k = new Knight(100);
Character h = new Healer(100);
Hit(h as dynamic, k as dynamic); // Healer hit Knight
Skill(h as dynamic, k as dynamic); // Healer skill Knight
}
타입이 추가되도
//Tanker 타입 추가
public record Tanker(int hp) : Character();
//Tanker 타입에 대한 함수 Skill 이 추가됨
static void Skill(Tanker tanker, Knight knight)
{
Console.WriteLine("Tanker skill Knight");
}
public static void Main()
{
Character k = new Knight(100);
Character h = new Healer(100);
Character t = new Tanker(100);
Skill(t as dynamic, k as dynamic); // Tanker skill Knight
}
걍 맞는 함수 구현 하면 된다.
추가적인 팁으로, 기본적으로 정적언어인 C# 과 함수 오버로딩은 컴파일타임에 된다는걸 활용하여
static void Hit(Character c1, Character c2)
{
Hit(c1 as dynamic, c2 as dynamic);
}
public static void Main()
{
Character k = new Knight(100);
Character h = new Healer(100);
Hit(h, k); // Healer hit Knight
}
다음과 같이 컴파일 타임에 바인딩 되는 함수내에서 Mutiple dispatch 를 사용 할 수 있다.
Multimethod
위키백과에 따르면 Multi Method == Multiple dispatch 이기 때문에 크게 신경쓸것은 아니지만
(clojure 에 관심없으면 패스)
굳이 섹션을 나눠서 적는 이유는
- Multiple dispatch는 일반적으로 타입(class-based) 에 디스패칭을 의미한다.
- clojure 의 multi method 는 이것저것 다 된다. (클래스, 값, 심볼, multiple dispatch + adhoc 상속)
따라서 multiple dispatch 를 안써도 Expression Problem 을 해결할수있기 때문 oop 에서는 method 가 Class 와 강하게 결합되어있기 때문에 single dynamic dispatch 로는 expression problem 이 해결되지 않았으나 (클래스의 수정이 필요), clojure 는 애초에 function 과 type 이 직교하기 때문에, 걍 object 타입에 맞는 함수를 외부에서 작성하면된다.
'프로그래밍 기술 노트 > Functional Study' 카테고리의 다른 글
[HKT] Value / Type / Kind 와 Higher Kinded Type (Feat. 고차함수) (1) | 2022.04.30 |
---|---|
[Expression Problem] 객체 대수 (Object algebras) 와 Tagless Final (0) | 2022.04.28 |
[Expression Problem] Type classes (0) | 2022.04.26 |
[Expression Problem] Open classes , Protocol , 그리고 확장 메서드 (0) | 2022.04.26 |
[Expression Problem] 표현 문제란? (0) | 2022.04.26 |