5 ms·
> I always feel that for lisp-like languages it's just not a good idea to make them statically typed. Their power is derived from macros and the fact that code
by tines 5y ago
> I always feel that for lisp-like languages it's just not a good idea to make them statically typed. Their power is derived from macros and the fact that code is data and the other way around.
I completely agree with the idea that their power is derived from macros, but I vehemently disagree that macros are incompatible with a static type system. The type of a form can be just a simple ADT
data Form = FInt Int
| FCons Form Form
| FNil
| FString String
| ...
and a macro is just a function taking Form as arguments and returning a Form. With standard HM type inference, your macros can look exactly like they do in a dynamically typed Lisp. If the backquote is just a reader macro that produces a Form:
(defmacro double (x)
`(* x x))
that would be expanded into
(defmacro double (x)
(FCons '* (FCons x (FCons x FNil))))
and type inference would determine that the type of double is
double :: Form -> Form
And destructuring Forms is just normal ADT pattern matching.
- valenterry 5y ago> and a macro is just a function taking Form as arguments and returning a Form Why would it not just be a normal function then?
- nerdponx 5y agoBecause it's evaluated at compilation time.
- dgb23 5y agoNitpicking: I think we say „expanded“ since evaluation is reserved for runtime.
- tines 5y agoFor the same reason that it wouldn't be a normal function in any Lisp. The difference is that the macro expander keeps a list of macros (separate from the set of normal functions in the program), so that when it sees a list with a symbol in the first position, it can look up that symbol in its macro list, and if it is a macro, it can call the macro and replace the macro call form with the return value of the macro. Then that gets fed into the evaluator, just like how macros work in any lisp.
- valenterry 5y agoI see, thank you so much for the explanation. I think to clarify what I said - I didn't mean that static typing and macros are excluding each other. But I think you can either have a sound static type system and highly restricted macros, or less restricted macros but only an unsound type system. That's what I meant with "conflict".
- tines 5y agoNo problem. I'm interested to hear though why you think unrestricted macros make the type system unsound. Can you explain? The code generated by macros would be type-checked in the scheme I described, and type-unsafe code can't be executed by the macro itself, so it seems safe to me.
- valenterry 5y agoThat is more of my impression that I have gathered from watching programming languages evolving. Rust is a good example, where they tried hard and the macros still become unsound. Scala is another good example where macros where unsound first and they had a really hard time to make them sound and work with the type-system - they lost a lot of power in the process. Now, that doesn't have to mean that there is some inherential that stops the combination from super powerful macros and sound statical types from working together, but if anything, there is at least a compromise to be made in terms of resources, because it seems you would have to put a lot of effort into it. > The code generated by macros would be type-checked in the scheme I described, and type-unsafe code can't be executed by the macro itself Well, I would already consider this to be quite a big restriction though.
- tines 5y ago> Well, I would already consider this to be quite a big restriction though. Which part, the type-checking? Don't we want it to be type-checked?
- 5y ago
- andyferris 5y agoIn some languages they can be, and macro calls are specially marked at the call site instead of (or in addition to) the definition. But in LISP they are called and expanded implicitly, with the compiler/interpretter keeping track of what's what.
- wavegeek 5y ago> (defmacro double (x) > `(* x x)) Two bugs in two lines. Lisp is indeed a powerful language.
- stonewareslord 5y agoWhat are the bugs? I am not familiar with lisp
- wk_end 5y agoNot OP so there might be others, but the two that I see are: * most crucially, x isn't actually spliced in, meaning that the macro always literally expands to (+ x x). For example, (+ (double 2) 5) just expands to (+ (+ x x) 5), which will either crash if x is undefined or do something unexpected if x is. * Even if x were spliced in properly, it gets evaluated twice. That's wasteful at best, and if x has some kind of side effect you (arguably) would get unexpected behaviour - the side effects would run twice.
- tluyben2 5y agoYep, see for example [0]. https://www.emacswiki.org/emacs/MacroUtilities https://www.emacswiki.org/emacs/MacroUtilities
- sedachv 5y ago> meaning that the macro always literally expands to (+ x x) Make that three bugs. The macro is called "double," but the numeric result is square.
- simiones 5y agoJust for completeness, the correct macro would look like this: (defmacro double (x) (let ((temp (gensym)) `(let ((,temp ,x)) (+ temp temp)))) This way, when you write (double (parse-integer (read-line)) it would expand to (let ((#:uniqueName123 (parse-integer (read-line))) (+ #:uniqueName123 #:unique_symbol_123)) This guarantees that that the macro will not accidentally refer to some outside variable, and that it's argument will only be evaluated once (so that we don't read two lines of input in this example). To explain a little bit what is going on: normally if you want to have an s-expression as a piece of data, you can use the quote special form - (quote (a b c)), usually shortened to '(a b c), returns a list containing three symbols (think of these as special strings), "a", "b", "c". If you want instead to evaluate a variable named "a", you can use the ` syntax, together with , and ,@. That is, `(,a b ,@c) will produce a list containing the value of a variable named "a", the symbol "b", and the value of a variable named "c", spliced in. If a is '(1 2 3) and c is '(4 5 6), `(,a b ,@c) will return the 5-element list ((1 2 3) b 4 5 6). Depending on how this is used further, b itself may be evaluated or just printed as is. So, when expanding the macro, x will be initialized to the form provided as argument to double (not the value of that form). Then, temp will first be assigned a value that is produced by gensym, which generates a unique symbol; then, we'll return a list that represents some Lisp code binding the form represented by x to a variable whose name is the value returned by gensym, and then using this same variable name in a call to +. Finally, if this macro was "called" from regular lisp code, the expression it returned will be compiled or interpreted. The macro could also be called from a special form like macroexpand-1, which would just return the list returned by the macro, without evaluating it; or macroexpand, which would do the same but recursively until there are no more macros in the expansion. Note: a symbol is basically a string that can be used as a Lisp identifier, and is registered as such in the Lisp runtime. It is a separate type from string, but you can create a string from a symbol, or try to create a symbol from a string (which fails if the string is not a valid Lisp identifier).