Emacs Lisp

2023년 2월 18일 수정

Emacs Lisp은 이름 처럼 Emacs에서 사용할 수 있는 Lisp 언어를 의미한다. 물론 표준 Common Lisp에 Emacs의 다양한 함수가 지원되는 형태라서 Lisp이 아니라고 하기에도 좀 미묘하긴 하다.

개념 메모

샤프(Sharpsign)

# (Sharpsign) 은 s-expressions 특수 문법이다. 근데 뭔 소린지 하나도 모르겠다. 사실 당연하다. # 은 그 자체만으론 의미가 없고 뒤에 뭐가 오는지에 따라 달라지기 때문이다.

에를 들어 #(1 2 3) 은 벡터(Vector), #'function-name 의 경우는 function abbreviation 으로 불린다.

자세한 내용은 🌏Sharpsign 글을 참고하자.

단 따옴표(Single Quote)

단 따옴표 ' (혹은 quote 함수)는 식을 그대로 표현할 때 사용한다. 예를 들어 (1 2 3) 이라는 리스트를 메시지로 출력하고 싶어서 아래 코드를 작성했다고 가정해 보자.

(print (1 2 3))

그런데 위 코드는 1 이라는 함수를 찾을 수 없다는 오류가 난다. 이는 (1 2 3) 리스트가 print 함수 호출 시점에 먼저 실행(evaluation)되어 버리기 때문이다. 참고로 Lisp의 리스트의 첫 아이템은 함수가 위치한다.

하지만 이를 아래처럼 고치면 원하는대로 할 수 있다.

(print '(1 2 3))

단 따옴표를 붙인 '(1 2 3) 리스트는 바로 실행되는 게 아니라 인용 처리가 되어서 실행(evaluation)되지 않은 리스트 형태로 넘어가게 된다.

참고로 인용 처리된 리스트는 eval 함수를 이용하면 원래의 리스트(?)대로 만들 수 있다.

(eval '(1 2 3))

위 코드는 처음과 같이 1 이라는 함수가 없다는 오류가 난다.

샤프 단 따옴표(Sharpsign Single Quote)

#'function-name 의 의미는 (function function-name) 을 줄여서 표기한 것이다. 다르게 말해서 함수를 심볼 형태로 넘기기 위한 것이다.

(defun do-what (f)
  (funcall f "test"))

(do-what #'message)

보통은 이렇게 매크로나 C의 함수 포인터(?)를 넘기는 스타일과 비슷하게 쓸 수 있다. 물론 위의 경우 #' 대신 그냥 ' 만 써도 문제 없이 동작한다. 다만 #' 의 경우 컴파일러에게 이것은 함수다 라는 것을 명시적으로 알려 주는 의미도 있다고 한다.

apply vs funcall

위에서 본 funcall 과 비슷하게 apply 라는 것도 있다. 사용 방법도 거의 동일하다. 하지만 매개변수를 그대로 받냐 아니면 리스트로 받냐의 차이가 있다.

(funcall #'+ 1 2 3)

위의 것을 apply 를 이용해 구현하면 아래와 같다.

(apply #'+ '(1 2 3))

두 코드 다 6을 반환 한다.

이를 이용해 함수에서 매개변수를 넘기는 방법도 조금 다르다.

(defun do-what-apply (what &rest args) (apply what args))
(defun do-what-funcall (what arg) (funcall what arg))

(do-what-apply #'message "test-apply")
(do-what-funcall #'message "test-funcall")

Character

Emacs에서 문자는 ASCII 번호로 표현된다. 즉 숫자다. 만약 특정 ASCII 코드가 기억나지 않는다면 ? 를 붙여서 표현할 수 있다.

(eq ?A 65)

A는 65이기 때문에 위 코드는 참을 반환한다.

setq vs setq-default

Emacs에는 버퍼 로컬 변수라는 개념이 있다. 예를 들자면 tab-width 가 대표적인데, 각 버퍼의 메이저 모드마다 이 변수의 값이 다 다를 수 있다. 버퍼에 속한 로컬 변수이기 때문이다.

setq 명령어는 만약 로컬 변수가 있다면 해당 변수의 값을 지정하고, 로컬 변수가 없다면 전역 변수에 지정하는 방식으로 동작한다.

setq-default 는 버퍼 로컬 변수를 생략하고 무조건 전역 변수의 값을 지정하는 용도로 사용된다.

Comma and Back-Quote

(setq a "a's value" b "b's value" c "c's value")
'(a  b  c)      ; => (a b c)
`(,a b ,c)      ; => ("a's value" b "c's value")

backquote는 quote랑 비슷한데, 인용된 리스트 내부에서 , 가 붙은 요소는 evaluated 시킨 결과로 치환 시킨다는 점이 다른 것 같다. 간혹 quote로 인용 시킬 때 요소들이 이름이 아닌 값 자체로 들어가길 원할 때가 있는데 이럴 때 쓰면 좋을 것 같다.

Property List (plist)

프로퍼티 리스트는 사전(Dictionary) 타입과 비슷한 자료 구조다.

'(name "Conrad"
  age 20
  phone "82-10-1234-5678"
  address "Earth, Milkyway, Laniakea Supercluster")

키와 값이 차례로 들어가는 리스트로, 키는 다양한 방식으로도 표현할 수 있는데 예를 들어 :name 처럼 심볼로 넣을 수도 있다.

값을 읽을 때는 plist-get 을 사용한다.

(plist-get plist 'name)
;; -> "Conrad"

값을 쓸 때는 plist-put 을 사용한다.

(plist-put plist 'gender "etc")

코드 예제

각 함수의 설명은 Emacs Lisp Functions 글을 참고하자.

파일 읽고 편집하고 저장하기

(with-temp-buffer
  (insert-file-contents "/foo/bar/file.md")
  ;; do what...
  (write-file "/foo/bar/file-updated.md"))

타입 변환

숫자(정수 및 실수)와 문자열 간의 전환 두 가지 예제다.

(number-to-string 1)
(string-to-number "1")

굳이 설명할 필요는 없을 것 같다.

커맨드

커맨드를 실행시키기 위해 대충 두 가지 함수가 있다.

(shell-command "ls")

작은 버퍼가 열리면서 결과가 표시된다. 해당 함수의 반환 값은 유틸리티가 반환하는 숫자가 돌아온다. 일반적인 UNIX 유틸리티는 성공 시 0을 반환한다는 점을 참고하자.

반환 값이 아닌 출력물을 가로채려면 아래의 방식이 있다.

(shell-command-to-string "ls")

이렇게 하면 결과가 문자열로 반환된다. 파이프를 흉내 내려면 이 함수가 유용할 듯 하다.

비동기 호출

특정 함수를 비동기로 실행시키기 위해서 단순하게는 run-with-idle-timer 를 사용하는 방법이 있다.

;; call my-function after 1 seconds
(run-with-idle-timer 1.0 nil #'my-function)

특정 시간 이상 유휴 상태여야 비동기로 동작시키는 커맨드다. run-with-timer 와는 동작이 약간 다르지만 유사하다.

비동기여도 과도하게 CPU를 사용하면 Emacs 자체가 멈춘다. 아무래도 싱글 스레드로 동작해서 그런 것 같다는 느낌이다. 이럴 때는 sleep-for 함수나 sit-for 함수를 사용해서 잠깐씩 화면을 갱신할 틈을 줄 수 있다.

(sit-for 1)  ;; wait for 1 seconds and redisplay

sit-forsleep-for 와는 다르게 특정 시간 동안 쉰 뒤 화면을 갱신 시킨다는 차이가 있어서 화면이 얼어 보이는 것이 싫을 때 유용하게 사용할 수 있다.

파일의 내용을 실행시키기

Emacs Lisp 코드를 작성한 파일의 내용을 읽어서 바로 실행시키 위하 아래 함수를 정의해서 사용할 수 있다.

(defun eval-file (file)
  (eval
   (ignore-errors
     (read-from-whole-string
      (with-temp-buffer
        (insert-file-contents file)
        (buffer-string))))))

당연하게도 공용 시스템에선 보안 구멍으로 작용할 수 있으니 가급적 개인 PC에서만 사용하자.

에러 노트

commandp

Wrong type argument: commandp, your-function

commandp 에 대한 문서를 보면 알 수 있지만 이 경우 your-function이라는 함수가 interactive 하지 않아서 발생한 문제일 가능성이 높다. 이 경우 interactive를 넣어주면 해결될 수도 있습니다.

(defun your-function (...)
  (interactive)  ;; <- THIS
  ...)

안 쓰는 매개변수

예를 들어 아래와 같은 코드에서

(defun somefunc (foo bar)
  ...)

bar 라는 매개변수를 사용하지 않으면 아래와 같은 경고를 볼 수 있다.

Unused lexical argument 'bar'

이런 경고 메세지를 귀찮다면 매개변수 이름 앞에 밑줄(underscope 혹은 under-line)을 붙이면 경고가 사라진다.

(defun somefunc (foo _bar)
  ...)

위 처럼 bar 대신 _bar 라고 적으면 안 쓰든 말든 상관하지 않는다.