Pond'ring aloud:
We know that "loop" is recursion, but recursion is often expressed in too-academic a manner.
We know that recursion consists of 2 parts:
- termination case
- recursion case.
I'm thinking about what might be a less-inhumane syntax for expressing a recursive solution. Suitable for non-programmers and LLMs?
[aside: the goal is not "efficiency" at the machine level, but expressiveness and human (non-programmer) understandability]
humane syntax???:
----------------
break down member (x, list) -> ([#found | #not-found], value) {
finish when list is empty { ^ #not-found, ɸ }
finish when x in list { ^ #found, list }
decompose list' <- rest (list) {
^ again (x, list')
}
}
break down append (x, list) -> value {
finish when list is empty { ^ x }
decompose «item» <- first (list), «list'» <- rest (list) {
^ prepend «item» onto again (x, «list'»))
}
}
inhumane syntax:
----------------
(defun my_member (x lis)
(cond ((null lis) (values nil nil))
((eq x (car lis)) (values t lis))
(t (my_member x (cdr lis)))))
(defun my_append (lis x)
(cond ((null lis) x)
(t (cons (car lis) (my_append (cdr lis) x)))))
[aside: "send ..." sends something forward asynchronously instead of returning it synchronously to the caller and unblocking the caller]
less-inhumane syntax involving async ports:
-------------------------------------------
break down member (x, list) output ports: { success: [#found | #not-found], value: object } {
finish when list is empty { send success: #not-found, send value: ɸ }
finish when x in list { send success: #found, send value: list }
decompose list' <- rest (list) {
^again (x, list')
}
}
break down append (x, list) output port: { value: object } {
finish when list is empty { send value: x }
decompose «item» <- first (list), «list'» <- rest (list) {
send value: prepend «item» onto ^again (x, «list'»))
}
}
suggestions / comments?
I don't really have a sense for what about the first syntax make it seem more "humane" to you. But FWIW, here's how Dijkstra (likely also "too-academic") would implement member
and append
, respectively:
i := list.lob;
do
i <= list.hib and list(i) != x -> i := i+1
od
{i now describes where to find x, if valid}
list:hiext(x)
(At least as of 1977, Dijkstra doesn't seem to think about function boundaries at all.)
(Dijkstra recommended designing data structures out of arrays, in the way Lisp designs them out of cons pairs.)
So in your language, would it be illegal to make a recursive call? Requiring the use of this special syntax?
This isn't an attempt to banish recursion, only to capture its essence and give it a different (better?) syntax.
I am tinkering and brainstorming with ideas. I don't necessarily believe in this particular syntax, but, I do know that trying something , then critiquing it tends to move ideas forward...
IMO, the essence of recursion is: divide and conquer.
I am trying to do something that I thought would be simple, but am finding that it requires way more detail than I would expect anyone but a programmer would have the patience for. I have been trying to explain to an LLM (Claude 4.0) what I want, but, it keeps making blunders (due to my fault in not supplying enough niggly details, it gets really, really close, but always leaves out some important detailed edge-case).
Syntax-wise, I know that Loop is too low-level and leaves too many foot-guns lying around. Recursion is better, but too weird-looking. I'm wondering if there is a way to express the essence of divide-and-conquer in a less inhumane way. Maybe this needs multiple syntaxes? Or, maybe it just can't be expressed any better. We do see lots of attempts to define and improve on low-level loop, for, while, etc. syntaxes.
The Dijkstra thing smacks of too much low-levelness, esp. to someone who thinks that an array is just a list with its CDRs optimized away.
Programming is like a Necker cube. Each of us has a slightly different perspective on what is good and what is bad. The interesting question for me lately is: at what point should one give up on finding an axis of good-bad that everyone will get on board with, and instead focus on other aspects that seem more well-posed.
I agree, with the nuance that there are multiple possible syntaxes / perspectives. There's what academic-analysts think is useful, there's what programmers think is useful, there's what non-programmers think is useful, etc. LLMs seem to come close to providing syntax for non-programmers, but, in this case, the LLM couldn't infer sufficient detail from my prompt. In other cases, I asked for smaller snippets and was very satisfied. I wonder what the "grain" size is for getting reliable results?
Absolutely, multiple perspectives. That's what I was getting at with the Necker cube analogy. None of them more right than the others.
But you often strike me as too attached to one perspective and ignoring the others. With any tool you're only going to get out what you put in.
Where I am: Recursion or iteration? Necker cube. Low level or high level? Necker cube. Academics or builders? Necker cube. Natural language or formal language? Necker cube. Correctness or friendliness? Necker cube. Companies or people? People, people, people.
Syntax is "rules". I argue that your "humane" samples have way more syntax than lisp ones.
You throw a lot of english words in there as a separate syntax element each.
This maybe would make my mom
1) think she kinda gets what this is about just because of familiar words
2) when she reads this.
But
1) I doubt she would actually understand what's that all about w/o further explanation
2) And certainly she would not be able to write anything in this syntax after seeing these 2 samples (I know because I can't).
Lisp's ~syntax~ shape is way closer to the actual "abstract shape" of the problem essence: base cases + recur case + branching (picking only 1 of them at time).
Lisp shape has fewer (but not 0) thing to question, e.g. are different parens mean different things? (yes they do, that's why vanilla lisp sucks to read and scared off so many people)
Specifically these lisp samples suck because nobody knows what the ** eq
*t*
*car*
*cdr*
*values*
** are. But those are names, not structure, not syntax. (ok, maybe values
is somehow syntax, I don't know why it is there, what it does, and why would I care to have there anything other than yes
or no
)
Here is the clojure example (notice I redefined only 3 words, and honestly, I think true
false
defn
would be ok w/o substituting them)
;; defn renaming needs to be done differently, via new defmacro, but it can be done globally once, and here it just illustrates the point: "keep the structure, use better names":
(let [NO false
YES true
new-function defn]
(new-function is-x-in-list? [x list]
(if (empty? list)
NO
(if (= x (first list))
YES
(is-x-in-list? x (rest list))))))
It has the same structure as vanilla lisp samples, but much more "humane" (more widely recognizable names (to make 'em even more "humane" – maybe replace with chinese mandarin))
It explicitly has different types of words grouping: () and []
My mom would still have questions, but way fewer than in case of "humane" and vanilla lisp samples.
On the other hand, questions to the "humane" samples I have (to slightly better understand semantics, and maybe to try to write my own fns in this syntax):
- why there are commas there, but not here?
- what
#
^
ɸ
'
|
->
<-
are? - where does function name ends and arg name starts?
- why some args are in () and some are inline?
- I presume [] is a grouping around "regex or", and () - around return type? (try to explain it to my mom)
- why
«»
in the second sample but not in first? - is
break down
a part of fn name or some keywords? why have them? what other options are? are those required? - if
^
isreturn
indicator, why sayfinish
? iffinish
isreturn
- why say
^
?
- why say
^
is in every {}, is it required? are those separate concepts, or is it just grouping syntax{^ ...}
?- would
«
and»
be the same as"
"
or do I need to find them on my keyboard? - is
ɸ
– some constant? if yes, why not#ɸ
(because#found
seems like a constant, and#
seems like a way to mark constants). if not a constant, but some type (likevalue
presumably is, can there be differentɸ
? (assuming that's an empty set. but we receive list (something sequential) and return set? huh.) - is
))
in(x, «list'»))
- a typo? or some syntactic thing?
- there are commas
,
between words in{ ^ #found, list }
, but not right after^
, is comma - separating args, and args are not in parens (seedecompose
and^
), butrest (list)
seems like a function call, with parens... huh. - So if
^
is a fn call, how do I start another fn call after^
(how do I stop the args)? Or is^
– a syntactical keyword? Should it be be the last "line" in {} block? Why lastdecompose
"line" in both samples is without^
? does {} "returns"^
inside of it? - there are commas
,
between words in{ ^ #found, list }
, but not between 2finish
lines. why?
(and few more :) )