Tag Archives: Functional Programming

함수 계산의 지연성과 엄격성

엄격성(strictness)

함수의 엄격성(strictness)이란 ‘함수의 계산 순서가 엄격하게 지켜지는가’에 관한 특성이다. 일반적으로 함수나 계산식이 계산되는 방향은 안쪽에서 바깥쪽을 향한다. 즉 a + (b * (c / d))와 같은 연산이 주어졌을 때, 엄격성에 맞춰(일반적인 방향에 따라) 연산의 단계를 밟게 되면 다음과 같이 부분 연산의 과정이 진행되게 된다. 그리고 이렇듯 부분적인 연산을 통해 전체 결과를 도출해가는 과정을 리덕션(reduction)이라 한다.

  1. 식에는 a = 4, b = 3, c = 2, e = 1을 대입하고, f는 계산의  최종 결과라고 해보자.
  2. f = a + (b * 2)  : 2는 c / d의 결과
  3. f = a + 6 : 6은 b * 2의 결과
  4. f = 10 : 10은 a + 6의 결과

이는 일반적으로 널리 사용되는 프로그래밍 언어에서 따르고 있는 계산의 기본적인 방식이기도 하다. 일반적인 언어(예, 자바, C++ 등)에선 중첩된 함수의 호출은 콜 스택을 증가시키며, 가장 마지막(가장 안쪽) 함수를 가장 먼저 계산하게 된다.
하지만, 스칼라와 같은 함수형 언어에선 이런 일반적인 계산 순서를 준수하지 않을 수 있는 비엄격성(non-strictness)의 구현 방법을 제공한다. 함수형 언어에선 함수 자체를 저장하거나 전달할 수 있기 때문에, 함수 안쪽에 중첩돼 있는 함수의 결과를 알지 못하더라도 바깥쪽의 결과만을 미리 확정하여 활용할 수 있다. 즉, 바깥쪽에서 안쪽으로 계산을 진행할 수 있다. 앞서 살펴본 계산의 예제를 비엄격성에 따라 풀어보자면 다음과 같은 계산의 진행(reduction)이 가능해진다.

  1. 위의 계산에서와 같은 조건을 적용하자.
  2. f = 4 + g : 여기서 g는 아직 계산되지 않은 (b * (c / d))에 해당하는 함수다.
  3. f = 4 + (3 * h) : 여기서 h는 아직 계산되지 않은 (c / d)에 해당하는 함수다.
  4. f = 4 + (3 * (2 / 1)) = 10

비엄격성을 지원하는 함수형 언어에선 중간 계산 과정(아직 결과를 확정할 수 없는)에 해당하는 2번과 3번 상황의 함수 f를 변수에 저장하거나 다른 함수로 전달할 수 있다. 일반적인 순서에 따른 계산(엄격한 계산)에서 가장 먼저 수행돼야 할 가장 안쪽에 위치한 하위 계산식을 바텀(bottom)이라 하는데, 비엄격성을 따르게 되면 바텀에 도달하지 않고도(먼저 계산하지 않더라도) f의 값을 활용할 수 있다. 이런 식으로 역방향(바깥쪽에서 안쪽으로)으로 함수의 계산을 진행할 수 있는 성질을 비엄격성이라 하며, 이는 이어서 살펴볼 지연성의 장점을 활용하는 기반이 된다.


 지연성(laziness)

지연성이란 프로그래밍 언어적 특성을 벗어나 더 넓은 범위에서 활용되는 개념으로써, ‘필요한 순간까지 계산을 미루는’ 성질을 의미한다. 예를 들어, JPA에선 외래키로 연결된 데이터를 가져오는 전략으로써 지연성을 지정할 수 있는 어노테이션을 제공하고 있으며, 실제로 지연성이 명시된 경우 하이버네이트는 실제 해당 필드에 접근이 일어나지 않는다면 DB에서 관계된 데이터를 미리 가져오지 않는다.
스칼라와 같은 함수형 언어에선 비엄격성에 기반해, 함수 계산에서 지연성을 활용할 수 있는 방법을 함께 제공하고 있다. 앞서 살펴본 두 번째 계산 예제에선 비엄격성에 따라 g나 h에 해당하는 하위 계산식이 먼저 계산되지 않을 수 있었는데, 프로그래밍 언어적 측면에선 g나 h와 같이 아직 계산되지 않은 하위 계산식(또는 함수)를 크(thunk)라는 개념적 그릇에 보관하게 된다. 물론 전혀 계산되지 않은 f자체도 하나의 성크로 지정될 수 있으며, 언어에선 실제로 필요한 순간이 올 때에만 해당 성크 내부에 담겨있는 표현식과 데이터를 이용해 해당하는 하위 계산식의 계산을 진행하게 된다. 즉, 스칼라와 같은 언어에선 함수를 변수에 연결(bound)하는 순간에 즉시 계산할 필요 없이, 해당 함수를 성크로 보관해 두고 필요한 순간이 왔을 때에야 비로소 계산이 진행되도록 하는 장치를 제공한다. 그리고 이를 함수의 지연 계산(lazy evaluation)이라 한다.
이런 언어적 장치를 활용하면 복잡한 계산이 불필요하게 먼저 수행될 필요 없이, 필요한 경우에 한해 계산되도록 하여 리소스를 절약할 수 있다. 하지만 지연 계산을 위해선 성크를 구성하는 비용이 추가되고, 필요한 시점이 됐을 때 계산의 결과를 기다려야만 하는 페널티를 감수해야 하기 때문에, 적절한 상황에 한해서 활용해야 할 장치다.


스칼라와 지연 계산

스칼라에서 지연 계산을 위한 두 가지 장치를 제공하고 있다.

지연 변수(lazy val)

스칼라에선 변수를 선언할 때 lazy 키워드를 사용해서 지연 계산의 속성을 부여할 수 있다. lazy로 선언된 변수는 실제로 사용(접근)되기 전엔 계산되지 않는다. 일단 계산이 일어나면 그 결과는 내부적으로 캐싱되어 재접근이 일어날 경우에도 계산을 다시 수행하지 않는다. 그리고 동시성을 고려해야 하는 변수를 lazy로 선언했다 하더라도, 최대 1회 계산이 보장된다.

콜-바이-네임(call-by-name)

함수형 언어인 스칼라도 자바와 같은 객체지향 언어와 같이, 함수(메소드)의 호출은 콜-바이-벨류(call-by-value)로 이뤄진다. 함수를 파라미터로 전달할 땐 함수 자체가 전달되는 것이 아니라 함수 수행의 결과가 전달된다. 즉, 함수는 다른 함수로 전달되기에 앞서 그 값이 계산된다. 하지만 콜-바이-네임의 방식을 지정게 되면, 함수에 해당하는 값이 아니라 함수 자체를 가리키는 이름을 전달하게 되며, 해당 함수는 전달받은 함수 내부에서 명시적으로 호출돼야만 실행되어 계산 결과 값을 얻을 수 있게 된다. 단, 이는 함수 자체를 전달하고자 하는 목적에 초점을 둔 개념일 뿐이며, 그에 따른 계산의 지연은 부수적이며 제한적인 결과다. 따라서, 지연 변수와는 달리 함수의 수행 결과를 캐싱하는 효과는 얻을 수 없으며, 전달받은 함수 내부에서 호출되는 횟수 만큼 실제 계산이 진행된다.


 레퍼런스