Emacs Lisp의 alist 사용법 간단 정리
alist
Emacs Lisp에서 alist는 Association List의 축약어로, 한국어로 직역하면 '연관 목록' 정도로 표현할 수 있는 자료구조다. 다른 언어에서 자주 취급되는 가장 비슷한 자료구조를 들자면 사전형(dictionary)가 비슷할 것 같고, Emacs Lisp 내에서는 용도 면에서 plist와도 좀 비슷한 편이다. 참고로 plist와는 다르게 alist는 Common Lisp과 사용법이 비슷한 편이다.
alist의 데이터는 키와 값이 점(.
)으로 구분되는 형식으로 표현된다. 혹은 cons
라는 함수를 이용해 키와 값을 연속으로 배열하기도 한다. 예제로 아래의 두 줄은 같은 키와 문자열 값을 표현하는 두 가지 방식이다.
'(key . "value")
(cons ('key "value'))
이런 키와 값의 쌍 데이터는 car
혹은 cdr
같은 함수를 이용해 키와 값을 분리해 낼 수 있다.
(cdr '(name . "Conrad"))
;; => Conrad
(car '(name . "Conrad"))
;; => name
사실 좀 미묘한게 앞과 뒤 어떤 게 키일까 정의할 수는 없다. 왜냐하면 alist는 키나 값 개념 자체가 없고 둘 다 기준으로 사용할 수 있기 때문이다. 다만 일반적으로 키-값(key-value) 순서가 많이 쓰여서 여기에 맞춰지는 경우가 많을 뿐이다. 이 글의 예시에서도 키-값 순서로 사용함에 주의하자.
alist 생성
alist를 생성하는 방법 중 자주 사용되는 방법으로 인용(quote)을 이용한 축약 표현이 있다.
(setq mycard
'((name . "Conrad")
(age . 20)
(phone . "82-10-1234-5678")
(address . "Earth, Milkyway, Laniakea Supercluster")
(:memo . "Lazy Human")))
이 외에 cons
를 이용할 경우는 아래와 같은 식으로도 표현할 수도 있다.
(setq mycard
(list (cons 'name "Conrad")
(cons 'age 20)
(cons 'phone "82-10-1234-5678")
(cons 'address "Earth, Milkyway, Laniakea Supercluster")
(cons :memo "Lazy Human")))
둘 다 동일한 데이터를 생성하기 때문에 원하는 방식을 사용하면 된다.
이 외에 약간 독특하게 키 리스트와 값 리스트를 이용해 alist를 생성하고 싶다면 cl-pairlis
함수를 사용해보자.
(cl-pairlis '(key1 key2 key3) '("value1" "value2" "value3"))
Common Lisp의 함수이기 때문에 Common Lisp에서는 'cl-' 접두어를 때어내고 pairlis
함수를 쓰면 될 거라 예상된다.
특정 키의 데이터 쌍 및 값 얻기
특정 키의 데이터를 읽기 위해서는 assoc
함수를 사용할 수 있다.
(assoc 'name mycard)
;; => (name . Conrad)
assoc
의 경우 다른 언어와는 다르게 값 만을 읽는게 아니라 키와 값이 연결된 데이터 한 쌍을 얻을 수 있다. 그래서 값 만을 얻기 위해서 앞서 언급한 cdr
함수를 추가로 이용해야 한다.
(cdr (assoc 'name mycard))
;; => Conrad
그런데 cdr
과 assoc
의 조합은 사실 alist-get
이라는 함수 하나로 해결하는 것도 가능하다.
(alist-get 'name mycard)
;; => Conrad
이 외에도 assoc
와 비슷하지만 비교 함수가 다른 assq
등도 있으니 필요하다면 레퍼런스를 찾아보자.
특정 값으로 데이터 쌍 및 키 얻기
보통 사전형 자료구조에선 쉽게(?) 허용하지 않는 alist의 특수한 기능으로 값을 이용해 데이터 쌍을 얻을 수 있는 방법을 제공한다. assoc
의 뒤집어진(reversed?) 형태인 rassoc
함수를 사용한다.
(rassoc 20 mycard)
;; => (age . 20)
이번에도 키와 값 쌍을 얻을 수 있는데 여기서 키를 분리해 내고 싶다면 앞서 언급한 car
함수를 이용해 뽑아낼 수 있다.
(car (rassoc 20 mycard))
;; => age
하지만 키-값 기반의 데이터를 운용할 때는 아무래도 키 기준을 사용하는 경우가 대부분이라 이 기능을 쓰게 될 일이 있을지는 잘 모르겠다.
alist에 데이터 쌍 추가하기
이미 만들어진 alist 인스턴스에 새로운 키-값 쌍을 추가하는 대표적인 방법으로 Emacs 설정 중 자주 볼 수 있는 add-to-list
함수를 사용할 수 있다. 예를 들어 아래는 mycard라는 alist를 생성하고 여기에 nick과 job이라는 두 개의 키와 값을 추가하는 예제다.
(setq mycard
'((name . "Conrad")
(age . 20)
(phone . "82-10-1234-5678")
(address . "Earth, Milkyway, Laniakea Supercluster")
(:memo . "Lazy Human")))
(add-to-list 'mycard (cons 'nick "Seorenn"))
(add-to-list 'mycard '(job "Untitled"))
그밖에 push
함수를 사용할 수도 있다.
(push (cons 'nick "Seorenn") mycard)
(push '(job . "Untitled") mycard)
참고로 add-to-list
나 push
함수는 리스트에 끼워 넣기만 하기 때문에 해당 키의 값을 변경하는 용도로는 사용할 수 없다. 애초에 alist는 키 개념이 없기도 하고 따라서 키를 중복시키는 것도 가능하다.
이 외에 일반 리스트처럼 append
도 쓸 수 있지 않을까 예상된다.
alist에서 데이터 쌍 삭제하기
alist에서 특정 키를 가진 데이터 쌍을 모두 삭제하기 위해서 assq-delete-all
함수를 사용할 수 있다.
(assq-delete-all 'job mycard)
왜 -all이라는 접미어가 붙어있을까는 유니크 키 개념이 없다는 점 때문이라고 생각하면 된다. 중복되는 키가 있을 수 있기 때문에 몽땅 지워버린다는 의미 또한 충분히 있을 수 있다.
alist에서 특정 키의 값 수정하기
가장 간단한 방법으로 setf
함수를 이용할 수 있다.
;; (setf (alist-get KEY ALIST) NEW-VALUE)
(setf (alist-get 'nick mycard) "another nick name")
사용법은 주석에 적은 대로다. 약간 복잡한 것 같지만 키의 데이터 쌍을 다 삭제하고 다시 추가하는 것보다는 간단할 것 같다.
코드 상으론 동작이 약간 이해되지 않는데, 아마도 setf
는 레퍼런스 혹은 포인터가 가 가리키는 메모리의 값을 갱신하는 것 같은 느낌이다.
마무리 및 관련된 글들
사실 소개 수준의 짧은 글을 쓰려고 했는데 예제가 생각보다 길어졌다. alist는 사전형이나 해시테이블과는 다르게 '연관된 데이터 묶음'에 가깝기 때문에 그 부분을 더 설명할 수밖에 없었던 것 같다. 그래서인지 이 외애도 많은 내용이 존재한다. 예를 들자면 테스트 함수 같은 개념도 있다. 상당히 많은 기능이 더 있으니 궁금하다면 링크된 관련글에서 내용을 더 찾아보자.