ht 20161015.1945(in MELPA)
The missing hash table library for Emacs

概要

あなたはEmacs Lispでハッシュテーブルを使っていますか?
Emacs LispはLispだけに、どうしてもリストに目がいってしまうでしょう。
実際にリストを扱う関数は豊富です。

key/valueペアを扱うにはalistやplistを使うのが普通でしょう。
ハッシュテーブルにはそれらにない特性があります。
ハッシュテーブルの持ち味はなんといっても
高速性
です。

リストは頭から順に探索しますが、
ハッシュテーブルはたとえ巨大なデータであっても一瞬でアクセスできます。

ところが、

Emacs Lispのハッシュテーブルは扱いにくいのが正直なところですよね。
make-hash-table で作成して、
puthash でkey/valueペアを設定して、
gethash でkeyに対応するvalueを得ます。

各ペアに対してループする maphash はmapと名乗ってるくせに
nilを返すという奇妙な仕様…。
これではどうしてもリストに流れてしまうのは仕方ないですね。

ht.el はこの状況をなんとかすべく、使いやすい関数群を用意します。
Rubyプログラマにはおなじみの関数名になっています。

インストール

パッケージシステムを初めて使う人は
以下の設定を ~/.emacs.d/init.el の
先頭に加えてください。

(package-initialize)
(setq package-archives
      '(("gnu" . "http://elpa.gnu.org/packages/")
        ("melpa" . "http://melpa.org/packages/")
        ("org" . "http://orgmode.org/elpa/")))

初めてhtを使う方は
以下のコマンドを実行します。

M-x package-install ht

アップグレードする方は、
以下のコマンドでアップグレードしてください。
そのためにはpackage-utilsパッケージが必要です。

M-x package-install package-utils (初めてアップグレードする場合のみ)
M-x package-utils-upgrade-by-name ht

基本

使い方は簡単です。
ht マクロでハッシュテーブルを作成し、
ht-getht-set でアクセスします。

(setq test-table (ht ("fuga" "hoge")
                     ("test" "table")))
;; => #s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8 data
;;                  ("fuga" "hoge" "test" "table"))
(ht-get test-table "fuga")               ; => "hoge"
(ht-set test-table "foo" "bar")
(ht-get test-table "foo")               ; => "bar"

ht-p はハッシュテーブルであるかどうかを検査し、
ht-equal-p は同値性を検査します。

ht-keysht-values はそれぞれkeyのリスト、valueのリストを返します。

ht-contains-p はkeyがハッシュテーブルに含まれているかを検査します。
ht-get との違いはvalueがnilかどうかを区別することです。

ht-size はハッシュテーブルのサイズを返します。

ht->alistht->plist はそれぞれalist/plistへ変換します。
以後のサンプルではスペース省略のため ht->alist を使うことにします。

ht-clear はハッシュテーブルをクリアします。

(require 'ht)
(setq test-table (ht ("foo" 1)))
(ht-p test-table)                       ; => t
(ht-equal-p test-table (ht ("foo" 1)))  ; => t
(ht-update test-table (ht ("bar" 2)))
(ht-keys test-table)                    ; => ("bar" "foo")
(ht-values test-table)                  ; => (2 1)
(ht-contains-p test-table "foo")        ; => t
(ht-contains-p test-table "not-exist")  ; => nil
(ht-size test-table)                    ; => 2
(ht->alist test-table)                  ; => (("bar" . 2) ("foo" . 1))
(ht-items test-table)                   ; => (("bar" 2) ("foo" 1))
(ht->plist test-table)                  ; => ("bar" 2 "foo" 1)

(ht-clear test-table)
(ht->alist test-table)                  ; => nil

ハッシュテーブルをマージする

ht-merge は2つのハッシュテーブルをマージしたハッシュテーブルを返します。

(setq table1 (ht ("foo" 1)))
(setq table2 (ht ("bar" 2)))
(ht->alist (ht-merge table1 table2))    ; => (("bar" . 2) ("foo" . 1))

ハッシュテーブルから値を削除する

ht-remove はハッシュテーブルから値を削除します。

(setq test-table (ht ("foo" "bar")))
(ht-remove test-table "foo")
(ht->alist test-table)                  ; => nil

ハッシュテーブルでループする

ht-eachht-map は各要素でループします。
両者の違いは、ラムダ式のリストを返すかどうかです。
mapcとmapcarの違いと同じです。

また、アナフォリック版の ht-aeachht-amap があります。
それらはkeyとvalueというローカル変数が使えるお手軽マクロです。

;;; ht-each / ht-aeach
(let ((total 0))
    (ht-each
     (lambda (key value) (setq total (+ total value)))
     (ht ("foo" 1) ("bar" 2)))
    total)         ; => 3
(let ((total 0))
    (ht-aeach
     (setq total (+ total value))
     (ht ("foo" 1) ("bar" 2)))
    total)                              ; => 3

;;; ht-map / ht-amap
(ht-map (lambda (key value) (cons key (* value 2)))
        (ht ("foo" 2) ("bar" 10)))      ; => (("bar" . 20) ("foo" . 4))
(ht-amap (cons key (* value 2))
        (ht ("foo" 2) ("bar" 10)))      ; => (("bar" . 20) ("foo" . 4))

条件を満たす要素のみのハッシュテーブルを返す

ht-select は条件を満たす要素のみで構成された新しいハッシュテーブルを返します。
ht-reject はその逆で、条件を満たさないものです。

ht-delete-ifht-reject の破壊的バージョンです。

;;; ht-select / ht-reject
(ht->alist
 (ht-select
  (lambda (key value) (= (% value 2) 0))
  (ht ("foo" 1) ("bar" 2) ("baz" 3) ("qux" 4))))
;; => (("qux" . 4)
;;     ("bar" . 2))
(ht->alist
 (ht-reject
  (lambda (key value) (= (% value 2) 0))
  (ht ("foo" 1) ("bar" 2) ("baz" 3) ("qux" 4))))
;;; => (("baz" . 3)
;;;     ("foo" . 1))

;;; ht-delete-if
(let ((table (ht ("foo" 1) ("bar" 2) ("baz" 3) ("qux" 4))))
  (ht-delete-if (lambda (key value) (= (% value 2) 0)) table) ; => nil
  (ht->alist table))                    ; => (("baz" . 3) ("foo" . 1))

ht-find は条件を満たす最初の要素をリストで返します。

(ht-find (lambda (key value) (= (% value 2) 0))
         (ht ("baz" 3) ("qux" 4)))      ; => ("qux" 4)

結論

このようにht.elはとても便利です。
あなたもht.elでハッシュテーブルを使いこなしてみませんか?


本日もお読みいただき、ありがとうございました。参考になれば嬉しいです。