아직 생각을 다 정리한 건 아니라서 그냥 여기에 잡담처럼 쓰는 건데, 난 아무리 봐도 함수형/불변성이 무슨 '은탄환' 같은 건 아닌 것 같다.
물론 내가 아직 함수형을 완벽하게 이해한 게 아니라 그럴 가능성도 있고, 그래서 단정적으로 말 안하는 것이기도 하고.
가끔 인터넷 돌다 보면 객체지향까면서 함수형이 진리라고 포장하는 거 꽤 자주 보는데, 이유를 물어보면 거의 대부분 불변성/동시성에 대한 이야기를 하는데 난 그건 상당히 과장된 주장이라고 본다.
물론 그게 진짜 큰 장점이 되는 경우가 있는 걸 모르는 바는 아닌데, 전체 프로그래밍 도메인을 보면 그건 일부에 불과하잖아? 그리고 게임이나 분산처리 같은 동네 보면 언어 수준이 아니라 아키텍쳐 수준에서 특정 코드는 마치 단일 스레드 처럼 동작하도록 보장해주는 경우도 많고.
객체지향이 유행한 건 그게 서버 백엔드부터 데스크탑 프로그램까지 범용적으로 적용할 수 있는 설계 방법이란 이유도 큰데, 그런 면에서 객체지향의 '상위호환'이라고 주장하는 (적어도 일부는 말이지) 패러다임이 일부 도메인에서만 유용한 장점이 된다면 좀 생각의 여지가 있다고 본다.
난 그래서 일단 가변 상태 자체를 죄악시하는 시각에는 동의 못하겠음. 패러다임은 기본적으로 모델링을 어떻게 하는지에 대한 접근을 말하는 거고, 모델링은 실세계의 개념을 프로그래밍으로 추상화하는 과정이잖아? 근데 현실 세계에서 불변성이 보장되는 개념이 얼마나 있냐?
카드 게임을 하는데 사람이 카드 한 장 더 받았다고 죽었다가 새 카드 들고 다시 태어나는 게 진짜 "player.draw(Deck): Card"보다 더 직관적일까? 모델링의 기본은 도메인에 해당하는 현실 세계의 측면을 최대한 실제와 유사하게 표현하는 거 아님?
반면에 나는 함수형의 가장 큰 장점은 동시성 문제 해결 같은 것 보단 어떤 동작을 선언적 파이프라인으로 구성할 수 있다는 점이 아닐까 싶다. 그건 누가봐도 직관적이고, 오류 가능성도 낮고, 테스트도 쉽고, 무엇보다 범용적이잖아?
그래서 난, 마치 ORM 쓰는 비지니스 어플에서 엔티티하고 VO/DTO를 나누듯 객체지향하고 함수형을 적정한 계층을 사이에 두고 분리하는 설계가 최적이 아닐까 하는 생각이 들더라. 그러니까 카드 게임할 때 '플레이어' 같은 건 가변 상태를 갖는 '엔티티'로 인정하고, 거기서 파생되는 동작에 대해서는 함수형으로 지지고 볶고 하는 그런 접근이 최선이 아닐까?
어차피 함수라는게 동작이나 흐름을 추상화하는데 최적의 도구이지 구조나 계층을 표현하는 좋은 수단은 아니잖아? 그래서 그건 기존 처럼 객체지향에 맡겨 놓고 뭔가 객체지향의 상태를 함수형의 동작으로 조작한다는 접근이 필요한 것 같다. 그리고 불변성은 그 과정에서 파이프라인 내부에서 사용하는 데이터에 대해서만 신경 쓰면 될 문제인데, 그건 뭐 객체의 단일 속성이나 VO/DTO/Record/DataClass 등등을 쓰면 되니 불변으로 처리하는 게 어렵진 않으니까.
그렇게 보면, VO 등등은 기본적으로 가변 상태를 가지는 객체지향적 엔티티의 일부 상태값에 대한 특정 시점의 스냅샷이라고 보고, 함수형은 기본적으로 그 불변 데이터를 어떻게 가공할 것인지를 모델링하는 역할을 맡으면 되지 않나 싶다.
게임은 진작에 객체지향 버리고 데이터 중심 설계로 돌아갔대여 느린데다 private으로 상태 가리는건 게임 쪽에선 별 도움이 안 되서 함수형의 불변성보다는 고차함수로 만드는 섹시한 표현이나 갖다 쓰자는게 메인스트림 언어들이긴 하죠
아마 유니티의 ECS 같은 거 이야기 같은데, 내 생각에 그 동네는 좀 복잡 미묘한 듯. 애초에 객체지향이건 뭐건 복잡한 거 싫어하는 분위기도 있고... 함수형은 뭐 C# LINQ도 안쓸려하더만 ㅎㅎ
근데 그런 심리적인 부분 떠나서 게임 동네에서 데이터 중심 설계가 분명 유효한 구석은 있는 것 같다. 단적으로 스카이림 폼 데이터 같은 걸 계층 관계로 표현했으면 모드가 그렇게 흥하지 못했겠지.
늅늅이 입장에서 볼 때에도 마지막 문단에 깊게 공감됨요. 사실상 필요한 부분에만 쓰면서 하는게 맞지 함수형에 너무 집착하는건 힙스터같음
선언적인게 제일 큰 장점 맞음. 불변성은 선언적이기 위해서 필요한 조건이니 합리적인 함수형언어에서 떼어내기는 힘들지. 근데 결과적으로는 함수형에서도 가변성 다 씀. 가변성에서 stateful, side effect, mutation 어디에 초점을 맞추느냐에 따라 해결책이 조금씩 다른데 - dc App
단순히 stateful한거는 그냥 인자로 주고받으면 끝남. 애초에 수학에서 가변적인 (하지만 결정적인)걸 모델링하려고 쓰는게 함수니. 도대체 왜 함수형이 stateful한걸 배제한다는 생각이 만연한지 모르겠음 - dc App
IO도(=side effect) 인자 주고받는걸로 다 표현할 수 있음. 다만 언어에서 primitive하게 주어지는 io연산들이 필요하긴 함. 모나드는 이런 코드에서 장황함을 낮추는 추상화 도구일 뿐이지, 본질적으로 필요한건 아님. - dc App
mutation은 단순히 그런걸 표현할 수 있냐를 떠나서, 효율적으로 동작하는지의 문제인데, 예를들어서 정렬 알고리즘이나 해쉬테이블 같은걸 불변 인자를 주고받는걸로 효율적으로 구현할수는 없지. - dc App
이건 타입에 안빡센 언어에서는 그냥 허용해줘버리고 (리습 set같은거), 하스켈은 IO, ST로 해결함. ST는 뭐 굉장한 기능이 들어있다기 보다는 그냥 메모리 연산같은걸 ST안에 가둬놓고 탈출 못하게 하는 용도임. 다른 언어에서는 그냥 되는데 그걸 막는거. - dc App
함수형이 객체지향의 상위호환은 아니지. 애초에 너무 달라서 비교가 잘 안될듯. 다만 나는 객체지향은 별로 안좋아함. 사실 메서드가 싫은건데, 함수로도 똑같이 표현할수 있는걸 메서드로 표현해서 얻는건 달라진 문법밖에 없다고 생각함. 보통 메서드는 고차함수로 써먹기 힘들기도 하고. - dc App
음 근데 카드게임 비유는 표현이 직관적인지 문제를 말하는간가? 인자 순서가 바뀐다 뿐이지 별 다를건 없을텐데. 실제로 플레이어가 사라지고 새로운 플레이어가 생긴다고 생각하면서 프로그래밍 하지는 않잖아. - dc App
draw :: Deck -> Player -> IO Card 아니면 draw :: Player -> StateT Deck IO Card - dc App
모델링에 대한 관점 차이인 것 같다. OOP의 경우 계층/속성/동작으로 가능하면 실세계의 대상을 흉내내는 접근이잖아? 함수와 메서드의 근본적인 차이도 그런 부분이고. 그냥 인자 넣으면 값 반환한다는 기능만 보면 비슷하지만, 메서드는 기본적으로 주체의 동작을 표현하는 반면에 함수는 기본적으로 수학적 개념이니까, 그래서 참조 투명성이니 불변성 같은 게 더 중요해지겠지.
카드 게임 예시의 경우 동작만 딱 뜯어서 놓고 보면 함수/모나드 조합으로 표현할 수 있는데, 그게 실세계 처럼 정체성을 유지하면서 손에 든 카드라는 '속성'이나 카드를 뽑는 등 동작으로 표현되는 주체라는 개념이 지워지는 거잖아. 그 부분에서 시각 차이가 있는 것 같다.
가변 상태나 부작용에 대해 조금만 부연하면, 함수형이라고 그런 걸 완전 배제하는 게 아니다라는 건 대략 이해하고 있다. 어떠한 상태도 바꾸지 않는 건 프로그램이 아니니까... 결국 그걸 명확하게 격리해서 최종적으로 원하는 부작용을 발생시키는 것이란 건데, 지금 맥락에서 문제는 가변 상태가 어디에 어떤 형태로 존재하는지인 것 같다. 즉, 그게 함수의 인자 같은 형태로만 존재하는 건지 아니면 실세계의 어떤 대상을 반영하는 객체에 종속된 속성으로 존재하는지의 차이이고, 내 생각엔 후자로 표현할 때 훨씬 직관적인 경우가 충분히 존재하는 것 같다는 정도로 정리할 수 있을 것 같네.
IORef 라는 타입이 있음. readIORef :: IORef a -> IO a writeIORef :: IORef a -> a -> IO () 로 조작할 수 있는데, IORef a 자체는 안바뀌고, 그걸로 레퍼런싱되는 값만 바뀜 위에쓴 Player도 사실 type Player = IORef PlayerInfo 정도로 생각하면 되는데
객체와 똑같진 않지만 Player를 '실체'로 봐도 될듯. 다만 Player 타입은 IO 안에서만 그 의미를 가질 수 있고, IO 모나드 밖에서 독립적으로 존재할 수는 없음.
해스캘을 안해봐서 IO는 몰라도 IORef는 바로 이해는 안되네 ㅎㅎ 근데 내 이야기에서 핵심은 언급한 그런 제약인 것 같다. OOP 적으로 보면 플레이어란 개념에서 변할 수 속성이 손에 든 카드만 있는 건 아니고, 또 예를들어 플레이어와 딜러의 공통점 같은 걸 상위 유형으로 표현한다던지 그런 자유도가 필요하니까. 플레이어를 특정 동작 내부의 개념으로 만 정의하면 그런게 힘들지 않을까?
IORef가 IO보다 훨씬 간단한건데.. 그냥 포인터 타입같은거임. 일반적인 포인터면 포인터가 가리키는 값을 바꾸지만, 함수형에서는 그게 안되니 IORef는 그 대신 포인터값 자체를 바꿈. - dc App
여담인데, 난 아직 함수형은 객체지향 만큼 이해하는 단계는 아닌데, 뭐 찾아볼려면 꼭 해스켈에서 어떻더라 그런게 나오더라 ㅎㅎ; 아무래도 함수형 더 공부하려면 해스켈 API 시그네쳐 이해할 정도는 배워야 할 듯;
동작으로만 보면 IORef를 객체로 치환해도 될듯. 물론 상속같은건 없지만. - dc App
그게 객체지향은 사용자가 여기저기 퍼져있는데, 순수함수형은 사용자가 적다보니 하스켈에 다 뭉쳐있어서 새로운 컨셉이 다 하스켈에서 나옴 ㅋㅋㅋ - dc App
패러다임은 그냥 생각의 틀을 잡기 위한 도구라고 보면 되지 않나 싶음. 많은 언어들이 multi paradigm을 지원하는게 이유가 있지. Script들은 Procedual하게 짜는게 맞는거고. 구글 파이썬 가이드에도 길어지면 map lambda질 하지 말고 for loop 굴리라고 하는데.
함수형은 그렇게는 안되는 패러다임임
꼬투리 잡는 기분이긴 한데 player.draw(deck)은 그럼 선언적 파이프라인으로 구성하면 안 되는 거임?
대략 이런 걸 생각해보는 중이다. 개념적으로 플레이어가 손에 들고 있는 카드라는 '속성'의 근원(source of fact)은 플레이어니까, 내부적으로 Rx Subject 같은 걸로 상태관리를 책임지고, 외부엔 Observable[List[Card]] 같은 속성으로 노출하는거지. draw는 일반 OOP에서 하듯, 내부 상태인 Subject에 값을 바꾸겠고, 그럼 파이프라인은 플레이어 외부에서 Observable을 가지고 구성하면 되지 않을까?
> 구조나 계층을 표현하는 좋은 수단은 아니잖아? 비교적 정확한 지적이라고 봄. 아직 하스켈엔 모나드를 편하게 섞는 방법이 없어서 다들 그 방법을 찾는 중이지. 그래서 나는 내가 하스켈을 좋아하는 이유를 가능성이라고 봤음. 네가 가려워하는 부분을 해결해줄 수 있는 함수형 언어는 나오지 않았으니 까일만 하지. 근데 객체지향이 손 놓고 해결하지 못하는 문제를 가능성이나마 제시한 게 함수형이고, 근래에 새로운 개념들은 다 함수형에서 나오고 있음. 가능성은 볼 만하다고 생각함.
소형이면 몰라도, 대형이면 객체지향이랑 스까서 쓰는게 좋긴할듯.
구조나 계층은 보통 모듈이나 네임스페이스로 표현... 그냥 함수 뭉테기로 하는 듯
근데 여기서 이야기하는 '계층/구조'하고 이름 공간은 또 다른 이야기 같다. OOP에서 계층이란 건 어떤 개념이 다른 개념을 확장한다는 거잖아? 예컨대 고양이->동물 관계처럼 말이지. 근데 이름 공간이 말하는 계층이란 건 파일 시스템의 리렉터리 같은 거잖아. 그래서 좀 용도가 다르지 않나 싶다.
clojure에서는 그런 계층/확장 표현은 타입 안 쓰고 그냥 심볼을 엮는 함수로 표현함. 다형성은 multimethod로 구현. 다른 함수형 언어는 모르겠음.
https://clojuredocs.org/clojure.core/derive
다형성은 multimethod를 이용함. 런타임 다형성인데 조건을 맘대로 넣어 줄수 있다고 보면 됨. 계층 구조랑 엮어서 타입 없이 갓바에서 타입 상속질로 하는 거랑 비슷하게 할 수도 있음.
https://clojure.org/reference/multimethods
저는 이런 모듈/네임스페이스랑 계층 구조는 다른 것인데 OOP에서는 클래스 하나로 퉁치는게 좀 불명확한 방식이라 생각함.
멀티 메서드는 대충 보니 오버로딩 같은 개념인가 보네. 솔직히 잘 모르겠다. 아무래도 난 OOP에 훨씬 익숙해서 그런지 저런 방식으로 복잡한 거 만들라면 좀 답답한 느낌이 들 듯... 근데 이건 내가 함수형을 그 만큼 잘 몰라서 그럴 수 있긴 해. 그래서 지금 단계에선 뭐라 결론을 내긴 좀 어렵네... OOP에서 모듈/네임스페이스의 구조와 상속의 계층 구조가 섞여있다는 건 어떤 이야기인지 잘 모르겠다. 단지 연관성 있는 코드끼리 모아놓는 개념은 대부분 OOP 언어에도 있고, 그건 상속하곤 별 관계없는 이야기 아닐까?
모듈/네임스페이스는 코드의 구조고 확장/계층 구조는 데이터의 구조인데 OO에선 둘 다 클래스를 쓰니까 헷갈릴 수 있다는 생각임다. 근로저에서는 보셨다시피 둘이 완전 다른 걸 써서 구분이 되죠. 사실 요즘 oop 언어들은 네임스페이스 있는 언어도 많고, 자바도 컨벤션만 잘 정해서 쓰면 큰 문제는 없을 거 같기도 합니다.