17 points ibobev 1 hour ago 21 comments
perrygeo 1 hour ago | parent
Difficulty is, by definition, relative to one's skill. You cannot so quickly discount the fact that 99% of programming is taught in Java/C style language syntax. If you've had lifelong exposure to Lisp, you might feel exactly the opposite. The author does a poor job of justifying why these pop-cognitive-psych theories should have more weight than prior exposure.
Personally, as someone with decades of exposure to both styles, I look at the factorial example and see everything I love about Lisp syntax - consistent, no magic keywords and syntax to memorize, it represents a tree just like my mental model of code, there's no way to fall through and forget an else, expressions instead of statements, no early returns ... literally everything about the Lisp example is more readable to me. YMMV.
convolvatron 31 minutes ago | parent
Supermancho 18 minutes ago | parent
It's a factor and not the sole factor. Saying it's "by definition" is incorrect.
> The author does a poor job of justifying why these pop-cognitive-psych theories should have more weight than prior exposure.
There's no reason to believe either way, except one path has decades of evidence. Human behavior is not overcome by programmatic "elegance". The dismissive "pop" prefix is signaling bias.
> no magic keywords and syntax to memorize
ie no syntactic sugar. Pointless repetition is counter productive.
>a there's no [logical] way to fall through >b [no way to] forget an else, >c expressions instead of statements >d no early returns
b. The interpreter catches it. d. Pointless execution is counter productive.
> literally everything about the Lisp example is more readable to me
That's a single data point. Statistically it's worse, but you're practiced and apparently still physically able to quickly discern the nested count (or use an IDE). Yet another example of a position that is counter to existing studies. Heavy nesting is error prone, even when a program compiles (eg Monden et al., “Evaluating the Applicability of Reliability Prediction Models between Different Software,” ISSRE 2001)
PaulHoule 1 hour ago | parent
result = sql.execute("""
SELECT someColumn
FROM thatTable
WHERE anotherValue>55
AND state='pending'
ORDER BY createdDate
""")
Ultimately the idea is that indentation should be influenced by semantics, the intention of the code, and not just the syntax. Of course that is against the "one way to indent" philosophy of Go, Biome, and such... But I might accept less than optimal indentation to put an end to tab wars once and for all.I find indentation of Lisp always seems to fail at communicating in the semantics, much worse than other languages. Things like
(if condition truePath falsePath)
are scrambled when your eye skips over something. If the Lisp community got over its respect for tradition perhaps they'd develop some kind of syntax highlighting or tooltips or something that would clarify this sort of structure.About 90% of real language have subject-verb-object or subject-object-verb orders
https://en.wikipedia.org/wiki/Subject%E2%80%93object%E2%80%9...
verb-subject-object and verb-object-subject are more Lisp-like and represent 10% of languages including Standard Arabic, Finnish and Fillipino.
I like extreme parsimony, like I'd love to write stuff like
format = lambda: `%1-%2`
but I think as complexity goes up depending on the meaningful order of elements breaks down in many ways and you need to give things meaningful names.wat10000 21 minutes ago | parent
whartung 27 minutes ago | parent
Consider:
(defun split-line (line)
(remove-if #'(lambda (word) (string= word ""))
(split-sequence:split-sequence #\Space line)))
Now imagine if we had some "lower weight" glyph besides the paren. .defun split-line .line,
.remove-if #'.lambda .word, .string= word "",,
.split-sequence:split-sequence #\Space line,,,
Obviously a contrived example replacing () with ., but you can see how "heavy" the parens and how they can dominate what the eye sees.With experience, the parens vanish. The parens being large and common take control of the conversation more than they should.
waffletower 12 minutes ago | parent
regenschutz 4 minutes ago | parent
lukaszkorecki 26 minutes ago | parent
sinabis 3 minutes ago | parent
ltbarcly3 26 minutes ago | parent
Lisp will say:
(+ (- (\* 3 5) 7) (/ radius pi))
but you are taught since 5: (3*5 - 7 + radius/pi)
2. Humans are highly adapted to using language, and understanding language constructs such as implicit context rules. Humans reduce token counts and structure in favor of implicit rules and making common patterns shorter. Lisp makes them all explicit, which forces you to cope with way more tokens. Lexical binding was added to Common Lisp almost as an afterthought, and the way LET/LET* force you to add layers of nesting demonstrates that. Every time you assign a variable the 'modern' way, you have to indent another block of code.A concrete example is introducing local bindings with actions in between. The thought is "calculate this, do something, then continue":
user = find_user(user_id)
require_admin(user)
report = build_report(user)
write_audit_log(user, report)
receipt = send_report(report)
record_delivery(receipt)
In Common Lisp, a direct translation adds a level of nesting for each binding: (let ((user (find-user user-id)))
(require-admin user)
(let ((report (build-report user)))
(write-audit-log user report)
(let ((receipt (send-report report)))
(record-delivery receipt))))
This is much closer to what is actually happening, and does not require you to understand scoping rules, but it's cumbersome and stupid.LET* handles consecutive bindings, but here the actions must happen between them. You can use PROGN inside the initializers, or introduce dummy bindings for the actions, but either way you're restructuring a flat sequence to fit the binding syntax.
That's the implicit context I mean: the statement order combined with syntax rules of the language can supply the scope, without requiring a new enclosing expression every time you introduce a local. Lexical scope itself doesn't require this nesting to be explicit in the syntax of the language and it's not helpful for it to be.
3. So many inconsistencies.
Common Lisp uses alternating keys and values for property lists:
'(:name "Ada" :age 37)
Association lists use a list of pairs: '((:name . "Ada") (:age . 37))
And LET uses two-element binding lists: (let ((name "Ada")
(age 37))
...)
Those aren't interchangeable conventions. In an association list, (:name . "Ada") pairs the key with the string; (:name "Ada")
pairs it with a one-element list containing the string.Lookup conventions differ as well:
(getf plist key)
(gethash key table)
(assoc key alist)
GETF puts the container first; GETHASH and ASSOC put the key first. ASSOC also returns the matching pair, whereas GETF and GETHASH return the value as their primary result.4. What happens at COMPILE-FILE time is arcane and almost impossible to keep straight.
Here's how much context can hide behind "load this file" in Common Lisp. Loading this source file prints (20 10 30):
(in-package :cl-user)
(defparameter reading 10)
(defun reading () reading)
(let ((captured #.reading))
(defun snapshot () captured))
(defparameter reading 20)
(format t "~S~%"
(let ((reading 30))
(list '#.(reading)
(snapshot)
(reading))))
The first value is 20 because #.(reading) executes while the final form is being READ. The preceding DEFPARAMETER has executed, but the LET binding to 30 hasn't.The second is 10 because the earlier #.reading ran while reading the earlier LET. SNAPSHOT closes over the lexical binding initialized with that value.
The third is 30 because DEFPARAMETER declares reading special, so the final LET establishes a dynamic binding that READING sees when called normally.
And this assumes source loading with read-time evaluation enabled. COMPILE-FILE followed by LOAD isn't equivalent here: compiling the file doesn't automatically execute the definitions that those #. expressions depend on.
It's a deliberately contrived example, but it illustrates what can sit behind "importing a file": reading can execute code, earlier evaluation can affect later reading, lexical and dynamic bindings behave differently, and compilation introduces another execution context. Uniform parentheses don't make those semantics uniform.
5. Common Lisp often feels designed primarily to implement Common Lisp, rather than to write useful application code. Its equality predicates are a good example: EQ, EQL, EQUAL, and EQUALP give you four fixed bundles of rules organized around Lisp's own representations.
Want arrays compared by content? EQUALP does that, but also makes string comparisons case-insensitive. Want objects compared by their slots? EQUALP does that for DEFSTRUCT instances, but not ordinary CLOS instances. Want to define what equality means for your class? None of these predicates is a generic function you can extend.
The complexity goes into distinguishing Lisp's built-in categories, while the application-level question, "when do these two values mean the same thing?", requires a separate operation of your own.
Comparison EQ EQL EQUAL EQUALP
Lists containing (1 2) NIL NIL T T
Strings containing "Ada" NIL NIL T T
String "Ada" versus "ADA" NIL NIL NIL T
General vectors containing 1, 2 NIL NIL NIL T
General vectors containing "Ada" versus "ADA" NIL NIL NIL T
Same-type DEFSTRUCT instances, identical slots NIL NIL NIL T
Same-class CLOS instances, identical slots NIL NIL NIL NILwhalesalad 20 minutes ago | parent
pfdietz 5 minutes ago | parent
This feels like someone who doesn't know French asking "what makes French difficult to read"?
shevy-java 3 minutes ago | parent
People say we can ignore them but I find the difficult to ignore.
But this is not the only problem with lisp syntax. I found that reading Ruby or Python is simply, on average, so much more efficient.
I found scheme somewhat readable - see haxima game world, https://sourceforge.net/projects/nazghul/ it contains scheme files - but I would not want to write any game logic in it.
groundzeros2015 3 minutes ago | parent