ADJOIN — 중복 없이 항목 추가

ADJOIN — 중복 없이 항목 추가

ADJOIN은 함수예요. 리스트에 새 항목을 추가하고 싶은데, 중복은 피하고 싶을 때 써요. 직접 cons로 항목을 붙이면 중복 여부를 신경 써야 하지만, adjoin은 "이미 있는 항목이면 그대로 두고, 없을 때만 추가"하는 동작을 한 번에 처리해 줘요.

출처: CLHS Function ADJOIN

본문

시그니처는 이렇게 생겼어요.

adjoin item list &key key test test-not => new-list

인자들을 살펴볼게요.

  • item — 객체. 추가할 대상이에요.
  • list — 정상 리스트(proper list).
  • test — 두 인자를 받아 generalized boolean을 돌려주는 함수의 디자이네이터(designator).
  • test-not — 두 인자를 받아 generalized boolean을 돌려주는 함수의 디자이네이터.
  • key — 한 인자를 받는 함수의 디자이네이터 또는 nil.
  • new-list — 결과 리스트.

동작은 이렇습니다. adjoinitemlist의 기존 원소와 같은지 검사해요. 만약 item이 기존 원소가 아니라면, cons 하듯 추가하고 그 결과 리스트를 돌려줘요. 이미 기존 원소와 같다면 아무것도 추가하지 않고 원래 리스트를 그대로 돌려주죠. test, test-not, key는 "같다"를 어떻게 판정할지를 정해요(자세한 규칙은 Section 17.2.1, 두 인자 검사에 관한 내용 참고).

예제를 볼게요.

(setq slist '()) => NIL
(adjoin 'a slist) => (A)
slist => NIL
(setq slist (adjoin '(test-item 1) slist)) => ((TEST-ITEM 1))
(adjoin '(test-item 1) slist) => ((TEST-ITEM 1) (TEST-ITEM 1))
(adjoin '(test-item 1) slist :test 'equal) => ((TEST-ITEM 1))
(adjoin '(new-test-item 1) slist :key #'cadr) => ((TEST-ITEM 1))
(adjoin '(new-test-item 1) slist) => ((NEW-TEST-ITEM 1) (TEST-ITEM 1))

한 가지 짚고 넘어갈 부분이 있어요. 기본 검사는 eql 수준이기 때문에 (test-item 1)을 넣어도 그 리스트 구조가 서로 다른 객체라 중복으로 안 보일 수 있어요. :test 'equal을 주면 내용이 같으면 중복으로 보고 추가하지 않아요. 또 :key #'cadr처럼 키 함수를 주면 각 원소의 두 번째 요소만 뽑아서 비교하므로, (test-item 1)(new-test-item 1) 둘 다 두 번째 요소가 1이라 이미 있는 걸로 판정돼요.

예외 상황으로는, list가 정상 리스트가 아니면 type-error 타입 오류를 신호해야 한다고 명세가 요구해요. 관련 함수로는 pushnew가 있고, adjoin은 다음과 같은 표현과 동치예요.

(adjoin item list :key fn) == (if (member (fn item) list :key fn) list (cons item list))

참고로 :test-not 파라미터는 비권장(deprecated)이에요. 새 코드에서는 :test 위주로 쓰는 게 좋아요.