are there any lispers around who can explain the d...
# linking-together
g
are there any lispers around who can explain the decision / make the argument for function call syntax looking like
(function-name arg1 arg2 arg3)
and not
(function-name (arg1 arg2 arg3))
or in clojure:
(function-name [arg1 arg2 arg3])
o
Less parenthesis perhaps? Plus conceptually, everything is a function? Plus lightest syntax possible?
k
Yeah. It's an interesting question I haven't encountered before. It's not quite the usual discussion about
function-name(arg1 arg2 arg3)
. What would be the advantages of this approach, Garth?
j
@Garth Goldwater Not 100% sure what sort of explanation you are looking for.
(function-name (arg1 arg2 arg3))
would have to have different evaluation semantics. Typically if you see
(something here)
it is a list and if it isn't quoted then it is a going to be interpreted as a function call. So with this change you'd have to say that things are only function calls if their first argument is a list and functions only can take 1 argument (i.e.
(function-name (arg1) (arg2))
would be undefined? What would
(f x)
now mean? Would it just be like a quoted list? Of course, you wouldn't want
(arg1 arg2 arg3)
to be an actual list, because now you have allocate just to call a function so it is just syntatic grouping but at the macro level it is of course still a list. In general, it would make syntax more awkward (just think about math). It would make the evaluation semantics much more awkward.
šŸ‘ 2
g
i’ll be more coherent about why i’m having this thought later as i’m on my phone and can’t just type my way to coherency—i’m thinking about things like symmetry, especially for macros and fexprs, and also what i found confusing when i started with lisp (i’d say ā€œa function takes a list of argumentsā€ but syntactically it doesn’t look like that, so i found it harder to read where calls were... not that beginner’s confusion is a super productive place to go with lisp)
šŸ‘ 2
d
The original 1960 paper on Lisp provides the context. https://web.cs.dal.ca/~vlado/pl/recursive.pdf • The original syntax for function call in Lisp was intended to be f[x,y]. • The paper describes S-expressions, which are the Lisp syntax for literal constants. '42' is an S-expr representing a number. '(1 2 3)' is an S-expr representing a list of numbers. 'f' is a symbol. '(f x)' is a list of two symbols. And so on. • The paper provides source code for a Lisp interpreter. In order for the interpreter to work, programs must be represented as data structures. So the function call 'f[x]' is encoded as the data structure '(f x)'. • One of McCarthy's students implemented the interpreter described in the paper, but didn't bother to implement the complicated Lisp syntax. Instead, he just used S-expression syntax for programs. • Everybody in the lab liked using S-expression syntax, so the original Lisp syntax was never implemented.
g
another way of putting it is purely aesthetic... like a function being invoked is a different ā€œtypeā€ (loosely) of thing than a list of inert data
@Doug Moen i’m familiar with the history side of this—it’s more like why do s-expressions consider the ā€œlistnessā€ of a function call higher priority than its ā€œpairnessā€ if that makes sense
šŸ‘ 2
i’d consider arguments to a function hierarchically subordinate to the invoking of a function—like in the notation we use for mathematical functions—to ahistorically backpropogate a justification for f(x) notation, the parens show that x is in some sense being ā€œput insideā€ the function x
d
I think it is history and path-dependence. A syntax is chosen by a hacker based on pure expediency and ease of implementation, then the decision sticks. It's the same reason why the Lisp community took so many years to accept lexical scoping and lexical function closures, even though closures were invented in 1964. It's because that original interpreter used dynamic scoping, and once they started using the original interpreter, nobody wanted to change the design.
šŸ‘ 1
g
totally! i’m wondering if anyone who works with lisp finds it more ergonomic for non-path-dependence reasons
(i know that i’m weirdly trying to evaluate the idea of adding MORE parens to lisp syntax)
k
Yeah, more parens is drowning out everything else for me at the moment. I'm going to noodle on it and see if I can get past that..
g
i’m particularly agnostic to more parens because i’m working on languages that are at a minimum frame-based, and at a maximum completely gamified—so the typing part gets mostly replaced by doing actions rather than typing syntax
šŸ’” 1
šŸ‘ 1
@opeispo if you take the less parens logic to its conclusion you get something spacing-based like haskell
o
Yeah and tbh, I struggle with no parens with Haskell sometimes. I think it’s the tension between explicit but tedious to write vs. implicit be could but be ambiguous
k
Yeah, systems with fewer spaces have been discussed many times. Which is why this thread is interesting šŸ˜„ @Garth Goldwater your reference to frame-based systems is helpful. Extra parens make a lot more sense if a "function" can take more than args. It might even make sense to indent it like this:
Copy code
(function-name
    (arg1 arg2 arg3)
    (block1
     block2
     block3)
    metadata1
    ...)
šŸ’Æ 1
g
this may be running way to far down into specifics but stuff like injecting runtime args for eg a thread macro—i know with lisp it would really just be eg
(cons (append (cdr (call-site)) new-arg))
, but if you look at it from a destructuring perspective or even more like a prolog-esque unification, something like (completely made up syntax)
return (function-name (...args additional-argument))
makes more sense to me than
return (function-name ...args additional-arg)
... i think. i may have just persuaded myself out of this discussion lol
ok, here’s my take: original, mutable lisp, sees code firstly as a list of instructions to be executed, not a data structure reflecting the semantics of code evaluation, if that makes sense. i now realize that part of my interest in more parentheses comes from the idea of parens indicating order of execution, so it makes sense to parenthesize function arguments, as a mnemonic reminding the user that they’ll be evaluated before being passed to the function. i have no opinion anymore lol
šŸ’” 2
k
So macros wouldn't have parens! I think you're on to something here.
🤯 1
šŸ’” 1
šŸ‘Œ 1
g
exactly!!
ironically i’m implementing the first pass of this in json because i’m uncultured swine and i live in a ditch with all the web app apis. but this has been super helpful!
ā¤ļø 1
šŸ‘ 1
s
Well, it seems you want
(apply fun (list arg1 arg2 arg3))
to be the default, with syntactic sugar for it. It feels odd, but you are correct in pointing out that while function calls are destructured as pairs, syntactically there is no indication that this is the case. But honestly I can't think of a way to make this work short of the usual
fun(arg1 arg2)
syntax. This is a bit unrelated, but it came to mind when thinking of uniform syntax—in fennel-lang, unquoting something that isn't quasiquoted moves its evaluation to compile time, which kind of gives you something like "anonymous macros" for free, which I've always found really neat.
šŸ˜Ž 1
šŸ’” 1
g
@S.M Mukarram Nainar i think you’ve nailed it, except i think i’m reaching for something maybe-impossible, which is
(apply (fun arglist))
, so that apply is the same kind of form as a function is—a tree-shape that dictates something about its children. i REALLY like the sound of the fennel-lang thing—i hadn’t heard of it. that’s exactly the kind of symmetry i go bananas for, particularly because it sounds like it deals with a model of managing the order of interpretation/execution. thanks for pointing me to it! (for any lurkers the link is https://fennel-lang.org/ )
in the example above,
arglist
might have to be something analogous to boxed. idk. trying to figure out if i can make it all consistent
s
I think there is something to be explored here, for sure, personally I wasn't really too comfortable with lisp style function calls until I did the interpreter chapter in sicp and implemented apply. I think for beginners it's hard to tell the difference between eval and apply—it took me a long time to understand it. I have it in my mind as one of those cs stopping points like recursion. Unfortunately I don't have any concrete ideas on the topic other than suggesting that everyone should study interpreters at some point.
āž• 1
g
yeah. this is definitely in the territory of trying to do something similar
d
I've been on this road before! Here's my story: Making a Lisp/Scheme-like language. There is no "compile time", just immediately invoked code -- BUT, that code can just happen to setup stuff to be invoked later. So compile time magic is replaced with regular old code of any sort, that just runs once at "startup". Any function can be a "macro", in this sense. (I think this is an unacknowledged genius of JavaScript, which is why people can do so many very interesting and powerful things with the language on levels that you don't really see elsewhere). Want to "compile away" that startup pass? Have that code do the "setup" and then just spit out the result, and send that off as the "built" code -- which is possible because all the "code" is just lists and stuff; just print it all out or copy it out of the inspector/repl/etc. Ok, so who needs macro expansion, right? But beyond that, if a "macro" is just a function that happens to take unevaluated code as arguments, then there needs to be a good way to control when arguments are or are not evaluated before passing them in. And hey, if these "macros" can be called any time (at "setup" or on the fly at "normal runtime" -- something macro expansion cannot do), then what if I want to use an expression whose result is the code to pass to the "macro"? I'll detail a could options I've gone through in a separate reply, since this is getting long
One option is to always evaluate everything, BUT have a concept of a "code block" that is not evaluated. This is the same idea as in SmallTalk or Rebol / Red, or lambda functions in languages like C# or JavaScript. In my lang (which I'm piloting in JavaScript, so think JSON), "functions" are represented as objects with "code", "args", and "parent" (lexical scope) fields. The "apply" function (i.e. the part of the interpreter that handles function calls) creates a new "lexical scope" object with its "parent" scope copied from the "parent" of the function. BUT, given a parent-less function-object, it uses the scope of the caller (dynamic scope) instead (Ta da! Closures! C# and JavaScript lambdas effectively do the same thing) ... So if I want to pass a code-block, I use { code: ..expr.. }. Otherwise I just use a regular old expression (which might happen to return a function). (Note: I'm making an editor where you build the code structure up directly, rather than type it out; so I care more about simple underlying structure than textual syntax, because I can dress it up. Not unlike how '(a b c) is shorthand for (list a b c) in some Lisps).
ā¤ļø 1
In a scenario where JSON is passed ad-hoc as code to be evaluated and I needed to make the syntax as non-programmer friendly as possible, It was better to let the function itself determine when to evaluate which arguments (i.e. by manually calling eval as needed) So an "If" (written as a lambda) would be: (cond, T, F) => eval(cond) ? eval(T) : eval(F) So someone might expect all of this to be evaluated: ["Foo", ["<", a, b], [c, d], [e, f]] But (because it's documented) only expect either c-d or e-f to be evaluated for this: ["If", ["<", a, b], [c, d], [e, f]] Or might expect this a-b-c to be used as-is as _a list_: ["ExpectsListAndNum", [a, b, c], 10]
j
@Garth Goldwater
(apply (fun arglist))
is
(apply (partial fun arglist))
in the usual semantics. Some of what you're thinking about might be illuminated by a quick look into dialects that assume function composition, like Shen.
A note on the history mentioned by @Doug Moen: the person referred to in the oft-repeated litany as "one of McCarthy's grad students" is Slug Russell. In addition to coding the first Lisp interpreter, he invented continuations, designed and coded Spacewar! (the first video game), and taught Gates and Allen programming on a PDP-10 as part of a school program in Seattle.
🤯 2
s
@Dan Cook At the risk of being a pedant, I wanted to point out that
'(a b c)
is not equivalent to
(list a b c)
in exactly the topic of this discussion—`quote` does not evaluate its arguments, and
list
does. So
'(a (+ 2 3))
is self evaluating, but
(list a (+ 2 3))
evaluates to
'(a 5)
. It's for that reason that
quote
requires a special case in an interpreter, but
list
does not.
šŸ‘Œ 2
w
@Dan Cook Let's add that how neat JITting can be with modern JavaScript heaven help us. Go ahead and compile away at runtime. (I confess I haven't followed any recent developments along those lines, so I don't know whether it's getting better or worse or just weirder.)
d
i’m working on languages that are at a minimum frame-based
Thanks for showing me the term "frame-based". I'm working on something similar, having problems finding prior art, and googling "frame-based" gave me this: https://www.greenfoot.org/frames/
Consider the hypothetical language T-Lisp, based on T-expressions: (f x)
A call. f is an abstraction (function or macro). Abstractions always have a single argument, like in ML/Haskell. In a call, 'f' is an evaluated subexpression, like in Scheme.
(f x y z)
A call chain. (f x y z) is sugar for (((f x) y) z). This gives us a natural syntax for invoking a curried function or macro.
[a b c]
A list. The subexpressions a, b, c are evaluated, similar to (list a b c) in Lisp.
List exprs and call exprs are orthogonal concepts. A list expr expresses the fundamental concept of an ordered sequence of expressions. It is a mistake to intertwine or complect list exprs with call exprs, because that creates complexity. A function has a single argument, but you can simulate multiple arguments using Currying, or by passing a list as an argument. There is no need for the 'apply' function. This is important for composability. Let's say that the + function takes a list of zero or more numbers, as it does in Scheme. Then you write (+ [a b c]). Suppose you want to abstract out the list [a b c], replace it with the variable x. No need for mental gymnastics, you just write (+ x). Macros can be curried. The 'let' macro has two curried arguments. For example,
Copy code
(let [[x [a b c]]] (+ x))
In T-Lisp, every value is printed as a constructor expression. When you evaluate this printed representation, you reconstruct the original value. List values are printed as list expressions, eg
[1 2 3]
. This is intuitive, easy to understand, useful. One of my criticisms of Lisp is that the printed representation of a list is a function call, and this is confusing to non-experts. For example, in Lisp, the printed representation of
(list 'a 2)
is
(a 2)
. You can manually convert this to an expression that reconstructs the original list by writing
'(a 2)
, but now you have lost composability (the ability to substitute like for like). You can't trivially abstract out one of the subexpressions and replace it with a variable containing the same value. Here is empirical research supporting my complaint about list syntax in Lisp: https://www.cs.kent.ac.uk/people/staff/dat/miranda/wadler87.pdf
The difference between (1 2 3) and (quote (1 2 3)) is subtle, and it inevitably confuses students. In particular, it plays havoc with the substitution model of evaluation.
ā¤ļø 2
šŸ’” 1
g
holy crap this discussion has been insanely valuable. @Dan Cook it sounds like we’re working on super similar stuff @Jack Rusher i have absolutely checked out shen. wish there was more video content on it. aditya siram has demonstrated a lot of really cool stuff but i could really use a tutorial lol. @S.M Mukarram Nainar this is exactly the kind of pedantry that’s really important to get right in this discussion! @Doug Moen i’m going to have to pin this message on my wall. i think there are a lot of great specifics about the lisp you outlined that line up with what i’m thinking about and i will reread this when it’s not midnight my time
šŸ» 3
ā¤ļø 1
d
@wtaysom - Or "compile away" ahead of time, but by having your code operate on itself rather than being bound by some set-in-stone compiler/language. Instead, YOU choose how to programmatically express the program you want to build. Computationally / expressiveness-wise, you get the same benefit (minus the optimization) by just doing it "at runtime" (like JavaScript), but it can be done Ahead Of (rather than Just In) Time. But especially for Lisp-like languages where the compile-time stuff is really no different than the runtime stuff (i.e. macro expansion), that "compile pass" can be made of your own code that does whatever you want, rather than having to be granted (and locked down by) something separate called a compiler. ... WAIT ... Have I just been describing Racket this whole time?? 🤯
šŸ˜„ 3
šŸ‘ 3
g
this is sage advice for everyone’s marketing pages!
šŸ‘ 1
d
My response to "Why Racket/Lisp" is that these claims are true for other powerful, expressive language families as well. You need to seriously look at the Haskell and APL language families to understand the full picture (APL includes J and K). • "Lisp is concise" (compared to Java). Okay, I find Lisp verbose and klunky. Haskell programs are half the size of Lisp programs, and APL is far more concise than Haskell. • "Everything is an expression" (compared to Python). True, but Lisp is still an imperative language where most expressions are statements executed for their side effects. This claim is more true of Haskell and APL, where more of the work is done using true expressions (that have no side effects and return values). APL invented "map/reduce" style programming, where pure functions applied to bulk data replace imperative loops, and the APL family still supports this style much better than any other language. • Composability: "Since expressions are nestable, anything in the language can be combined with nearly anything else." Definitely a core strength of Lisp, but Haskell beats Lisp in this category by a large margin. If you only know Lisp, you can't even see the composability gap, you need to learn Haskell to understand what is missing. The APL family lags behind Scheme/Racket and Haskell due to the distinction it makes between functions and values. Common Lisp also lags behind Scheme and its descendents due to the distinction between functions and values. • "Every expression is either a single value or a list. Single values are things like numbers and strings and hash tables." This is meant to point out that lists are a universal data structure, and there are a ton of list operations, which you leverage if you use lists to structure data. Good point. But, this also points out one of the composability failures of the Lisp family: character strings are atoms. In the Haskell and APL language families, a string is a list of characters, so all of those available list operations also apply to strings. By contrast, Scheme has length and string-length; reverse and string-reverse; and so on and so on. In APL, everything is an array, and APL takes this idea of a universal data structure much farther than Lisp does. • "Functional programming". Haskell owns this space. • "Macros". The Lisp family owns this space. It's worth noting that due to its greater expressive power, Haskell programmers use functions in situations where Lisp programmers are forced to use macros instead. Haskell has lazy evaluation built in. In Lisp, control structures such as 'if' and 'and', which evaluate their arguments lazily, must be macros, not functions. Macros are not first class values, so there is a loss of composability relative to Haskell. • "Create new programming languages". Racket owns this space. • Externalities: libraries, documentation, tooling, community: If the language is good enough, these factors are more important than language design. I think Lisp family languages beat the traditional functional and array languages in this category.
ā˜ļø 1
šŸ‘ 2
g
i agree with pretty much all of your points, @Doug Moen . i just meant that i see a lot of projects with description pages that read like the lisp flattery described above