| ▲ | paddy_m 3 days ago |
| I have heard multiple people claim that macros are incompatible with strong or static typing and I don't see why. If there were a lisp with optional static typing like typescript, it would seem to me to be completely possible to write macros that write types. In many cases it woudl do away with the need for generic types (and allow multiple competing syntaxes for dynamic types). Most interestingly it would allow you to write new generic forms instead of waiting for whatever the language designer gives you. It would also allow you access to types at runtime (which the typescript language designers took away). Maybe people were telling me that lisp style macros were incompatible with hindley millner typing, but I still don't see how. The macros would just emit a hindley milmner subset. What am I missing? |
|
| ▲ | reikonomusha 2 days ago | parent | next [-] |
| As far as your first question is concerned, macros in and of themselves are not incompatible with a typed language. Coalton uses Lisp macros, for example, and they work seamlessly and as expected. But a Coalton macro is typically written in Lisp, not Coalton. This isn't in and of itself a problem---it's very easy and straightforward thus to write Common Lisp-style macros in Coalton. The difficulties arise when the macro must itself be (1) written in a statically typed manner, which gets into having a complete typing on your metalanguage (or your AST), and (2) allowed to access the type environment of the surrounding context in which the expansion is happening. If (2) should be accomplished, then the type-checking machinery must collaborate with the macro-expansion machinery, and that in practice makes both tasks very difficult to specify semantics for and implement. The language Hackett [1] worked toward solving the problem of having true typed and type-aware macros (they call them "type-aware" and "type-directed" macros [2]). Development unfortunately ceased ~7 years ago. [1] https://github.com/lexi-lambda/hackett [2] I think this video has a good discussion of Lisp, macros, and static types, from the perspective of implementing a Haskell in Racket. https://www.youtube.com/watch?v=5QQdI3P7MdY |
| |
| ▲ | spooky_deep 2 days ago | parent [-] | | Since the macros run at compile time, I am ok with them not being statically checked. Statically checked macros seems like an academic curiosity. What am I missing? | | |
| ▲ | reikonomusha 2 days ago | parent | next [-] | | Then nothing. Macros work. Silly example: COALTON-USER> (defmacro rpn (x y op)
`(,op ,x ,y))
RPN
COALTON-USER> (coalton-toplevel
(define (double x)
(rpn 2 x *)))
;; DOUBLE :: ∀ A. NUM A ⇒ (A → A)
COALTON-USER> (coalton (double 3.0))
6.0
| |
| ▲ | wk_end 2 days ago | parent | prev | next [-] | | One concern I’d have is that any type errors would be reported on the macro expanded code and thus be pretty much inscrutable outside of toy examples. | | |
| ▲ | reikonomusha 2 days ago | parent [-] | | I think you're right that debugging errors involving complicated macros can get difficult, but to at least make the situation more tolerable, when Coalton expands a macro, it remembers where the expansion came from, so an error will be reported in the right place with the right source code. For example, using the RPN macro from the sister comment, here's an intentional type error: COALTON-USER> (coalton (rpn "x" "y" +))
--> <macroexpansion>:1:9
|
1 | (COALTON (RPN "x" "y" +))
| ^^^^^^^^^^^^^^^ expression has type ∀. (NUM STRING) => STRING with unresolved constraint (NUM STRING)
| ^^^^^^^^^^^^^^^ Add a type assertion with THE to resolve ambiguity
[Condition of type COALTON-IMPL/TYPECHECKER/BASE:TC-ERROR]
|
| |
| ▲ | rscho 2 days ago | parent | prev | next [-] | | Statically checked macros are useful to introduce new syntax at no runtime cost. | |
| ▲ | taeric 2 days ago | parent | prev [-] | | The big miss here is that "compile time" is typically understood to be "batch compilation" time for languages. For Common LISP, macros run at read time. Which is often doable during runtime. | | |
|
|
|
| ▲ | kscarlet 2 days ago | parent | prev | next [-] |
| It's easy to just stick them together, but to me (who writes too much Lisp for my own health) this is unsatisfactory. The dream: just like macro can be seen as a (staged) extension mechanism for Lisp evaluator, there should be an extension mechanism for the static type system, which allows me to define new types, define new syntax (like Haskell do-notation) which makes use of typing environment and expected type of current context (return-type polymorphism), etc. The reality: very few environments figure this out. In Coalton Lisp macros do work, but only at the level of untyped S-expr. A Lisp macro can't know about types of the variables in the lexical environment, or expected type of its own context. But it quite possibly works fine for the "typescript-like" use case you described. The problem I see: H-M type system isn't designed with extensibility in mind, and it's hopeless to make it extensible. More technical explanation of why it's hard to integrate with Lisp macro is that H-M relies on a unification-based inference stage which execution flow is very different from macro expansion. Possible solution: There's no fundamental reason why static type can't have something as powerful as Lisp macro. However first of all you would need an extensible type system, which seems to still be an open research problem. I think bidirectional type system is hopeful -- it's so different from H-M at a fundamental level though that I think it's hopeless to retrofit into Coalton. |
| |
| ▲ | Xmd5a 2 days ago | parent | next [-] | | There are several ways to look at lisp macros but I think the most fundamental aspect is that they are hooks that allow one to run code at compile time in a dynamic compilation environment. In other words with lisp macros comes compile-time state. This is a big deal. Imagine the following piece of code: (serveMQTT ...)
(serveMQTT ...)
(serveMQTT ...)
(serveMQTTDoc ...)
With serveMQTT as a macro, we could store each mqtt topic, accepted args, etc ... into a compile-time datastructure. serveMQTTDoc, a macro too, would fetch the content of this datastructure, generate the documentation string for all mqtt topics at compile time, and return this string at run-time when queried.Compile-time state and a language that is also its own macro-language.
That'd be a nice thing to have. | | |
| ▲ | kazinator a day ago | parent [-] | | So you might think, but macro-expansion as it is implemented in mainstream Lisp dialects is separate from compilation and isn't informed by compilation. A form to be compiled is first subject to expansion, and then the expanded result, now devoid of macros is compiled. Implementations that have multiple modes of processing, such as both a compiler and interpreter, share the macro expander between them. There is redundancy between macro expansion and compilation. The macro expander has to understand and properly traverse special forms, and build up an environment structure that follow lexical scopes. Then the compiler has to do the same. Of course, the compiler won't have macro content in its environment stacks (lexical macros); those are gone. |
| |
| ▲ | JonChesterfield 2 days ago | parent | prev [-] | | What's the objection to putting type annotations on the macro, and refusing to compile code where the arguments to the macro don't match the type? | | |
| ▲ | kscarlet 2 days ago | parent | next [-] | | There's no objection, but I think this solves a different problem than above described, and is possibly a more complicate matter in itself than it may look like. First, are you describing a way to typecheck macro so that it doesn't generate ill-typed code? The type system that does this is active research (I think requires modal typing?). Also this does not increase the expressive power of untyped Lisp macro -- just making them safer, while I was proposing their expressive power is unsatisfactory and should be enhanced via a sort of reflection. That would only makes them even harder to type! | |
| ▲ | reikonomusha 2 days ago | parent | prev [-] | | Since a macro is a definition of syntax, I think you'd essentially need something like typing judgments to show how the typed elements of the syntax relate to one another, so that the type checker (e.g., a typical Hindley-Milner unifier) can use those rules. These are usually written as the fraction-looking things that show up in PLT papers. This is, as GP says, essentially extending the type system, which is a task fraught with peril (people write entire papers about type system extensions and their soundness, confluence, etc.). | | |
| ▲ | JonChesterfield 2 days ago | parent [-] | | Depends on your point of view really. I'd define a macro as a function with slightly weird evaluation rules. If you want to write a macro where any call to it which typechecks creates code which itself typechecks, you have to deal with eval of sexpr which is roughly a recursive typecheck on partial information, which sounds tractable to me. |
|
|
|
|
| ▲ | 2 days ago | parent | prev | next [-] |
| [deleted] |
|
| ▲ | deterministic 2 days ago | parent | prev [-] |
| You are missing nothing. Haskell has two different macro systems: typed and untyped. |