-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: macro expressions instead of macro bindings #51
Comments
This is a really interesting proposal. Thank you for writing it up! I do think that many of the things you’ve written make a lot of sense. I think it’s extremely likely that we want to be able to write macros in Hackett, not Racket, though I’m not sure if it’s an immediate concern of mine. Still, I don’t think it should theoretically be all that hard! As you describe, the right first step is to make something like a That said, I’m a little less convinced about the notion of automatically rewriting bindings and expressions to be macro bindings and macro invocations based exclusively on type information. That might be more feasible in a language with a simpler type inference scheme, but I think it would be potentially tricky to do right with Hackett. More to the point, however, Racket has notion of phases. Local macros are defined with Your first example of a local macro is essentially exactly the same with (defn cross-product : {Vec -> Vec -> Vec}
[[(Vec x1 y1 z1) (Vec x2 y2 z2)]
(let-syntax ([submul (syntax-parser
[(submul u:id v:id)
#`(- (* #,(suffix-id #'u "1") #,(suffix-id #'v "1"))
(* #,(suffix-id #'v "2") #,(suffix-id #'u "2")))])])
(Vec (submul y z)
(submul z x)
(submul x y)))]) Your section on macro combinators is a little more interesting. My gut instinct is that you really do want to have a We can do that, though, since Racket’s phasing system means we can theoretically just |
Concerning "local macros which look like lambda functions", I have built something along these lines as part of my type-expander library for Typed Racket (see here for the part of the docs which covers this aspect). @gelisam's example…
… would be similar to a type-level macro, which builds the type for a list of length 3 whose elements must be 1, 2 and 3:
Allowing expressions of the form Unfortunately, I haven't written enough type expanders to actually use this feature, so I really cannot say whether such a thing is a good idea or not. As @lexi-lambda says, this can most of the time be rewritten using I think it's a feature that sounds nice on paper, but is of little use in practice. I'd be interested to know if this does get implemented elsewhere, though, as I lack enough usage data to have a strong opinion on this feature's usefulness. |
Now there is: Typer: An infix statically typed LISP |
I'm surprised to see that this issue is still open, since you have rejected my proposal. You counter-proposed:
followed by userspace experimentations towards a typed |
I’m afraid I haven’t had much time lately to work on Hackett, which is why some maintainership duties have slipped, but I’m hoping that will change in a couple of months. Anyway, I did actually start working on a prototype implementation of types for syntax things (see this tweet and this other one), but there were some subtleties I never quite worked out. I do want to take the time to watch the video you linked, though. |
Acknowledgement
This proposal is inspired by a presentation on Typer I recently saw at McGill University's 2017 Programming Languages Workshop. There doesn't seem to be any information on the internet about this academic programming language yet. Typer's goals seem very similar to Hackett's, as it also wants to interleave macro-expansion and type-checking. The main difference is that Typer is a dependently-typed programming language, which aims to combine Lisp+OCaml+Coq instead of Haskell+Racket.
Proposal
Hackett macros currently have to be defined at the top-level, using the special form
define-syntax-parser
:After that definition, the identifier
list
is known to be a macro, so later occurrences of(list ...)
are macro-expanded instead of being treated as a function application:The proposal is: instead of a special form, let's have a special type. This type is an otherwise ordinary wrapper around a function which manipulates syntax objects:
The type needs to be treated specially: when type-inference detects that an expression of type
Macro
is applied to an argument, it knows that this is not a function call which will be evaluated at runtime, but a macro which must be expanded at compile-time. For example:Such expressions may refer to locally-bound variables:
In that case, the expression to which the locally-bound variable refers must be evaluated at compile-time. If that expression refers to other variables, then those must be evaluated as well. Obviously, this is only possible for constant expressions, for which all the information is available at compile time; if the expression depends on a lambda-bound variable (such as one bound by the
do
macro), this results in a compile-time error. Typer calls it an "elaboration error".Type inference
Worryingly, since the application
(f x)
could either be a macro application or a function application, we can no longer deduce that there are some typesa
andb
for whichf
has type{a -> b}
andx
has typea
. We can't even deduce thatx
has a type, because it could be some keyword recognized by the macro! That sounds pretty bad.Fortunately, since we know that the only valid macro expressions are constant expressions, intuitively we should always be able to determine from the surrounding context whether that expression is a macro or not. More formally, the algorithm for bidirectional type-checking first infers the type of
f
, and if it has the form{a -> b}
, it checks thatx
has typea
. So changing the algorithm to not checkx
if it turns out thatf
has typeMacro
doesn't affect type inference at all.Motivating examples
The example given at the top was chosen for its simplicity, but it didn't really demonstrate the advantages of the proposal over
define-syntax-parser
. Here are some better examples.Local macros
One advantage is that macros can now be defined in a small local scope. For small syntactic improvements which aren't reusable outside of a narrow scope, it makes more sense to define macros locally than at the top-level. For example, in the cross-product implementation below, there is a pattern which is repeated 3 times with small variations:
That particular pattern isn't likely to be repeated anywhere outside of that function, so it would be nice to define a local macro eliminating the duplication:
Of course, the fact that we can't currently define local macros in Hackett isn't very limiting, as we can simply define a top-level macro whose name indicates that it isn't used anywhere else:
This example also demonstrates one way to implement DefMacro: by rewriting the program into a form in which all the macros are at the top-level.
Macro combinator libraries
Libraries like turnstile and syntax/parse demonstrate that it is possible to define "meta-macros", that is, macros which define other macros. The idea is to define a DSL for describing macros, and to write a meta-macro which interprets this DSL. Well, in Haskell, our DSLs are combinator libraries, made out of precise types and reusable abstractions like Applicative and Monad, so if we're going to have a DSL for describing macros, it would be nice if that DSL could be a combinator library. Now that a macro is a normal value, of type
Macro
, which can be constructed using normal expressions, we can do precisely that.Now that we have a library for creating macros based on a Template, we can now rewrite our previous example in a much more readable way, by defining a local Template and using
runTemplate
to turn it into a local macro:This time, the version where all the macros are at the top-level has to run some Hackett code at compile-time, and has to be careful to define the macros in the right order and in the right phase, so having the compiler do that for us is a lot more valuable.
The text was updated successfully, but these errors were encountered: