0

I really, really like Clojure-style maps:

{:keyword "value"
 :keyword2 'value2}

which can be used like

(get my-map :keyword)

or, even better,

(:keyword my-map)

Can I teach Elisp how to do this?

1

5 Answers 5

1

I don't believe it's possible to teach Elisp to default to this behavior, but you can write a function that does the job for a defined set of keywords:

(defun defgetters (&rest keywords)
  (when-let ((this-keyword (car keywords)))
    (defalias this-keyword
      (lambda (plist) (plist-get plist this-keyword)))
    (apply #'defgetters (cdr keywords))))
2
  • This is a funny way to loop over a list.
    – YoungFrog
    Apr 17, 2017 at 5:53
  • @YoungFrog It's the classic recursive definition of a loop. Apr 18, 2017 at 16:16
1

You can do something like this with a lexical closure also.

#+BEGIN_SRC emacs-lisp :lexical t :results raw
(let ((data '(:keyword "value" :keyword2 'value2)))
  (defalias 'd
    (lambda (key)
      (plist-get data key))))

(d :keyword2)
#+END_SRC

#+RESULTS:
(quote value2)
3
  • This isn't really answering the question; it's just demonstrating how cool lexical closures are. The question is to provide a catch-all means to define all keywords such that (:some-keyword '(:a 2 :some-keyword some-symbol)) yields some-symbol. Apr 18, 2017 at 16:18
  • It might not be exactly what the OP wanted, but the syntax is just backwards from what was wanted, and it is better in some ways. One is you can call it with keywords not contained in the dictionary and get something useful. Second it only adds one function to the global namespace, instead of one for each keyword. Apr 21, 2017 at 22:40
  • But that's not the point of the question… your solution only works with a map that is known at compile-time. Apr 22, 2017 at 18:02
1

I think to get this to work the way you want you have to change the elisp reader.

There is a proof of concept here: https://github.com/mishoo/elisp-reader.el

To use it, we define a new syntax like the one you want from clojure anyway. Then, while reading it define the keywords on the fly.

(require 'elisp-reader)

(def-reader-syntax ?{
  (lambda (in ch)
    (let ((list (er-read-list in ?} t)))
      (cl-loop for (key val) on list by #'cddr
           do
           (message "making %s" key)
           (defalias key
         `(lambda (lst)
            (cdr (assoc ,key lst)))))

      `(list ,@(cl-loop for (key val) on list by #'cddr
            collect `(cons ,key ,val))))))

(setq d { :foo 1 :bar "string"})
(:bar d) ; -> string
(:foo d) ; -> 1

(:foo { :foo 5 }) ;-> 5

This seems to work for me.

1
0

Here is another approach to define functions with a limited scope. It might be a mistake to make global functions for a dictionary because you might get name clashes/shadowing for different dictionaries. This macro creates a scope where the keyword functions exist. The syntax isn't quite what you asked for; it is simpler because you don't need to pass the mapping as an argument here.

(defmacro with-dict (key-vals &rest body)
  (let* ((g (if (symbolp key-vals)
        (symbol-value key-vals)
          key-vals))
     (keys (-slice g 0 nil 2)))
    `(labels ,(loop for key in keys
            collect
            (list key '() `(plist-get ',g  ,key)))
       ,@body)))

(with-dict (:a 1 :b 'some-symbol :c 3)
       (:b))
0

You can override macroexpand to rewrite forms like (:keyword foo) into (plist-get :keyword foo). This code is inspired by the cl-symbol-macrolet implementation. It worked when I tried it, but I would not be surprised to learn that it breaks things.

(defun macroexpand+keyword (macroexpand-orig exp &optional env)
  (let ((macroexpand-all-environment env))
    (while (pcase (funcall macroexpand-orig exp env)
             (`(,(and kw (guard (keywordp kw))) ,plistform)
              (setq exp `(plist-get ,plistform ,kw))))))
  exp)

(advice-add 'macroexpand :around #'macroexpand+keyword)
(advice-add 'macroexpand-1 :around #'macroexpand+keyword)

(require 'cl-lib)
(defun macroexpand-first (eval-args)
  (cl-callf macroexpand (nth 0 eval-args))
  eval-args)

(let ((my-map '(:one 1 :two 2)))
  (:one my-map)) ;=> 1
6
  • This doesn't appear to do anything in your example. Is "guard" an emacs-lisp function? Apr 22, 2017 at 23:38
  • @JohnKitchin It's a pcase pattern: (guard BOOLEXP) matches if BOOLEXP evaluates to non-nil.
    – npostavs
    Apr 23, 2017 at 0:41
  • Thanks. It doesn't work for me still. Does it only work because of the let macroexpansion? Apr 23, 2017 at 1:32
  • Nevermind, it seems to work afterall. It does seem to matter how I evaluate the forms. With special-lispy-eval (e) it doesn't work, and with C-x C-e it does. I guess this implies that macroexpand gets run even for a form like: (:bxf '(:one 1 :bar 2 :bxf "S")) which returns "S"! Apr 23, 2017 at 1:43
  • @JohnKitchin Oh I see, it doesn't work for forms passed straight to eval (which seems to be what lispy-eval does). I've now added advice to eval as well, what could possibly go wrong? :P
    – npostavs
    Apr 23, 2017 at 2:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.