tools/vim

Learn Vimscript The Hard Way - 39. Functional Programming

seul chan 2020. 4. 19. 21:21

Functional Programming

잠시 휴식을 갖고 프로그래밍 스타일에 대해 이야기하는 시간을 가져볼 것이다.

functional programming에 대해 들어본 적이 있을 것이다.

만약 당신이 Python, Ruby, Javascript, 특히 Lisp, Scheme, Clojure, Haskell 등의 프로그래밍 언어를 사용해 보았다면 이 방식에 아주 익숙할 것이다.

vimscript는 kind-of-functional 스타일이 필요한데, 조금은 구식이다. 이를 조금 덜 고통스럽게 하기 위한 몇몇 헬퍼를 만들 수 있다.

functional.vim을 만들어보자. 다음 장에서는 해당 파일을 이용하여 예시를 작성할 것이다.

Immutable Data Structures

불행히도 vim에는 Clojure의 vector나 map같은 immutable collection이 존재하지 않는다. 하지만 이를 만들어 볼 수 있다.

위 파일에 다음 함수를 입력해보자

function! Sorted(l)
    let new_list = deepcopy(a:l)
    call sort(new_list)
    return new_list
endfunction

이를 source 하고 (:source %) 다음을 실행시켜보자.

echo Sorted([3, 2, 4, 1])
[1, 2, 3, 4]

빌트인 sort와 무엇이 다를까? 핵심은 첫 줄의 let new_list = deepcopy(a:l)이다. vim의 sort() 함수는 해당 리스트 자체를 변경하기 때문에 우리는 해당 리스트를 full copy하여 원본 리스트가 수정되지 않도록 하였다.

:let original_list = [3, 2, 4, 1]
:call sort(original_list)
:echo original_list
[1, 2, 3, 4]

:let original_list = [3, 2, 4, 1]
:let sorted_list = Sorted(original_list)
:echo sorted_list
[1, 2, 3, 4]
:echo original_list
[3, 2, 4, 1]

이는 사이드 이펙트를 방지해주고 코드를 조금 더 쉽게 적을 수 있게 해 준다. 비슷한 스타일로 몇 가지 헬퍼 함수를 더 작성해보자.

function! Reversed(l)
    let new_list = deepcopy(a:l)
    call reverse(new_list)
    return new_list
endfunction

function! Append(l, val)
    let new_list = deepcopy(a:l)
    call add(new_list, a:val)
    return new_list
endfunction

function! Assoc(l, i, val)
    let new_list = deepcopy(a:l)
    let new_list[a:i] = a:val
    return new_list
endfunction

function! Pop(l, i)
    let new_list = deepcopy(a:l)
    call remove(new_list, a:i)
    return new_list
endfunction

각각의 함수들은 받는 인자와 실행 함수를 제외하고는 거의 같다. 이를 source 해보고 실제로 실행시켜보자.

  • Reversed()는 리스트를 역전시킨 새로운 리스트를 반환한다.
  • Append()는 해당 value를 추가한 새로운 리스트를 반환한다.
  • Assoc()(associate)는 해당 인덱스의 값을 value로 변경한 새로운 리스트를 반환한다.
  • Pop()은 해당 인덱스의 값을 제거한 새로운 리스트를 반환한다.

Functions as Variables

vimscript는 함수를 변수로 저장할 수 있게 해준다.

:let Myfunc = function("Append")
:echo Myfunc([1, 2], 3)
[1, 2, 3]

우리가 사용한 변수 (Myfunc)가 대문자로 시작되는 것에 주의하라. vimscript의 변수가 함수를 지칭한다면 반드시 대문자로 시작하여야 한다.

소문자로 입력할 경우 E704: Funcref 변수명은 대문자로 시작해야 함: myfunc와 같은 에러가 발생한다.

함수는 다른 모든 변수들처럼 list에 저장될수도 있다.

:let funcs = [function("Append"), function("Pop")]
:echo funcs[1](['a', 'b', 'c'], 1)
['a', 'c']

funcs는 함수 자체가 아니라 리스트이기 때문에 소문자로 시작되어도 아무런 문제 없이 작동한다.

Higher-Order Functions

이제 자주 쓰이는 higher-order 함수(고차함수)들을 만들어보자. 고차함수는 다른 함수들을 받는 함수라고 이해하면 된다.

map 함수로부터 시작해보자. 우리가 만든 파일에 다음 내용을 추가해보자.

function! Mapped(fn, l)
    let new_list = deepcopy(a:l)
    call map(new_list, string(a:fn) . '(v:val)')
    return new_list
endfunction

source를 하고, 다음 명령어를 실행시켜보자.

:let mylist = [[1, 2], [3, 4]]
:echo Mapped(function("Reversed"), mylist)
[[2, 1], [4, 3]]

Reversed() 함수가 적용된 것을 볼 수 있다.

Mapped()가 어떻게 작동하는것일까? deepcopy()로 새로운 리스트를 만들고, 이를 반환하는 로직은 새로울 게 없다.

Mapped()는 두 개의 인자를 받는다. funcref(vim의 용어로 "function을 담은 변수")와 리스트이다. 우리는 빌트인 map() 함수를 사용하여 실제 작업을 한다. :help map()을 읽어서 어떻게 작동하는지 알아보자.

map({expr1}, {expr2}): {expr1}은 리스트나 딕셔너리를 받고, {expr2}는 string이나 Funcref를 받는다. 각각의 expr1의 값을 expr2의 결과로 수행한다. (python의 map과 흡사하다고 이해하면 편할듯)

이제 다른 몇몇 고차함수를 더 만들어보자.

function! Filtered(fn, l)
    let new_list = deepcopy(a:l)
    call filter(new_list, string(a:fn) . '(v:val)')
    return new_list
endfunction

다음 명령어로 만든 Filtered()를 실행시켜보자.

:let mylist = [[1, 2], [], ['foo'], []]
:echo Filtered(function('len'), mylist)
[[1, 2], ['foo']]

Filtered()는 함수와 리스트를 받아서 처리해주는 역할을 한다. 위의 예시에서는 len() 함수를 사용하여 length가 0인 값들을 제거하였다.

마지막으로 Filtered()와 반대되는 함수를 만들어보자.

function! Removed(fn, l)
    let new_list = deepcopy(a:l)
    call filter(new_list, '!' . string(a:fn) . '(v:val)')
    return new_list
endfunction

Filtered()를 사용한 것처럼 사용해보자.

:let mylist = [[1, 2], [], ['foo'], []]
:echo Removed(function('len'), mylist)
[[], []]

Removed()Filtered()와 똑같지만 제거되는 값들만 남겨놓는다.

두 코드의 유일한 다른 점은 '!' .를 추가한 것이다.

Performance

만약 이렇게 리스트를 만드는 것이 낭비라고 생각한다면, 그 말이 맞다. vim의 리스트는 Clojure의 벡터같은 구조적 공유는 제공하지 않는다. 따라서 모든 copy operation은 상당히 비싼 값을 치룬다.

이는 가끔 고려해야 할 수 있다. 만약 많은 수의 리스트를 다룬다면, 느려질 수 있다. 하지만 일상 생활에서는 이 차이를 느끼기가 아주 어렵다.

이를 보여주기 위해 저자는 자신의 vim이 (여러 플러그인을 사용하면서도) 노트북의 8기가바이트 메모리 중에 80메가바이트의 메모리밖에 차지하지 않는다고 얘기한다. 따라서 몇개의 리스트 copy는 거의 영향을 미치지 않는다. (firefox는 1.22 기가바이트를 사용중)

Exercises

  • Read :help sort().

  • Read :help reverse().

  • Read :help copy().

  • Read :help deepcopy().

  • Read :help map() if you haven't already.

  • Read :help function().

  • Modify Assoc(), Pop(), Mapped(), Filtered() and Removed() to support dictionaries. You'll probably need :help type() for this.

type은 type(inter)이 3일 경우 list, 4일 경우 dictionary이다.

Assoc()Pop()은 dictionary도 동일하게 사용할 수 있다.

  • Implement Reduced().

  • Pour yourself a glass of your favorite drink. This chapter was intense!