오늘 오카믈에서 mutable 하게 데이터타입을 정의하는 부분을 보면서, 불변에 대한 이해도가 확 올라갔기에 그것을 공유해보려고 함.
1. 변수의 사용, 그 자체는 부작용이다.
불변을 이야기하려면 변수를 이야기해야한다.
후술하겠지만, 변수는 그 존재 자체가 부작용이다.
변수라는 것은 일종의 데이터를 위한 공간이다.
함수형은 부작용을 사용하지 않는 것이 아니다. 통제할 뿐이다.
즉, 함수형 또한 공간, 즉 변수를 사용한다.
단, 그것은 불변이 아니라 가변이다.
엥? 함수형은 불변이라면서요?? -> 함수형은 공간을 배정하지 않고도 연산을 하는 방법을 가지고 있다.
함수형의 공간, 즉 변수는 가변이다. 단, 변수를 선언하지 않고도 연산할 뿐이다.
공간을 배정한다는 것은 필연적으로 부작용을 포함한다.
대부분의 경우 공간의 데이터를 읽어오고, 공간의 데이터에 따라서 연산이 바뀌기 때문이다.
이는 참조투명성을 깨뜨리는 행위이다.
IO라는 행위를 예시로 들면, 컴퓨터 바깥의 입력을 받아오기 때문에 결과값이 뭐가 나올지 예상할수가 없다.
마찬가지로, 공간에 데이터가 무엇이 들어있을지를 읽어오는 함수가 있다면, 그 결과는 예상할 수가 없다.
변수의 존재 자체가 부작용을 의미한다는 것이다.
변수를 선언하고 바꾸지 않는다?? -> 그것또한 부작용이다. 변수의 선언 자체가 부작용이기 때문.
공간에 데이터를 쓰는것 또한 부작용으로 간주된다.
즉, "함수형의 성질로 변수의 불변을 제시하는 것"은, 철저히 객체지향적으로 사고하고, 피상적으로 상황을 이해하는 것이다.
상태는 그 자체가 부작용이다. C언어가 변수를 사용하는것 그 자체가 부작용인 것이다.
함수형 언어는 부작용을 철저히 통제하면서 코딩한다. 부작용이 없는게 아니다.
함수형 언어도 부작용을 사용한다. 다만 철저하게 통제하며, 부작용이 없는 쪽의 연산을 극단적으로 발달시켜놓았을 뿐이다.
부작용을 철저히 통제하는 만큼, 통제 없이 사용하는 pure 한 부분은 함수형언어의 "철학"이라고 할 수 있다.
여러분들이 그렇게 알고싶어하는 "그래서 함수형이란게 뭔데?" 말이다.
2. Ref = 함수형 언어도 공간을 사용한다.
자 앞서, 변수의 존재 자체가 부작용이라고 말했는데,
함수형 언어도 변수를 사용한다. 또, 변수가 반드시 필요한 경우가 있다.
아무리 함수형으로 쇼를 써도, 변수 없이는 한계가 있다.
그러면, 이제까지 얘기한걸 종합해보자.
1. 변수는 부작용이고
2. 함수형도 부작용을 사용한다.
뭐야 그러면 객체지향과 뭐가 다른데???
함수형 언어는 부작용을 모나드 안에서 철저하게 통제한다.
함수형 언어를 강력하게 만드는것은, 부작용이 없는 순수의 영역에서 그 진가가 발휘된다.
부작용을 안쓰고도 이만큼 해낸다고?? 가 선행되어야
부작용을 통제하는 의미가 있다. 부작용의 영역을 줄이지 않고, 단지 통제만 한다는 것은 그냥 바보짓 아니겠는가?
그럼 이제 함수형이 변수를 안쓰고도 연산을 해내는 법을 알아보자.
3. 함수형이 순수하게 연산하는 방법
변수를 배정하지 않고 연산하는 방법은, 모든 연산의 입력을 함수의 인수로 전달하고,
모든 연산의 결과를 함수의 리턴값으로 반환하는 것이다.
즉, 상태를 컴퓨터 공간에 저장하는것이 아니라, 흐르는 성질로 만드는 것이다.
데이터의 흐름을 정의하고, 최초 입력값을 명확한 argument로 대입하는것,
이게 함수형이 연산하는 방식이다.
이에 대한 가장 간편한 예시는 바로 꼬리재귀이다.
꼬리재귀는 공간의 배정 없이 순수하게 상태를 묘사한 연산이다.
sum list 0 = sum tail (0 + head)
두번째 인수가 마치 변수처럼 보이지 않는가?
대충 이렇게 이해하면된다.
물론, 더 깊이 들어가자면, 상태라는 것을 이어가기 위해선 상태를 조작하는 함수들을 엮어서 새로운 함수를 만들어야하며,
이것이 모나드연산으로 이루어짐을 알 수 있고,
그중에서도 특히 Reader 모나드와 State 모나드에 집중해야하는데,
이부분의 내용은 어렵다. 사실상 이 부분을 떼고 나면 함수형을 진짜로 입문했다고 말할 수 있는 단계이지 않을까 싶다.
모나드에 대한 이야기는, 함수형이 변수에 대한 배정 없이 연산을 하는 방법이 함수형스타일이라서 할 얘기가 많다는것만 표현하려고 했고,
지금은 대충 꼬리재귀로 이해하자. 함수형은 꼬리재귀를 통해서 순수하게 연산을 한다.
물론, 순수한 방법만이 정답은 아니고, 앞서 얘기했듯, 하스켈도 상태, 즉 부작용을 쓴다. 모나드 안에서
4. 컨트롤되지 않은 부작용 => 불변이라는 개념 형성.
여기까지가 하스켈을 중심으로 논리전개가 이루어졋다면, 이제부터는 오카멜을 중심으로 얘기가 흘러갈 것이다.
오카멜에 이르러서야 왜 객체지향 언어가 그토록 불변을 부르짖었는지 깨닫게 되었다.
오카멜은 하스켈과 큰 차이가 있다.
부작용이 컨트롤되지 않는다. 그럼 오카멜은 이제까지 얘기한 함수형 특징에 벗어나는가?
벗어난다고 볼 수 있다. 하지만, 하스켈이 구현 가능한 부분들. 하스켈의 강력함을 오카멜은 적용이 가능하다.
하스켈이 안전한부분은 보장받지 못한다.
앞서, Ref 라는 공간배정은 모나드 안에서 이루어져야한다고 했지만,
오카멜은 공간이 모나드컨텍스트 바깥으로 튀어다닌다.
그 결과로,
상태를 포함한 데이터타입 이라는 괴물이 튀어나온다.
하스켈에서는 상태를 포함한 데이터타입을 만들어봤자, 모나드 컨텍스트 안에서만 값을 꺼내올 수 있는데,
오카멜은 바깥에서도 값을 꺼내버린다.
자, 여기서부터는 객체지향이다.
오카멜의 O는 Objective를 의미한다고 들었는데, 이런 부분도 객체지향적인 요소라고 간주되는지는 모르겠다. 어쨌든 pure하지 않다는것은 확실하다.
dataType MyType 이 있고,
data mut_prop 라는 가변 프로퍼티가 있고
data prop 라는 프로퍼티가 있다고 가정하자.
이때, mut_prop 는 상태고, prop은 일반적인 원시타입이다.
절대로, 두개 다 상태인데 단지 prop은 변경 불가능 으로 받아들이면 안된다.
prop 은 int 고, mut_prop 은 int ref 라고 타입이 다른거라고 받아들여야한다.
int 는 공간이 배정되어있지 않다. 즉, 변경 불가능하다.
int ref 는 ref 뒤에 공간이 배정되어있고, 그 공간에 접근하는 부작용 메소드를 통해 연결된 공간을 조작할 수 있다.
int ref는 모나드 안에서만 공간조작이 이루어져야하지만, 오카멜은 모나드 바깥에서도 공간을 조작할 수 있다.
5. 잠시 짚고넘어가야할 사안 -> 함수형에서 let 이란?
논지를 진행하기 위해선 이단락이 꼭 필요하다.
앞서, 함수형은 변수선언이 없다고 말했다.
그러면, 함수형에서 let 이 보이는데 얘들은 뭔가요??
함수형에서 let 은 연산식 이름붙이기이다.
let a = expression 라고 쓴다면, 이는 절차지향과 해석방법이 다르다.
절차지향의 경우, expression을 실행하고 그 결과물을 a에 집어넣어라
함수형의 경우, "내가 앞으로 a라고 적으면 그것은 다음 expression 에 대한 축약이야" 라는 뜻이다.
즉, a를 쓸때마다 expression이 재평가된다.
a가 변수가 아니라 함수다 라고 생각하면 이해가 편할것이다.
a는 함수다.
다만, 무엇이 하스켈을 이해하기 어렵게 만드냐면, 하스켈의 지연평가는 전부 thunk라는 형식으로 내부에서 저장되어서 평가를 기다리는데'
같은 이름을 사용하면 a의 평가결과를 한번 기억해 놨다가, 재사용한다. 즉 여러번 연산을 반복하지 않는다는 의미.
심지어 이 연산 캐싱 로직은 매우 복잡해서, 숙련된 하스켈러들조차 연산의 평가시점을 완벽하게 계산하긴 어렵다고 한다.
근데, 그 복잡성은 하스켈의 단점이 되지 않는다.
왜냐?
참조투명성
표현식이 재평가 되던지 말던지간에 언제나 값은 똑같다. 즉, 코드의 재평가 여부가 코드의 전체 행동에 영향을 주지 못한다.
이제 이 단락의 결론인데,
let a = ref 0 로 공간을 배정하고
a := 3 로 쓰고
!a 로 읽으면
모두 같은 공간을 접근할까??
답은 yes 다.
하스켈에 대해서는 모나드를 풀어헤쳤을때 ref가 인수로 들어가고 인수가 named되어있으며 named parameter가 없기 때문(이라고 쓰고 그냥 무시하면 된다)
오카믈에 대해서는 오카멜은 앞서 설명한 지연평가가 적용되지 않는 Strict 한 언어다.
따라서 모든 수식들이 즉시 연산이되며, 그 연산 결과가 같은 이름에 대해 공유된다.
6. 다시 돌아가서... 결론
다시 앞서 설계한 MyType 과 prop 그리고 mut_prop 을 살펴보자.
원래 함수형이 MyType 을 다루는 방법은
꼬리재귀와 같이,
인자에 넣고, 흘려보내는 것이다.
그러면 MyType 은 m0 에서 m1으로, m1에서 m2로 바뀌면서 데이터를 변형해 나갈 것이고, 그 결과물로 마지막 함수를 투과하고 나온 m_n 이 결과물로 반환될 것이다.
하지만,
mut_prop이 있으면 이 방법은 먹히지가 않는다.
m이 내부의 공간을 바꾸기 위해서 mutable 하게 선언되었다고 생각하나??
공간을 바꾸기 위해서인가?
아니다. 공간 안의 변수를 바꾸기 위해서이다.
즉, 공간은 그대로 두어야한다. 공간이 변형되면 안된다.
공간 안의 변수가 그대로가 되면서 엄청난 일이 벌어진다.
Mytype은 m0 에서 m0으로, m0에서 m0으로... 이런식으로 체이닝이 이루어 질 것이고,
그 결과 prop 은 불변상태가 되어버린다.
mut_prop은 부작용을 통해서 바뀐다고 해도, prop은 전혀 건들수가 없는게 되는것이다.
prop이 불변인게 함수형 철학인가???
난 아니라고 생각한다.
m0 -> m1 -> m2 로 바꿔나가는 행동을 해야 함수형 철학이라고 생각한다.
함수형은 데이터를 고정시켜놓고 생각하는 철학이 아니라,
데이터를 통째로 바꾸는 철학이다. 내부가변성이 아닌,
근데 난 내부가변성도 좋다.
쓰고보니까 순혈 함수형을 주장하고있는것처럼 글이 써져버렸는데
난 오카멜의 융통성이 아주 마음에 든다.
재밌게 읽었읍니다 궁금한 점이 오캐믈이 모나드에서 비롯된 함수형 언어의 순수성을 지키지 못 한다면, 객체지향 언어의 리액티브 프로그래밍과 다른 점이 무엇이고 다른 언어들이 있음에도 오캐믈을 선택해야 될 이유가 있나요? 하스켈은 학문적인 언어로 생각하면 수요와 방향성이 이해가 되는데 말이져
어려운 질문이네요... 제가 하스켈 ML Lisp Scala 반응형 순서로 공부할 예정인데요 (현재 ML인 Ocaml 공부중) 저기까지 가봐야 완벽하게 대답할수 있을거 같습니다. 분명히 장단점이 있을거라고 생각합니다. 오카믈의 장점은 객체지향과 함수형을 동시에 사용할수 있다는 점인데, 제가 배운 C, Js, Rust, Python, Java 로 함수형을 쓰기가 너무 불편합니다. 함수형이 부작용을 제한하는것에도 의미가 있지만, 순수함수적인 부분을 순수함수적으로 짜는 것만 해도 충분할수도 있다고 생각하거든요. 순수함수적으로 코딩을 함에 있어서 오카믈이 편합니다. 물론 부작용을 계산하는건 프로그래머 본연의 몫이겠지만, 순수함수적인 조각들을 만드는것에는 오카믈로도 하스켈의 구현력과 유사합니다.
예를들면 1. 데이터타입의 정의. 패턴매칭이 가능한가. (Sum이 정의되어야함) 2. 표준 라이브러리에서 다루는 데이터가 상태를 포함하지 않은 함수형 데이터 타입인가? (리스트는 그렇지 못하죠) 3. 타입클래스 단위에서 추상화가 가능한가 이런 부분들에서 오카믈이 장점을 가지고 있는거같습니다.
반응형 프로그래밍이 무엇인지 공부가 안되어있어서 답변이 제대로 되지 않을것같네요 ㅠㅠ 죄송합니다. 반응형 프로그래밍이 모나드 연산이랑 닮아있다고 들었는데, 느낌이 왠지 함수들을 합성하면서 표현력을 증가시키는 방향이 아닌가 싶은데 (함수형의 또다른 장점) 함수형의 표현력을 객체지향의 방식대로 이루어낸 것이라면 굳이 함수형이 필요 없지않을까요? 물론 말씀하신대로 순수성이라는 측면도 고려를 해봄직 한데, 그 관점에서라면 오카믈이 좀더 유리하긴 할거같습니다. 변수(가변)를 안쓰고 연산하기에 더 유리하니까요. 아마 반응형프로그래밍이 패턴매칭까지 구현되어있진 않을거같아요. 근데 패턴매칭과 재귀는 변수(가변)을 안쓰는 키워드라서...
요약하자면, 인자로 건네받는 데이터타입이 가변이냐, 불변이냐 에서 반응형 프로그래밍과 오카멜이 차이가 있을거같아요. 데이터타입이 상태라면 부작용을 컨트롤할수 없어서 순수함수라는걸 만들수가 없는데 (상태에서 값을 꺼내려면 부작용이 필수고 부작용은 모나드안에있어야함. 함수의 내부는 모나드의 바깥이고, 따라서 함수 내부에서 상태에 접근하는것은 순수성 위반임) 오카멜은 상태가 없는 데이터타입을 그대로 사용할 수 있어서, 순수함수를 만들수가 있습니다. 반응형에서 순수함수를 만드려면 아마 그냥 원시타입끼리 가벼운 연산을 하는것 정도로밖에 못하겠죠? 리스트를 쓰면 안됩니다.
오해할까봐 남기자면, 리스트가 자꾸 상태이며 순수하지 못하다고 말하는것은, 객체지향의 리스트를 말합니다. array 혹은 벡터 모두 마찬가지입니다. 하스켈의 리스트와 오카멜의 리스트는 상태가 아니고 순수합니다
오해가 많아 보여서 문단별로 간략하게 적습니다. 1. non-local한 environment에 변화가 생기는 걸 side effect라고 합니다. 다시말해 닫혀있는 상황에서 블랙박스 내에서 뭐가 일어나든 그건 side effect가 아닙니다. 2. monad를 사용하는 건 environment에 변화가 생기는 것까지 functional하게 표현하기 위한 겁니다. monad의 사용이 필요조건인 건 아닙니다. 3번 문단은 monad의 특징이라기보다는 순수 함수형 언어로 side effect를 처리하는 일반적인 원리라고 보아야 합니다.
4. environment를 데이터로 만들어서 argument로 넘기든, 함수로 넘기든, 사실 발상 자체는 똑같은 겁니다. f(0)=1, f(1)=1, f(2)=2인 함수 f랑 [0, 1, 2]는 사실상 다를 게 없는 거랑 똑같습니다. 구현만 다를 뿐입니다. 물론 하스켈러들은 이런 것까지 포함해서 하스켈을 좋아하는 거긴 하겠지만 말입니다.
5. 이건 lazy evaluation이나 normal order evaluation의 특징이지 함수형의 특징이라고 보긴 힘듭니다. 하스켈은 이렇겠지만서도요
*4번에서 argument를 data로 읽어주세요
1. 오카멜에서 함수 내부에서 ref 를 배정하고, 내부데이터만 읽고 쓰다가 밖으로 방출되지 않고 내부에서 소멸된다면, 그렇다면 전체적으로 보았을때는 순수함수가 되겠죠. 근데 하스켈에서는 프로그래머가 직접 순수성을 보장할수 있더라도 코드에 IO 모나드가 붙는거는 필연적입니다. 하스켈적인 관점으로는 부작용을 썼다고 이해하는게 더 편할거라고 생각합니다. 실제 부작용이던지 아니던지간에요. 2. 엄밀히 이야기하자면, State 모나드와 그 트랜스포머, 그리고 그 트랜스포머의 파생형인 ST와 IO의 사용이 필요조건이라고 말하고 싶은데요 (하스켈 한정), 말씀하신 바에 의하면 environment에 변화가 생기는것을 functional하게 표현하지 않아도 된다는 것으로 받아들여도 될까요?
흠.... 대충 말하시는 내용을 이해할거같습니다. 함수형언어를 하스켈과 동일시 여기면 안되겠네요
혹시 언어 어떤거 사용하시는지 여쭤봐도 될까요? 배우고 싶어서 그렇습니다. 사실 함수형 관련으로 이야기 나눌 사람이 없어서 책만 보고 뉘앙스 파악하긴 좀 어려워서... 언어마다 차이도 극명하고 (리습으로 곧 넘어갈건데 리습은 타입이 없다고하니..) 제가 궁금한게, 1. 하스켈이 이렇게 순수성에 목매다는거 효용을 잘 모르겠다 => 요새 오카멜에 빠져있는 이유 2. 분명 함수형의 특징으로 부작용이 없다는걸 얘기할때 하스켈에만 한정되는 내용이 아닐텐데, 다른 언어들이 순수성을 지키는 방식이 궁금하다
입니다
제가 하고 싶은 말은 functional하게 표현할 수 있겠지만 그 방법이 monad일 필요는 없다는 것이고, 같은 연장선상에서 하스켈일 필요도 없다 정도로 생각하시면 될 것 같습니다. IO는 monad로 포장해도 결국 side effect인 건 맞으니까 말씀하시는 게 본질적으로는 맞다고 봅니다. 다만 (함수형 공부하시는 이유가 여러가지 있겠지만) 타입추론이나 프로그램 증명 같은 게 필요하신 게 아니라면 functional이라는 용어에 너무 집착하실 필요는 없지 않나 싶습니다. 하스켈이나 coq, agda 같이 순수 함수형 언어들은 제가 알기로는 전부 monad를 사용하긴 합니다. 그거 이상의 솔루션이 없긴 할 겁니다.
4번 재밌네요. 확실히 함수를 인자로 넘긴다는것은, 가능한 모든 경우를 전부 인풋으로 때려박았다고 볼 수 있죠. 근데 그것이 바로 함수를 인자로 건네는 장점 아니겠습니까? 더 효율적인 데이터구조로 모든 경우를, 어쩌면 무한한 경우를 다룰 수 있으니까요
흠.... 하스켈에는 집착하지 않는데 제가 지금 배워둔 함수형언어가 일단은 하스켈뿐이라서 좀 왜곡됐다던지 극단적인 견해들이 많아서 별로 좋은 글이 안되어버렸을수 있겠다는 생각이 드네요. 저도 안그래도 오카멜이 너무 맘에 들어서, 하스켈 바깥의 함수형에 대해 알아가고 싶은 열망이 강한 상태입니다. 아직 많이 발전해야돼요
오카멜같은 언어는 부작용을 대하는 방법이 무엇인가요? 그냥 마구잡이로 가변 변수들을 남발할거같진 않은데
하스켈이 그짓을 해서 얻어갈 수 있는 장점은... (사람마다 다르겠지만) 프로그램 증명만큼 매력적인게 없지 않나 싶습니다. 자기가 짠 프로그램이 진짜로 원하는 기능을 수행하는지 수학적으로 검증이 가능하니까요. 그래서 순수 함수형 언어를 원하시는게 맞다면 하스켈 공부하시는 것이 맞긴 하구요.
저는 일단 지금 부작용 생각 안하고 그냥 구현의 자유로움을 만끽하고 있습니다. (이러면 안되겠지만)
ocaml은 많이 써보질 않아서 잘 모르겠네요. scheme 포함한 lisp 방언들이 재밌긴 한데, 언어 차원에서 강제하는 건 거의 없다시피 하죠. 절차형으로 해도 별 상관 없으니까요.
https://www.youtube.com/watch?v=ZgUEbbZYpYc&list=PLF1Z-APd9zK5uFc8FKr_di9bfsYv8-lbc&index=11
결국엔
이런거 하자고 하스켈 하는 거 아닌가 싶습니다.
프로그램 논증은 하스켈, 다른 함수형 언어, 명령형 언어 다 상관 없이 할 수 있음. 하스켈이 프로그램 논증에 특화된 언어도 아니고.
이상한 소리 잔뜩 싸놨네요
반대의견이 많은데 생각을 빨리 중계하셔서 잘 된듯 맞나 틀리나 확인해보기까지 시간이 훨씬 단축되셨을듯요
내말이 맞음 이러면서 우기는거만 아니면 고찰은 좋은거라고 봄
얘 몇년전에 프갤에서 유명한 어그로꾼 아니였냐? 이제 깃갤까지 넘어왔네 ㅋㅋㅋ 클났노
따군이랑 루비?
딱국이랑 루비 절친같더라
루비랑 반대로 깃갤에서 러슬람짓하면서 어그로끔
근데 당시엔 러스트 좋아하는사람이 많았는데 요즘은 러스트 유저들이 깃갤에 별로 안보이는거같음
그때나 지금이나 루비 딱국 같은 어그로꾼은 싫어함 러까냐 러빠냐 문제가 아님