2022.01.17
map()
과 같은 고계함수에 자료 변형 코드 블록으로 전달하는 것이 대표적인 예이다.def Closure makeCounter() {
def local_variable = 0
return { return local_variable += 1 }
}
c1 = makeCounter()
c1();
c1();
c1();
c2 = makeCounter()
println "C1 = ${c1()}, C2 = ${c2()}"
// C1 = 4, C2 = 1
커링이나 부분 적용 모두 몇몇 인수의 값만 주면 인수가 몇 개 빠져도 호출할 수 있는 함수를 리턴해준다. 다만 커링은 체인의 다음 함수를 리턴하는 반면에, 부분 적용은 주어진 값을 인수에 바인딩시켜서 인수가 더 적은 하나의 함수를 만들어준다.
예를 들자면 process(x,y,z)
의 완전히 커링된 버전은 proccess(x)(y)(z)
이다.
process(x,y,z)
의 인수 하나를 부분 적용하면 인수 두개 짜리의 process(y,z)
가 된다.
움직이는 부분
의 관리를 런타임에 양도하는 것이다. ( 개발자가 중간 값을 건드리지 못하게 하면 결국 그로 인한 오류도 생기지 않는다. )object CurryTest extends App {
def filter(xs: List[Int], p: Int => Boolean): List[Int] =
if (xs.isEmpty) xs
else if (p(x.head)) xs.head :: filter(xs.tail, p)
else filter(xs.tail, p)
def devidesBy(n: Int)(x: Int) = ((x % n ) == 0)
var nums = List(1, 2, 3, 4, 5, 6, 7, 8)
println(filter(nums, devidesBy(2)))
println(filter(nums, devidesBy(3)))
}
스택 증가는 재귀가 좀 더 보편화되지 못하는 주된 이유 중 하나이다. 재귀는 보통 중간 값을 스택에 보관하게끔 구현되는데, 재귀에 최적화되지 않은 언어에서는 스택 오버플로를 유발하게 된다. 개발자가 런타임이 이 문제를 처리하는데 도움을 줄 수 있는 방법 중 하나는 꼬리 호출 최적화(tail-call optimization)이다. 재귀 호출이 함수에서 마지막 단계이면, 런타임이 스택을 증가시키지 않고 스택에 놓여 있는 결과를 교체할 수 있다.
public String cleanNamesP(List<String> names) {
if (names == null) return "";
return names
.stream()
.map(e -> capitalize(e))
.filter(n -> n.length() > 1)
.collect(Collectors.joining(","))
}
2장의 예제 코드와는 다르게 map()
작업이 filter()
보다 먼저 실행된다. 명령형 사고로는 필터 작업이 먼저 와야 맵의 작업량이 줄어들기 때문에 좋지만, 함수형 언어에는 Stream이라는 추상 개념이 정의되어 있다.
Stream은 여러모로 컬렉션과 흡사하지만 바탕 값(backing value)이 없다. collect()
함수가 호출되기 전까지는 map()
과 filter()
는 게으른 함수로, 실행을 미룬다. 영리한 런타임은 게으른 작업들을 재정렬할 수 있다.
런타임에 최적화를 맡기는 것이 양도의 중요한 예이다. 시시콜콜한 세부사항은 버리고 문제 도메인의 구현에 집중하게 되는 것이다.