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

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))))
  • This is a funny way to loop over a list. – YoungFrog Apr 17 '17 at 5:53
  • @YoungFrog It's the classic recursive definition of a loop. – Sean Allred Apr 18 '17 at 16:16
0

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)
  • 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. – Sean Allred Apr 18 '17 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. – John Kitchin Apr 21 '17 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. – Sean Allred Apr 22 '17 at 18:02
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

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.

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
  • This doesn't appear to do anything in your example. Is "guard" an emacs-lisp function? – John Kitchin Apr 22 '17 at 23:38
  • @JohnKitchin It's a pcase pattern: (guard BOOLEXP) matches if BOOLEXP evaluates to non-nil. – npostavs Apr 23 '17 at 0:41
  • Thanks. It doesn't work for me still. Does it only work because of the let macroexpansion? – John Kitchin Apr 23 '17 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"! – John Kitchin Apr 23 '17 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 '17 at 2:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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