Emacs Lisp의 alist 사용법 간단 정리

Emacs // 2024년 06월 27일 작성

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

그런데 cdrassoc의 조합은 사실 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-listpush 함수는 리스트에 끼워 넣기만 하기 때문에 해당 키의 값을 변경하는 용도로는 사용할 수 없다. 애초에 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는 사전형이나 해시테이블과는 다르게 '연관된 데이터 묶음'에 가깝기 때문에 그 부분을 더 설명할 수밖에 없었던 것 같다. 그래서인지 이 외애도 많은 내용이 존재한다. 예를 들자면 테스트 함수 같은 개념도 있다. 상당히 많은 기능이 더 있으니 궁금하다면 링크된 관련글에서 내용을 더 찾아보자.