What is a programmable programming language? (2019)
hiphish.github.io
hiphish.github.io
There is a level of metaprogramming even more fundamental than Lisp macros - the ancient and maligned fexpr [0]. They are almost as if you took lambda and just didn't evaluate the args when called by default. In fact, this is what Kernel [1] elegantly showed, bringing along with it the pure notion of a vau calculus, which can help restore optimization techniques who's absence resulted in the original defamation of the fexpr.
What’s that? I googled and am still confuzled.
This has given the impression of slowness and difficulty to grok with fexprs, as dynamic eval foregoes certain optimisations, and scoping the fexpr bindings is hard (variable capture is a problem).
https://web.wpi.edu/Pubs/ETD/Available/etd-090110-124904/unr... is a good paper on this stuff (see p41/42 for a discussion).
Another version of fexprs/vau is open composition https://www.piumarta.com/freeco11/freeco11-piumarta-oecm.pdf
I guess the main issue is handling dynamic binding cleanly. Koka https://koka-lang.github.io/koka/doc/book.html#why-handlers handles this well with static (HM) type inferrence.
Shen and Maude (https://shenlanguage.org/TBOS/tbos_255.html, https://maude.lcc.uma.es/maude321-manual-html/maude-manual.h...) are the only languages that I know that can do dynamic type checking, and thus a full handling of dynamic binding, the Shen calculus syntax is a bit clunky though.
Unless one uses an interpreter.
CL-USER 1 > (defmacro my-if (test a b)
(print "Hi, I'm the executing MY-IF macro")
`(if ,test ,a , b))
MY-IF
CL-USER 2 > (defun foo (a)
(my-if (> a 10) 'big 'small))
FOO
CL-USER 3 > (foo 11)
"Hi, I'm the executing MY-IF macro"
"Hi, I'm the executing MY-IF macro"
BIG
CL-USER 4 > (foo 2)
"Hi, I'm the executing MY-IF macro"
"Hi, I'm the executing MY-IF macro"
SMALL
In this Lisp implementation, the macro even runs twice on each runtime invocations. $ /usr/local/bin/sbcl
This is SBCL 2.2.1, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.
SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.
* *evaluator-mode*
:COMPILE
* (setf *evaluator-mode* :interpret)
:INTERPRET
* (defmacro my-if (test a b)
(print "Hi, I'm the executing MY-IF macro")
`(if ,test ,a , b))
MY-IF
* (defun foo (a)
(my-if (> a 10) 'big 'small))
FOO
* (foo 11)
(foo 11)
"Hi, I'm the executing MY-IF macro"
BIGAnd yet, Javascript, possibly the most widespread language on the planet, does exactly this and has been optimized for speed to hell and back several times.
The problem wasn't unsolvable. The problem was that there wasn't enough money behind solving it.
And, yet, somehow a lot of very clever people made Javascript one of the fastest languages around.
OTOH, typically Lisp code uses zillions of macros, incl. most of control flow constructs.
https://stackoverflow.com/questions/13393673/how-do-you-stor...
Of course this is all highly subjective. Perhaps fexpr was just a weird language construct that introduced more problems than it solved. I think it is an unexplored avenue that was brought to light again by Kernel. You really need fexprs to get a true programmable programming language in the sense their are no exceptions. Maybe the cost can become reasonable.
You mean constituents? Or operands?
"Paralinguistic"
I think you mean syncategoremata?
"Iconic forms"
?
"vau calculus"
Interesting. For the interested, see: https://web.wpi.edu/Pubs/ETD/Available/etd-090110-124904/unr...
> I think you mean syncategoremata?
I think this is the most HN thing I've ever read on this site. :D
https://en.wikipedia.org/wiki/The_Art_of_the_Metaobject_Prot...
Please explain why you think that might be "a good thing". (Assuming you do.)
Please explain what you think are the differences between "source code" and "an in-transit format" that would allow us to distinguish one from the other.
The difference is that the grammar of the transit file is not the language's grammar.
Good? Bad? Unimportant?
> … method is the basic unit…
Are there method definitions outside the context of a class?
> … not the language's grammar.
GNU Smalltalk?
It was not me who wrote that so I do not know his meaing.
> Are there method definitions outside the context of a class?
As I said above, I was talking about the basic compilation unit and it is simplified perspective.
> GNU Smalltalk?
Well, that's an exception :-) But I must note that not a nice one. About 15 years ago, I read an article about a novice evaluating Smalltalk based on experiences with GNU Smalltalk only, and he was complaining about how strange and crazy the grammar is with all that exclamation marks.
So how can you say some other aspect is more important :-)
(Please provide some explanation of what you think is important about " method is the basic unit".)
> … the basic compilation unit …
Object subclass: #MoreBasicCompilationUnit
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: ''! !My opinions only :-)
> (Please provide some explanation of what you think is important about " method is the basic unit".)
I think it has many interesting consequences. The methods have only a few explicit dependencies required for compilation; they are not dependent on each other, they may be loaded and presented in any order, and the compilation is very fast. As I said, it may be comparable to having every small method in a single file. With such a setup, the code editor itself starts to lose its importance - everything on the level of notepad basically does its job. You need to focus more on the processing of the code as code objects graph. Not as a text file to edit, which may be an important shift in thinking.
Incremental compilation is not related to that directly. And yes, you still need a way how to define classes which Smalltalk file-out format does do as simple do-its, which is not a fortunate solution for statical analysis of the given file.
Unless they are — methods that change how methods are compiled.
> … loaded and presented in any order…
And which may give different results depending on order.
Isn't that what "First-Class Undefined Classes for Pharo" was about?
https://hal.archives-ouvertes.fr/hal-01585305/document
> … how to define classes which Smalltalk file-out format…
That problem isn't created by the file-out format. That problem is everything is a message send.
"Class definitions are [not] static, declarative syntactic structures."
1988 "An Overview of Modular Smalltalk"
https://dl.acm.org/doi/pdf/10.1145/62084.62095
Seems like there have been plenty of problems caused by "method is the basic unit" ;-)
July 10, 1989 "ParcPlace Systems Inc, of Mountain View California, has launched its Objectworks software development system for AT&T’s C++ Release 2.0: the integrated set of object-oriented tools include an incremental compiler and linker, source level debugger and source code browsing, and will be available in late August for Sun Microsystems’s Sun-3 workstation, selling for $2,500."
https://techmonitor.ai/technology/parcplace_launches_objectw...
Are you explaining what a Smalltalk browser is like?
https://files.mtstatic.com/site_7337/32007/0?Expires=1654646...
The ‘normal’ way to redefine words in forth is a bit of cheating, though. Redefining a word merely introduces a word with a name that shadows that of the old, still existing word.
Words created before the definition still call the old implementation, and, depending on the implementation (a phrase that, I think applies to about any statement about forth) you can still find it when you walk the dictionary and call it.
Smalltalk is missing too, and that’s a bigger omission. When you redefine a function there, it starts getting used everywhere.
I wonder what a blog post written about this 50 years from now will look like. Would it still mention C and lisp? Would readers of said article still mention forth and smalltalk as missing? Also, what reasonably successful languages from the ‘60s/‘70s already are gone from the collective mind?
Being used today to write very popular OSes, I see C still being mentioned, but by then lisp might have fallen into the abyss of history (except for, probably, a few computer historians)
I don't think so, because being a lisp is a trait of a language rather than a specific language. Scheme is a lisp. Racket is a lisp. Clojure is a lisp.
People keep making new ones because lisp continues to be a useful idea. It keeps not quite going mainstream, the reasons for which have been subject to much speculation. It's most likely that there will be some semi-popular lisp in 50 years.
MicroEngenho: https://www.youtube.com/watch?v=Mu9CsdGHybw
EDIT: YouTube link
The reader is only shown what they're replacing and how they might be used.
> When I was still researching this fabled obscure language called Lisp one thing people kept saying about it is that “Lisp is a programmable programming language”, but I could never figure out what they meant by that.
Somehow I feel they fell into the same trap.
...at which point it's almost more economical to just read a book about Lisp.
I think showing what instead of how can be instructive and motivating for some readers, but it definitely doesn't satisfy all of them.
It compiles to native, highly optimized machine code.
Common lisp is compiled to optimised native code.
How do you optimize code without knowing the types of variables?
> How do you optimize code without knowing the types of variables?
Very carefully.
For a large amount of programs (basically all "classical" programs like you'd read about in the past rather than modern apps) you have a fairly clear path through the code for their data, this means that you can optimize that path.
This doesn't mean the performance will be as good as a statically typed language but keep in mind 90% of compiler optimization is basically useless on many programs vs. the 10% that makes the important bits run fast.
http://clhs.lisp.se/Body/d_type.htm
I bet Allegro or Lisp Works don't have any issues generating better code than Nim.
There is also https://en.wikipedia.org/wiki/Stalin_(Scheme_implementation) which was doing lifetime analysis waaay back in the mid 90s, but I don't think Suskind had a declare system at all in Stalin. So, places where inference did not work were probably just "slow to be fixed by users tweaking code" to make inference better. I think he was mostly his own main user.
Both Coalton & Carp do look interesting. There can be complaints with how "prefix notation" scales to large expressions. Cython is a gradually typed Python, but late noughties ideas in metaprogramming/macros/etc. never quite took off.
There can be an undesirable effect of gradual/optional static types that code starts off "lazy" and then can require surprisingly large efforts to tighten down, especially in team settings. Even with profiling/90-10 heuristic rules, a later dev trying to optimize earlier lazy work can still be 10X, especially if type dynamism was really leaned upon. So, at least having an easy switch to force early/in-development static typing on original authors is likely a good thing (or at least strongly encourage with warnings...).
Probably doesn't cover the 'optimized native code' part but it obviously could, like many programming languages it is, practically speaking, a one-man show.
Not only is Shen inherently statically-typed (it's not annotations over a dynamic core language) but the type system is based on sequent calculus and is expressive and powerful. It would be nice to see what a Nim-sized community could do with it over five to ten years.
I found the best way to learn shen was the book of shen: https://shenlanguage.org/TBOS/tbos_255.html (dynamic type checking example here)
* (defun adder (x y) (+ x y))
* (disassemble 'adder)
; disassembly for ADDER
; Size: 37 bytes. Origin: #x541CEE1B ; ADDER
; 1B: 498B4510 MOV RAX, [R13+16] ; thread.binding-stack-pointer
; 1F: 488945F8 MOV [RBP-8], RAX
; 23: 488BD6 MOV RDX, RSI
; 26: 488BFB MOV RDI, RBX
; 29: FF14250001A052 CALL QWORD PTR [#x52A00100] ; SB-VM::GENERIC-+
; 30: 488B5DE8 MOV RBX, [RBP-24]
; 34: 488B75F0 MOV RSI, [RBP-16]
; 38: 488BE5 MOV RSP, RBP
; 3B: F8 CLC
; 3C: 5D POP RBP
; 3D: C3 RET
; 3E: CC10 INT3 16 ; Invalid argument count trap
Because Common Lisp is dynamic, you can redefine adder with more information for the compiler to optimize with (such as types or ability to freely inline). The compiler is part of the runtime (which really ups the programmability of the language too, you can teach SBCL's compiler new optimization rules to output even better assembly). So if you Know What You're Doing, you can get rid of the generic add by declaring the expected argument and return types (e.g. a fixnum) and redefining the function: * (declaim (ftype (function (fixnum fixnum) fixnum) adder))
(ADDER)
* (defun adder (x y) (+ x y))
WARNING: redefining COMMON-LISP-USER::ADDER in DEFUN
ADDER
* (disassemble 'adder)
; disassembly for ADDER
; Size: 35 bytes. Origin: #x541CE8DC ; ADDER
; DC: 498B4510 MOV RAX, [R13+16] ; thread.binding-stack-pointer
; E0: 488945F8 MOV [RBP-8], RAX
; E4: 488D1C3E LEA RBX, [RSI+RDI]
; E8: 488BD3 MOV RDX, RBX
; EB: 48D1E2 SHL RDX, 1
; EE: 700A JO L0
; F0: 488D141B LEA RDX, [RBX+RBX]
; F4: 488BE5 MOV RSP, RBP
; F7: F8 CLC
; F8: 5D POP RBP
; F9: C3 RET
; FA: L0: CC52 INT3 82 ; OBJECT-NOT-FIXNUM-ERROR
; FC: 0E BYTE #X0E ; RBX(s)
; FD: CC10 INT3 16 ; Invalid argument count trap
Note SBCL still has some safety and debugging bits in there. You can tell it you Really Know What You're Doing, and do so locally so you don't put the rest of your code at risk: * (defun adder (x y)
(declare (optimize (speed 3) (safety 0) (debug 0)))
(+ x y))
WARNING: redefining COMMON-LISP-USER::ADDER in DEFUN
ADDER
* (disassemble 'adder)
; disassembly for ADDER
; Size: 9 bytes. Origin: #x541CEAB6 ; ADDER
; 6: 4801FA ADD RDX, RDI
; 9: 488BE5 MOV RSP, RBP
; C: F8 CLC
; D: 5D POP RBP
; E: C3 RET
Because of the use of ftype, SBCL will let you know against certain kinds of errors elsewhere, at compile time: * (defun mistake () (adder 5 "5"))
; in: DEFUN MISTAKE
; (ADDER 5 "5")
;
; caught WARNING:
; Constant "5" conflicts with its asserted type FIXNUM.
; See also:
; The SBCL Manual, Node "Handling of Types"
;
; compilation unit finished
; caught 1 WARNING condition
MISTAKE
* (defun mistake2 () (adder 5 (subseq "555" 0 1)))
; in: DEFUN MISTAKE2
; (ADDER 5 (SUBSEQ "555" 0 1))
;
; caught WARNING:
; Derived type of (SB-KERNEL:VECTOR-SUBSEQ* SB-C::SEQ SB-C::START SB-C::END) is
; (VALUES (SIMPLE-ARRAY CHARACTER (1)) &OPTIONAL),
; conflicting with its asserted type
; FIXNUM.
...
(You would normally see these as you go as you send forms or whole files/buffers to the REPL from your editor, but you can also hook up a sort of 'linting' tool to catch them earlier, or at least fail CI jobs. I use vim rather than emacs and can detect such issues at file save-time; with some work on the underlying tooling it would be possible to detect them when leaving insert mode or switching buffers.)Of course, because Lisp is dynamic, these are just "warnings" because you can always just redefine (and recompile) these things to fix issues without having to restart your program, or recompile the whole system yet again. And when you leave debugging on or even increase its priority during development, you get an experience in iterative creation unmatched by anything else.
Currently, looking at doing a fluent-style interface in C# (same as host language) which is inspected/compiled at runtime using Roslyn.
In other words: if your function is able to produce (+ 1 2), same function produces 3, when used as macro.
a lambda evaluates all it's args, a macro not.
Metaprogramming allows you to write a function that generates the boilerplate, and transparently use that function as if it's a brand new keyword in your programming language. That function in metaprogramming circles is called a "macro", not to be confused with C macros.
Stated another way: a macro = a function that takes as input the code you want to write, and provides as output code that your programming language can actually execute. (It's like a super miniature compiler.)
For example, let's pretend C is metaprogrammable.
Perhaps I want a new keyword "pfor" which is like "for" but executes the loop in parallel.
int x[20];
pfor(i = 0; i < 10; i++) {
x[2*i] = expensive(2*i);
x[2*i+1] = expensive(2*i+1);
}
Here, we assume "expensive" is some really expensive function that maybe might take several seconds to execute, which is why we want to parallelize it.This "pfor" is just a sort is pseudo code. In reality, you know how we would actually have to write this: make a loop in which we spawn 10 threads, set i to be a thread local variable in each, call a callback function in each thread
void thread_callback(int i){
x[2*i] = expensive(2*i);
x[2*i+1] = expensive(2*i+1);
}
and join the threads at the end. This would have the equivalent effect as our desired "pfor" keyword.In a metaprogramming language, we can add this keyword by just telling the compiler what code to generate any time it sees "pfor". We could write something like
macro pfor(var = init, cond, incr, body...) {
symbol callback = generate_random_symbol();
```
void $callback(int $var) {
$...body;
}
vector threads;
for($var = $init; $cond; $incr) {
threads.push(make_thread($callback, $var));
}
for(t in threads)
join_thread(t);
```
}
The triple ``` starts a template, and the $ escapes a template. We have to generate a fresh symbol for the callback name so that they don't collide when we use "pfor" multiple times.Now every time we use "pfor", the compiler will look at our homemade code generator function and write out the code for us.
So in effect by telling the compiler how "pfor" can be implemented in more basic code, we've added "pfor" to the language.
This isn't a separate tool however! It's not a pre-processor or anything like that. It's built in to the language (hence why it's a programming language paradigm).
Since macros in effect receive structured source code (basically an AST) as input, you can also do analysis of that code. For example, a macro to simplify algebraic expressions at compile-time.
Abstractions based on high-order functions also tend to allocate closures at run-time, so in practice you frequently get a performance penalty for their use.
High-order functions remove the need to use macros in some cases, but definitely don't replace all of them.
Is there anything you should do with macros that you can’t do with similar language features like higher order functions?
My concern with macros is that they are completely open-ended, so you can really do anything - and if you can do anything, then the language is not usefully defining a strict enough semantic structure. As an example, see the 'hygiene problem'.
I am providing language-level APIs for a complex web service, one in Python and one in Common Lisp. The service has dozens of operations, all of which are wrapped around a REST API that takes query strings and headers in its inputs, and produces very different outputs. Model for this is something like S3.
In most languages, I have two choices: I can push the complexity to the users, making them plug the query string values into a big map or option object; or I can write dozens of functions, most of which are boilerplate.
In Python I wrote dozens of functions (I obviously reused a set of central ones, but there's lots of scaffolding to do, e.g., translation of arguments from user types to strings appropriate HTTP headers). In Lisp I wrote a macro that operates on a set of query string and header inputs, and then defines a function appropriate for the user-level API. When I fix bugs in the macro, I automatically fix all of the functions.
The trade-off is that the macro is itself a function that's operating at two different semantic levels (pre- and post-compilation), and it might be challenging to read for newcomers to Lisps. But this let me hide all the nasty API complexity from my users while writing substantially fewer lines of code. New additions are a single defmacro line.
For example: we may want a macro which outputs the source it encloses, before execution:
CL-USER 12 > (defmacro my-loop (&body things)
`(progn
(pprint (list :doing-loop-with ',things))
(loop ,@things)))
MY-LOOP
CL-USER 13 > (my-loop for i from 1 upto 3 sum
(my-loop for j upto 3
sum (* i j)))
(:DOING-LOOP-WITH
(FOR I FROM 1 UPTO 3 SUM (MY-LOOP FOR J UPTO 3 SUM (* I J))))
(:DOING-LOOP-WITH (FOR J UPTO 3 SUM (* I J)))
(:DOING-LOOP-WITH (FOR J UPTO 3 SUM (* I J)))
(:DOING-LOOP-WITH (FOR J UPTO 3 SUM (* I J)))
36
A normal function can't do that, since it does not get source as an argument, but just sees the arguments values.Since a macro sees source, it can not only print it, but also simplify, transform, mutate, etc. the source.
One nice thing I've used macros for is reading and parsing files at compile-time, to bake-in data into programs. That way, the compiled programs just process stdio, without having to care about file paths, permission errors, parse errors, etc. (those all become compile errors)
So I am not sure, I will succeed at it, but I am saving your comment, to try when they are ready. 10 years might be doable. Probably depends how many more times they hit their head..
(And whether I can spark enthusiasm in them, but robots are fun..)
for i in 1 to 10 do printf("%d\n", i)
Becomes (through a preprocessor in this hypothetical system) valid C as: for(int i = 1; i <= 10; i++) { printf("%d\n", i); }
Now that's not too interesting, but it is a bit (subjective) nicer looping syntax than C's straight syntax. In the Lisp case, macros allow you to take an arbitrary set of values (the parameters to the macro) and produce some other value or values in their place. This could be just executing the code to get a kind of compile time evaluation: (hypotehtical-expression-evaluator-macro (complex-computation-that-gets-reused-100-times-but-takes-10-minutes-each-time))
Or it could be more transformative, like Lisp's loop macro that lets you write an Algol-ish loop instead of a lisp-y loop (or using recursion or other approaches): (loop for i from 1 to 10 do (print i))
Which gets transformed into some other valid Lisp code (not on my laptop so not in a position to actually show the result).David Moon's "Moon's Mark Down" is actually a pretty neat case of Lisp meta programming. He defines a macro defdot which will take a name for a particular directive for his markdown syntax and generate all the necessary functions so that things "just work". And when parsing, when a . is found at the start of a line, the correct handler is calculated and called. To add a new directive, you add a new defdot and that macro will create the relevant functions and register the new handler.
Metaprogramming is "programming programming". Consider "programming" as writing a series of steps for a computer to follow. Metaprogramming is writing a series of steps for a computer to follow to write a series of steps for a computer to follow.
When working with LISP, metaprogramming is just a normal way to program things that's built into the language. Rather than writing everything as a linear program as you would in conventional languages, you can write a program that "writes itself" (or modifies parts of itself).
While you could meta-program in many languages (using code generators and whatnot), in LISP this is easy because the programs themselves are written in a way that makes them easy to be manipulated by other parts of the program. This is why you'll see so many parentheses when looking at LISP code. The simplicity of the syntax is also what makes it so powerful.
Metaprogramming is a technique that often results in much shorter programs which can make single programmers very effective. The downside is that the higher level of abstraction (leaving ELI5 zone here, sorry) can make the workings of LISP programs quite opaque to newcomers as things seem a lot like magic. Therefore preserving the metaprogramming advantages in larger teams requires _especially_ good documentation and attention to design.
Macros are "just" functions whose inputs are S-expressions and outputs are S-expressions, and they are run at compile-time and not run-time. The semantics of the execution of a macro corresponds more or less to the semantics of Lisp itself, which, in a simplified view, can be completely understood in a first course in computer science (as in SICP).
Any statements about correctness of a macro will derive from statements about correctness of code more broadly, for which there's a lot of literature and is an ongoing active and popular field of research.
Any statements about the security implications of a macro will derive from (1) statements about the security of a compiler more generally and (2) statements about code more broadly, both of which have lots of literature, and both of which have active research communities.
I don't know if there are many useful things to frame around security and correctness about macros specifically (without resorting to the above derivations), which is probably why there's no literature that I know of about it.
There is no the implementation of JavaScript. There are several, all with their own issues, security and otherwise.
(The implementation is usually the one with the security issues, not the programs you run on it. Of course, either one’s possible if it crosses a trust boundary.)
I don't think anybody has done a formal study or written some kind of report on the security of said implementations, however.
A gentleman named Paul Dietz implemented something called the "ANSI compliance test suite" [1], which is an implementation-agnostic battery of tests that examines how well an implementation adheres to the ANSI specification of Common Lisp. Dietz is also known for his prolific fuzz and randomized testing of Common Lisp implementations. He's been a source of obscure bug reports for years, and those bugs are always promptly fixed.
I did think of a specification-level security issue - bignums reduce math errors but mean operations are O(N), which puts you at great risk of DoS issues and sidechannel leaks in security-sensitive math.
And of course Lisp’s features like macros tend to be powerful, and security has a “rule of least power” meaning those are harder to verify if you can’t statically analyze them.
Sure, the Common Lisp standard specifies a default syntax mode, where the s-expression reader (-> the function READ) can execute arbitrary code at read time.
It does still have the other similarities; it has closures and a prototype based object system. In the 90s we only had boring Blub languages, so most of the influence that allowed such powerful concepts came from Lisp. (Though I think the prototypes are from Self.)
JS has other differences on the “correctness” spectrum, like all numbers are doubles instead of bignums. But even if that’s “incorrect”, just imagine how much more memory your Chrome tabs would use if you let people use bignums everywhere.
Bye, why would a fully numeric tower js only use the least machine friendly format everywhere ? Did I miss something ?
do you have any link to a source for this claim?
AFAIK JavaScript started as a language called Mocha, which was hacked together in a few days: Java syntax, Scheme functions + Self object system + plus a bunch of other things.
At that time: no symbols, no s-expressions, no cons cells, no lists, no macros, no tail call optimization, ...
And what do you mean about doing what you mean it to? The semantics of macro expansion are well-defined.
Does Lisp do anything more than other similar languages to help you avoid mistakes in meaning? I guess not. But it doesn’t claim to, as you say, so what’s the problem?
Lisp advocates don’t talk about something Lisp doesn’t claim to do and most other languages don’t do. Why’s that surprising?
But the main argument is that Lisp is hyperproductive if you’re an individual brain genius who’s memorized every emacs key combo. And my point is that it doesn’t matter if you can produce something fast if you can’t know if that thing actually works.
This is the traditional “static vs dynamic typing” argument but you know, a lot of new languages have new kinds of safety features going beyond “static typing”.
And of course Lisp does make unusual decisions affecting safety. Making all numbers bignums means math is more likely to be correct since it won’t overflow, but untrusted user input can pretty easily DoS you.
> But the main argument is that Lisp is hyperproductive if you’re an individual brain genius who’s memorized every emacs key combo.
I certainly haven't, though I suspect this is a bit of an exaggeration.
In reality things that are hopelessly broken and released quickly win market share all the time which is in contradiction to your statement.
The old Tony Hoare quote seems apt: "There are two methods in software design. One is to make the program so simple, there are obviously no errors. The other is to make it so complicated, there are no obvious errors."
Macros allows you to decompose and simplify problems in a way that is impossible with functions. Macros are especially good at removing boilerplate and writing syntactic glue code.
Since macros are programs making sure that they do what you meant them to do you can use similar techniques as you use for other programs: making them small and obvious, testing.
You can, but there is a simpler and more natural one to one mapping: 2[two] +[plus] 3[three] =[equals] 5[five]. 'Two plus three equals five'.
Unfortunately, once you have more than 1 programmer involved in the project, you end up with slightly different versions the same concepts.
Any reasonably run project will have standards and code review.
I have not, once, in my decade+ career writing Common Lisp professionally, run into a single instance where my peer programmers on a project have painted one another into silly corners due to unfettered use of macros of any ilk. It doesn't happen, because (1) code comes from some manner of requirements and (2) PRs are reviewed. Macros that don't make solutions easier to read and write for the team, not just a programmer are patently rejected.
* I feel this fallacy and others frequently come from people who hear about a relatively obscure feature or superpower of a language and—having never used it—then try to imagine pessimistic or pathological uses of said feature. Unfortunately, the speculation is usually flat-out wrong, and not even adjacent to some truth.
My guess would be that people that come from C++, where meta programming is insanely complex and hard to follow, would guess that a language that encourages meta programming would often lead to complex and hard to follow solutions. A DSL that looks simple on usage but then looking into how it works is 10 nested complex compile time macros.
I don't necessarily like learning a bunch of DSLs either, but I'm not sure the ruby situation is that different from what is always the case, just more honest about it. I've been trying to pay attention to the problems I solve and noticing early when they benefit from being explicitly defined as a little language and it's been quite successful in a few cases.
It reminds me of something a mentor pointed out early in my career that has remained a valuable insight: a whole lot of real world logic is implemented with state machines, but the only reliable and maintainable ones are where the author knew that from the beginning. Often we're using a known pattern to solve a problem without recognizing and naming it, so missing opportunities to apply hard-won knowledge and tools from elsewhere.
Yes. A macro gives a certain programming feature a name and a syntax. But with a function we also need to give it a name and we need to define the arguments. In the case of function arguments, we also need to know what the input and output of those function needs to be.
For example in Lisp there is a macro WITH-OPEN-FILE, which opens a file, executes code using the input stream and then closes the file. It also ensures that the file is also closed in case of an error.
(with-open-file (stream filename)
(read stream))
We now may have two other ways to deal with that often needed functionality: write the code or use a function which does the same (here in this example this is possible, but often a macro does something which a function can't).First we write the code. We may pull this from a snippet library. We have to remember what the primitives are and what they do.
(let (stream
return-value)
(unwind-protect
(setf return-value
(progn
(setf stream (open filename))
(read stream)))
(close stream)
return-value))
Or we write a function. This gets a name CALL-WITH-OPEN-FILE, a list of arguments and in case of the function it needs to now what the input and outputs are: a stream is the input and the result is arbitrary, but will be returned by CALL-WITH-OPEN-FILE. (call-with-open-file
filename
(lambda (stream)
(read stream)))
For the macro we need to know:* the name of the macro
* the syntax of the macro:
with-open-file (<symbol, non-evaluated> <filename, evaluated>)
form*
* what the generated code is supposed to doFor the function we need to know:
* the name of the function
* the list of arguments and their order
* the types and semantics of the arguments: a filename and a function which takes a stream
* what the function is supposed to do
The effect is basically the same: both ways are essential leading to a DSL, where the language operators and their usage has to be learned.
And thereafter you can consider if you're seeking a powerfully modifiable reader, whether through macros or other, or if you're looking to modify the executor.
example (A' 2 3) to access row 2 and col 3 of a transposed matrix A
In Common Lisp, it's much easier if you prefix. It would totally be possible to do something like
[A x y]
to be an array accessor at indexes x and y by defining reader macros for the characters '[' and ']'. And then one could define the reader macro for the character 'ᵀ' so that ᵀA
expands to the transpose of A. Then you could write [ᵀA x y].
If you didn't like this prefix thing, you could instead be more elaborate with '[' and parse something more complicated with postfix notation.