함수형 사고

date
Mar 19, 2024
slug
functional-thinking
status
Published
tags
PL & Compiler
summary
type
Post

패러다임 전환 - p18

1967년에 만들어진 시뮬라 67은 최초의 객체지향 언어였지만 1983년에 등장한 C++가 보편화된 후에야 주류가 되었다. → 종종 훌륭한 아이디어는 기반이 되는 기술이 쫓아오기를 기다린다.
함수형 프로그래밍도 지난 2 ~ 30년간 학계에서 연구되었고, 현대 프로그래밍 언어들에 조금씩 도입되고 있다.
존 베틀리가 도널드 커누스에게 요구한 기능 → 텍스트 파일을 읽고, 가장 많이 사용된 단어들을 찾고, 그 단어들과 빈도를 정렬된 목록으로 출력하라.
찾은 단어가 처음이라면 Map에 추가하고, 처음이 아니면 빈도 수를 증가하는 평범한 코드이다.
다음은 Java Stream API, 람다 블록을 이용한 고계함수(map, filter 등)를 사용해서 만든 코드이다.
명령형 프로그래밍을 하다 보면 효율을 높이기 위해 여러 작업을 한 루프에 넣음으로써, 작업들을 복잡하게 하는 경우가 종종 있다.
함수형 프로그래밍에서는 map, filter 같은 고계함수를 사용하여 추상화의 단계를 높여서 문제를 더욱 명료하게 볼 수 있다. → for 문을 작성하여 반복 처리하는 방법에서 고계함수를 사용하여 런타임에 양도하라는 것이다.
또한 첫 번째 코드를 멀티 쓰레드로 동작하고 싶다면 쓰레드 관련 코드가 문제 해결 코드에 섞이면서 복잡하고 크기가 커지게 되는데, 자바에서는 stream()parallelStream() 으로 바꿔주면 병렬 스트림이 생성된다. → 저수준의 세부적인 최적화는 런타임이 담당하게 하면 된다.
이러한 기능은 클로저로부터 온 것이라고 볼 수 있다. 코드와 문맥을 한 구조로 캡슐화해서 만들어진 모델에 병렬 처리 유무만 갈아 끼우면 되는 것이다.

적은 수의 자료구조, 많은 연산자 - p111

함수형 언어에서의 코드 재사용은 객체지향 언어와 접근 방법이 다르다.
객체지향 언어는 수많은 자료구조와 거기에 딸린 수많은 연산을 포함하고 있다. → 클래스에 종속된 메서드를 만들어서 반복되는 패턴을 재사용하려고 한다.
함수형 언어는 적은 수의 자료구조와 많은 연산들이 존재한다. → 자료구조에 대해 공통된 변형 연산을 적용하고, 특정 경우에 맞춰서 주어진 함수를 사용하여 작업을 커스터마이즈함으로써 재사용을 장려한다. → 몇몇 주요 자료구조(list, set, map)와 거기에 따른 최적화된 연산들을 선호한다.

filter - TODO

qwewqe

map - TODO

flatMap (평탄화), collect

fold/reduce - TODO

asdasd

클로저 (Closure) - p62

명령형 언어는 상태로 프로그래밍 모델을 만든다. 그 좋은 예가 매개변수를 주고 받는 것이다.
클로저는 코드와 문맥을 한 구조로 캡슐화해서 행위의 모델을 만들 수 있게 해준다. 이렇게 만들어진 클로저는 마치 전통적인 자료구조처럼 주고받을 수도 있고, 적절한 시간과 장소에서 실행할 수도 있다. (Blog) → 1급 함수

지연 실행 (deferred execution)

클로저 블록에 코드를 바인딩함으로써 그 블록의 실행을 나중으로 연기할 수 있다. 예를 들어 클로저 블록을 정의할 때는 필요한 값이나 함수가 스코프에 없지만, 나중에 실행 시에는 있을 수가 있다. - p86

커링 (Currying)과 부분 적용(partial application) - p66

커링: 다인수 함수를 일인수 함수들의 체인으로 바꿔주는 방법 (ex: process(x, y, z)process(x)(y)(z))
부분 적용: 주어진 다인수 함수 중에서 인수의 값을 미리 정해서 생략 시키는 방법 → 적은 인수를 받는 함수로 변형하는 방법 (ex: process(x, y, z) - 인수 하나를 부분 적용 → process(y, z) )

재귀 - p75

런타임에 제어를 양도한다는 함수형 프로그래밍 개념에서 재귀는 깊은 관계가 있다. → 같은 메서드를 반복하여 호출하는 과정이 런타임에게 양도하는 예라고 볼 수 있다.
양도한다는 개념이 무엇인지 와닿지 않을텐데 아래 코드를 보면 이해할 수 있다.
두 코드의 차이점은 “누가 상태를 관리하는가?”이다. → 첫 번째 코드인 명령형 버전에서는 new_list라는 새 인수를 생성하고 계속 추가하며, 끝나면 반환하는데 이때 new_list개발자가 관리한다는 것이다. → 두 번째 코드인 재귀 버전에서는 메서드 호출 시마다 return 값을 스택에 쌓아가면서 관리한다. 즉 new_list에 대한 책임을 양도하고 언어 자체가 그것을 관리해준다.
new_list라는 변수를 만들고 추가하는 과정 자체가 사이드 이펙트라고 볼 수도 있다. 그러나 재귀 버전에서는 이러한 변수 자체를 만들지 않는 것이다.
가비지 컬렉션처럼 대단한 발전은 아니지만 재귀는 프로그래밍 언어의 중요한 흐름을 조명해준다. ‘움직이는 부분’의 관리를 런타임에 양도하는 것이다. 개발자가 중간 값을 건드리지 못하게 하면 결국 그로 인한 오류도 생기지 않을 것이다. - p79
양도의 다른 예시 - p152
위 코드에서 유동적인 동작을 위해 if 조건문을 사용하여 빈 리스트인지 null인지 등을 확인한다고 가정해보자. groovy에서는 ?. 라는 문법적 설탕 (syntactic sugar)을 사용하여 if 문으로 빈 리스트인지, null인지 등을 추가할 필요 없이 그저 언어에게 양도하는 것이다.
before, after를 보면 최소한의 코드만 변경하여 재사용하고 있다. - 보일러플레이트 코드를 표현이 풍부한 코드로 대체 가능
?. 에는 함수형의 성질이 없지만, 골치 아픈 일들을 런타임에 맡기는 예제로는 안성맞춤이다. - p152

꼬리 호출 최적화 (tail-call optimization) - p79 ~ 80

재귀는 느리고 스택이 쌓이면서 메모리 낭비가 심하다는 것으로 알고 있다. - 스택 오버플로우 유발
스칼라, 클로저 같은 언어들은 이 제약을 몇 가지 방법으로 우회한다.
개발자가 런타임에서 이러한 문제를 처리하는 데 도움을 줄 수 있는 방법 중 하나는 꼬리 호출 최적화이다.
재귀 호출이 함수에서 마지막 단계이면, 런타임이 스택을 증가시키지 않고 스택에 놓여 있는 결과를 교체할 수 있다.

스트림과 작업 재정렬 - p80 ~ 81

명령형 사고로는 filtermap 보다 먼저 와야 한다. 필터된 데이터로 map 을 사용해야 작업의 양이 줄어들기 때문이다.
하지만 자바 stream 에는 추상 개념이 정의되어 있다.
시작점인 names 컬렉션과 종점인 collect 함수 사이에서 동작하는 map , filterlazy(게으른) 함수이다. - 목적지에서 요구하기 전까지 실행을 미룬다. (결과를 내려고 시도하지 않는다.)
영리한 런타임은 filtermap 작업 전에 실행하여 효율적으로 재정렬할 수도 있다.
즉 개발자의 입장에서는 mapfilter 를 사용하겠다고 명시만 하고 최적화나 재정렬 등의 작업은 런타임에게 맡기는 것이다. (양도하는 것이다.)
런타임에 최적화를 맡기는 것이 양도의 중요한 예이다. 시시콜콜한 세부 사항은 버리고 문제 도메인의 구현에 집중하게 되는 것이다.

메모이제이션 (Memoization) - p83

시간이 많이 걸리는 연산을 반복적으로 사용해야 할 때 보편적인 해결 방법은 내장 캐시를 사용하는 방법이다. (이미 수행된 결과를 재사용하는 것)
주어진 매개변수를 사용하여 연산을 할 때마다 매개변수를 키 값으로 하는 캐시에 값을 저장한다. 후에 이 함수가 같은 매개변수로 호출되면 다시 연산하는 대신에 캐시의 값을 리턴한다.
메모아이즈하는 대상은 불변(immutable)해야 한다는 것이다. - 함수형 프로그래밍에서 불변하다는 특징 메모아이즈된 함수의 결과가 매개변수 이외의 어떤 것에라도 의존한다면 기대하는 결과를 항상 얻을 수 없다. - 사이드 이펙트가 존재한다면 캐시된 값이 return되어도 변하지 않았는지 신뢰할 수 없기 때문이다.
  • 부수효과가 없어야 하고
  • 외부 정보에 절대로 의존하지 말아야 한다.

함수형 프로그래밍 언어에 대한 고찰

게으른 평가 (Lazy Evaluation) - p95

평가를 가능한 최대로 늦추는 기법인 게으른 평가는 함수형 프로그래밍 언어에서 많이 볼 수 있는 기능이다. 게으른 컬렉션은 그 요소들을 한꺼번에 미리 연산하는 것이 아니라, 필요에 따라 하나씩 전달해준다.
이렇게 하면 시간이 많이 걸리는 연산을 필요할 때까지 미룰 수 있게 된다. → 요청이 계속되는 한 요소를 계속 전달하는 무한 컬렉션을 만들 수 있다.
컬렉션을 전부 유지하지 않고 순차적으로 다음 값을 유도하기 때문에 모든 표현을 바로 평가하는 방식에 비해 크기가 작다는 장점도 있다. 게으른 평가를 사용하고 말고의 결정은 값을 저장하는 것과 새 값을 계산하는 것 사이의 trade-off이다.
빅데이터 분야에서 Apache Spark, Apache Flink와 같은 프레임워크를 사용하는데 여기서 핵심이 lazy하게 동작할 수 있다는 것이다.

함수형 오류 처리 - p126 - TODO

type-safe, Either 클래스, 모나드?, …

OOP의 재사용, FP의 재사용 - p160 - TODO

객체지향의 목적은 캡슐화와 상태 조작을 쉽게 하는 것이다. 그래서 객체지향형 추상화는 문제 해결을 위해 주로 상태를 이용한다. → ‘움직이는 부분’인 클래스와 클래스 간의 상호 관계를 주로 사용
함수형 프로그래밍은 구조물들을 연결하기보다는 부분들로 구성하여 움직이는 부분을 최소화하려고 노력한다. 객체지향 언어의 경험만 있는 개발자들은 이 미묘한 개념의 차이를 쉽게 보지 못한다.
두 클래스에 존재하는 isFactor, factors 메서드가 중복 코드이기 때문에 리팩토링이 필요하다.
공통 코드를 묶고, 이를 상속하는 방식으로 코드를 재사용할 수 있었다. 이는 OOP를 배웠더라면 흔하게 접할 수 있는 사례이다.
이제 위 코드를 함수형 관점으로 접근하여 점점 수정한다.
객체지향은 코드 분리와 재사용 방식인 커플링 구조를 볼 수 있다. → 무성하게 커플링된 구조물들을 이해해야 하는 어려움 때문에 객체지향 언어에서는 코드 재사용이 피해를 많이 입었다.
함수형 스타일의 경우 코드 재사용에 커플링 대신 구성을 사용하였다. - p168 → ..?

참고 자료


© hyuunnn 2024