How would someone more seasoned with elisp/functional programming
write this function?
reddit / emacs
----------------------------------------------------------------------
SegFaultHell · 2022-03-22 16:40
First, the problem statement. I wanted to traverse an output of the
`libxml-parse-html-region` function, whose output takes the following
form (where `tag` is a symbol, `attribute-list` is nil or an alist,
and any remaining elements in the list are either strings or nodes
following the same format):
(tag attribute-list &rest child-nodes)
I would then want to check if there are values in the attribute-list,
and if there are to see if the (key . value) pair I'm looking for
exists. If not, I would like to recursively pass through each child-
node until I either find my (key . value) pair, or find nothing
matching and return nil.
For an example of the type of structure, this:
Genesis will remove all currently active curses, meaning Curse of the
Blind will not affect the item pedestals present inside the room.
would become this:
(li nil
"Genesis will remove all currently active "
(a ((href . "/wiki/Curses") (title . "Curses"))
"curses")
", meaning Curse of the Blind will not affect the item
pedestals present inside the room.")
and a sample search would be to find the `` node with the
attribute `'title` that has value `"Curses"`.
Knowing this, I wrote the following function that takes the node to
start with, the attribute name and value to find, and traverse the
tree of child-nodes until I find and set a return value.
(defun my/find-html-node-by-attr (node attr-name attr-value)
(when (listp node)
(let ((child-nodes (nthcdr 2 node))
(result))
(unless result
(if (and (consp (nth 1 node))
(equal (cdr (assoc attr-name (nth 1 node))) attr-
value))
(setq result node)
(progn
(dolist (child-node child-nodes result)
(unless result
(setq result (my/find-html-node-by-attr child-node
attr-name attr-value)))))))
result)))
Sample call: `(my/find-html-node-by-attr parsed-html 'title "Curses")`
This is obviously heavily inspired by a background in non-functional
programming languages, and you can probably see exactly where I would
have put "return" statements. The `setq`'s with the result value come
from looping over the child-nodes with a dolist, and I know there has
to be a better way but I can't quite get it.
Just getting a function that worked took several hours, and led to
several "breakthroughs" about what people talk about when they say
lisps are great because code is data, but I just can't quite wrap my
head around how this *should* be written. Having a more functional
version of the same thing would be very helpful to help start bridging
that gap, and I'm not really sure where else to post this.
----------------------------------------------------------------------
fragglestickcar0 · 2022-03-22 17:25
(require 'cl-lib)
(defun my/find-html-node-by-attr (node attr-name attr-value)
(cl-destructuring-bind (tag attrs &rest childs)
node
(if (equal attr-value (alist-get attr-name attrs))
node
(catch 'done
(dolist (child (cl-remove-if-not #'consp childs))
(when-let ((found (my/find-html-node-by-attr
child attr-name attr-value)))
(throw 'done found)))))))
(with-temp-buffer
(save-excursion
(insert "Genesis will remove all currently active curses, meaning Curse of
the Blind will not affect the item pedestals present inside the
room."))
(my/find-html-node-by-attr
(libxml-parse-html-region (point) (point-max))
'title "Curses"))
Incidentally, answerers will never care what you tried. Bring on the
downvotes.
----------------------------------------------------------------------
w0ntfix · 2022-03-22 18:12
Using the helpers from [dash.el](https://github.com/magnars/dash.el):
(defun find-match (attr-val node)
(seq-let (_ attrs &rest children) node
(if (-any-p (-partial 'equal attr-val) attrs)
;; we're looking at it!
node
;; search the list children, return the first non-nil value:
(->> children
(-filter 'listp)
(-some (-partial 'find-match attr-val))))))
(defun parse-xml (xml-string)
(with-temp-buffer
(insert xml-string)
(libxml-parse-xml-region (point-min) (point-max))))
(->> "Genesis will remove all currently active curses, meaning Curse of
the Blind will not affect the item pedestals present inside the
room."
(parse-xml)
(find-match '(title . "Curses")))
;; => (a ((href . "/wiki/Curses") (title . "Curses")) "curses")
Thinking functionally takes time, good luck and feel free to ask any
questions, thanks for the exercise
----------------------------------------------------------------------
markasoftware · 2022-03-22 18:17
(defun my/find-html-node-by-attr (node attr-name attr-value)
(when (listp node)
(destructuring-bind (tag attrs . children) node
(if (equal (cdr (assoc attr-name attrs)) attr-value)
node
(find-if #'my/find-html-node-by-attr children)))))
Specific notes:
+ Why the first `unless result`? Result will always be nil there.
+ Why do you write `(setq result node)` instead of just `node`? If you
just write `node`, then the return value of the if statement becomes
`node` (unlike in most programming languages, the `if` in Lisp returns
the value of whichever statement is executed. Similarly for `unless`).
+ Why do you use `progn` where you do? Progn is only useful when you
need to put multiple statements where only one is usually allowed.
----------------------------------------------------------------------
flyin1501 · 2022-03-22 19:38
- This:
(catch 'tag
;; to return a value early
(throw 'tag value)
;; ...
or
(cl-block nil
;; ...
(cl-return value)
;; ...
are more idiomatic for writing code as if you have a return
statement, as they actually jump out of the block.
If you use cl-defun to define a function, the body is automatically
placed in a block named after your function, so you can just use `(cl-
return-from name-of-your-function value)`. (You can't just say `(cl-
return value)` without a `(cl-block nil ...` though.)
- All arguments to `if` other than the first two form the else block.
This is sufficient:
(if (my-predicate-p)
(do-stuff)
(message "This is the else block")
(+ 1 2))
This is called an "implicit progn".
- The result parameter to `dolist`:
;; Reversing a list should be done with `nreverse' as it is way
faster
;; (it's a builtin function that has a dedicated opcode).
;; This is just for demonstration.
(let (ret)
(dolist (var (list 1 2 3) ret)
(push var ret)))
is simply short hand for
(let (ret)
(dolist (var (list 1 2 3))
(push var ret))
ret)
. As you are discarding the value of that dolist statement, adding
it there is just confusing.
- Here's what I might do to the function:
I'll first remove the useless progn and unless blocks:
(defun my/find-html-node-by-attr/remove-useless (node attr-name
attr-value)
(when (listp node)
(let ((child-nodes (nthcdr 2 node))
(result))
;; `result' is always nil here as we've just initialized
it to
;; nil above. Remove the useless `unless' check.
(if (and (consp (nth 1 node))
(equal (cdr (assoc attr-name (nth 1 node))) attr-
value))
(setq result node)
;; Remove extra progn
(dolist (child-node child-nodes result)
;; If we reach the else block, the setq above has not
been
;; executed. Remove the useless `unless'.
(setq result (my/find-html-node-by-attr/remove-useless
child-node attr-name attr-value))))
result)))
The (setq result ...) is also mostly not necessary:
(defun my/find-html-node-by-attr/last-value-return (node attr-
name attr-value)
(when (listp node)
(let ((child-nodes (nthcdr 2 node)))
(if (and (consp (nth 1 node))
(equal (cdr (assoc attr-name (nth 1 node))) attr-
value))
;; This is the last value if we go into this branch.
So
;; this is the return value.
node
;; dolist discards the return value, so we have to
capture it.
(let ((result))
(dolist (child-node child-nodes result)
(setq result (my/find-html-node-by-attr/last-value-
return
child-node attr-name attr-
value))))))))
The dolist will only keep the value from the last child node. Let's
fix that:
(defun my/find-html-node-by-attr/style-and-bug-fix (node attr-
name attr-value)
(when (listp node)
;; `cadr' = second element. (nth 1 node) is arguably more
;; readable; I just find `cadr' to be more succint..
(if (and (consp (cadr node))
(equal (cdr (assoc attr-name (cadr node)))
attr-value))
node
;; Because `child-nodes' is only used in the else block,
moving
;; the let binding here reduces the amount you have to
scroll to
;; find the definition.
(let ((child-nodes (nthcdr 2 node))
;; This is another way to initialize a new local
variable to nil.
result)
(dolist (child-node child-nodes)
;; This diverges from your original function, but I
assume
;; you want *every* child, not just the last one.
(push (my/find-html-node-by-attr/style-and-bug-fix
child-node attr-name attr-value)
result))
;; `push' inserts in front, so (car result) is now the
last
;; child node. Reverse it so first result is first child
node.
;;
;; It is slower to insert from the end in the loop than
to
;; insert in front than use `nreverse' to reverse it.
This is
;; because `nreverse' is a builtin function, written in
C,
;; which also has a dedicated bytecode.
(nreverse result)))))
Demo on using cl-block for early return. I'm not sure whether this
is better than the previous one.
(defun my/find-html-node-by-attr/early-return (node attr-name
attr-value)
(when (listp node)
(cl-block nil
(when (and (consp (cadr node))
(equal (cdr (assoc attr-name (cadr node)))
attr-value))
(cl-return node))
(let ((child-nodes (nthcdr 2 node))
result)
(dolist (child-node child-nodes)
(push (my/find-html-node-by-attr/early-return
child-node attr-name attr-value)
result))
(nreverse result)))))
----------------------------------------------------------------------
[deleted] · 2022-03-22 21:39
[deleted]
----------------------------------------------------------------------
MechoLupan · 2022-03-22 21:48
Use `seq-contains-p` to check whether this node has that
attribute/value pair. If it doesn't, (ab?)use `seq-reduce` to return
the child node that does:
(defun my/find-html-node-by-attr (node attr-name attr-value)
(when (listp node)
(or (when (seq-contains-p (cadr node) (cons attr-name attr-
value))
node)
(seq-reduce (lambda (found child)
(or found
(my/find-html-node-by-attr child attr-
name attr-value)))
(cddr node)
nil))))
(You may want to change the function signature to pass the attribute
as `(name . value)`. it saves you code :)
----------------------------------------------------------------------
MechoLupan · 2022-03-22 23:50
I don't have dash.el installed, so I can't test it, but I think your
function will return the node passed as parameter or a first level
child only, since AIUI, `-first` doesn't return what the predicate
returns. Can you test the case when it should return a more deeply
nested node?
----------------------------------------------------------------------
w0ntfix · 2022-03-23 00:09
good catch, that should be a `-some` -- I initially had it as a -keep
->> first, and prematurely smooshed. updated op
----------------------------------------------------------------------
github-alphapapa · 2022-03-23 01:03
I would start by seeing if one of the existing XML-querying libraries
would do the job. https://alphapapa.github.io/emacs-package-dev-
handbook/#outline-container-XML%20%2F%20HTML
----------------------------------------------------------------------
SegFaultHell · 2022-03-23 03:33
Thank you for answering, that `destructuring-bind` function is
especially nice, I felt like there had to be a way to do something
similar but couldn’t find it. I’d be happy to answer your questions
too.
> Why the first unless result ? Result will always be nil
there.
I put it in thinking it would prevent the function from running when
called within itself if the result had been set already. Now that it’s
been pointed out though it’s immediately clear it wouldn’t work that
way.
> Why do you write (setq result node)
This goes in hand with with the above comment, if result was set I
wanted to return that instead of continuing to traverse the tree.
That’s obviously not how that works though.
> Why do you use progn where you do?
I didn’t realize at the time that `if` would would evaluate everything
remaining as the else branch, so the `progn` was to put the else case
in a single statement.