PHP에서 람다 구현
$add = def($a, $b)?
$a + $b
:fed();
assert($add->call(1, 2) == 3);
위와 같은 것이 가능하다. 물론 익명 함수에다 표현식이므로, 굳이 $add라는 이름을 부여하지 않고 사용해도 된다.
usort(
$strings,
def($a, $b)? strcmp($a, $b) :fed()->toCallback()
);
단순히 익명 함수가 아니라, 클로져(closure)를 구현한 것이므로 아래와 같은 것도 가능하다.
$x = 1;
$addX = def($y)?
$x + $y
:fed();
assert($addX->call(1) == 2);
++$x;
assert($addX->call(1) == 3);
자세히 보면 알 수 있겠지만, 구현은 삼항연산자의 특성을 이용해서 했다. 삼항연산자는 첫 피연산자 표현식이 거짓(false)으로 평가될 경우 두번째 피연산자를 평가하지 않고 무시한다. 설령 없는 변수명, 없는 함수 따위를 언급하더라도 말이다. def() 함수는 무조건 false를 반환하는데, 그렇게 될 경우 람다 함수 본체가 들어있는 가운데 피연산자 부분은 평가되지 않고, 마지막 피연산자 평가식인 fed()를 평가하게 된다(fed는 def를 뒤집은 것이다). def()는 false만 반환하긴 해도, 내부적으로 람다 함수를 생성하는 부분이 구현되어 있고, 생성된 함수 객체는 어딘가에 보관되어 있다가, fed()가 호출될 때 그것을 꺼내 반환한다.
결국 삼항연산자 표현식 전체는 하나의 함수 객체로 평가되는 것이다. 게다가 실제로 함수 객체를 반환하는 fed() 함수는 마지막 피연산자로 위치하기 때문에 메서드 체이닝도 가능하다. (... :fed()->call() 같이.)
아, 인자에 기본값을 줄 수도 있다.
$x = 'ab';
$f = def($a, $b = $x.'cd')? substr($b, $a) :fed();
일반 함수에서의 인자 기본값은 상수 표현식만 가능하게 되어 있는데—사실 PHP에서의 상수 표현식이라는 것은 이상야릇한 놈이다—람다 함수는 사실 일반 함수 호출 표현식으로 DSEL을 구현한 것이므로, 아무런 표현식이나 가능하다. (일반 함수였다면 기본값으로 $x.'cd' 같은 것은 쓸 수 없다.)
다만, 람다 함수의 시그너쳐는 내부적으로 소스 파일을 스캔해서 호출한 쪽의 줄 번호 따위를 가지고 해석하는 방식이라, 람다 함수 안쪽에서 람다 함수를 생성하는… 고차 함수(higher-order function) 객체는 만들 수 없다. 완전히 불가능한 것은 아니고, 람다 함수를 생성하는 함수 이름이 다르면 가능하긴 하다. 이제 고차 함수도 아무 문제 없이 잘 작동한다.
$ho_func = def()?
Lambda::begin($x, $y)? $x + $y :fed()
:fed();
assert($ho_func->call()->call(1, 2) == 3);
(사실 Lambda::begin()과 Lambda::end()로 람다를 생성하는 건데, 그게 너무 길어서 def()와 fed()라는 단축 함수도 구현했다.)
또 문제점이 하나 더 있는데, 아직 클로져가 완전하지 않다는 점이다. 전역에서 생성된 람다의 경우 $GLOBALS 덕분에 모든 전역 변수를 클로즈할 수 있지만, 함수 안쪽에서 생성된 람다의 경우 지역 변수를 접근할 수 있는 방법이 PHP에서 딱히 없어서 모든 변수가 클로즈되지는 않고, 정적 변수(static variables)와 인자만 클로즈된다. (전자는 ReflectionFunction과 ReflectionMethod를 통해 구현했고, 후자는 debug_backtrace() 함수를 통해 구현했다.)
function ho_func($a, $b) {
static $c = 123;
return def($x)?
$x + $a + $b + $c
:fed();
}
assert(ho_func(1, 2)->call(3) == 1 + 2 + 3 + 123);
위와 같이 정적 변수($c)나 인수들($a, $b)은 잘 클로즈되지만,
function ho_func() {
$local = 123;
return def()? $local :fed();
}
위와 같이 함수 안쪽에서 새로 생성된 지역 변수($local) 같은 경우에는 클로즈가 되지 않는다. 람다 함수 안쪽에서 쓰인 $local은 정의되지 않은 변수로 취급될 것이다.
명시적으로 전달해서 클로즈하는 편법이 있긴 하다. 관련 글 참고.
내가 지금까지 PHP에서 별 삽질을 다 해봤고, 그 중 가장 숭고했던(?) 삽질이 람다 함수를 구현하는 것이었는데, 지금까지 총 세 개의 구현이 있었다. 그 중에서 제일 나은 구현이라고 생각한다. 일단은 하나의 표현식만 갖을 수 있는 람다 함수를 구현했는데(Python의 lambda처럼), 기본적인 구현 방식만 가지고서 while 등을 사용하면 여러 문장을 포함할 수 있는 클로져 구현도 가능할 것이다.
이 구현은 Phunctional 최신판에 들어가있고, 자세한 것은 소스 코드나 레퍼런스 문서를 보면 되겠다.
덧. 정말 오랜만의 포스팅인데도 불구하고, 들떠서 쓰다보니 횡설수설…
덧2. 의외로 모르는 사람들이 많은데, PHP에는 create_function()라는 함수가 있다. 함수를 만들어주는 함수다. 그런데 이건 클로져가 아니다. 게다가 코드를 문자열 리터럴 안쪽에 우겨넣어야 하기 때문에 조금 복잡한 표현식은 작성하기 곤란하다.

September 17th, 2007 at 10:59 AM
놀랍습니다!!
March 1st, 2008 at 1:20 PM
[…] 전혀 다르고, 새로운 구현이 훨씬 깔끔하고 직관적이니 참고 바란다. PHP에서 람다 구현, Phunctional Lambda, 함수 내에서 지역 변수 클로즈하기, Phunctional Lambda: 이제 […]
March 21st, 2008 at 12:04 AM
[…] 지난 포스팅에서 언급했듯, Phunctional Lambda는 클로져를 완벽하게 지원하고 있지 않다. 함수 내에서 생성된 지역 변수들은 클로즈할 수 없는 것이다. 아마 이 기능은 내가 Lambda를 C로 작성하기 전까지는 구현할 수 없을 것이다. […]