4 ms·
Many people seem to write DSLs these days. My goal here was simply to encourage people who might not typically work in a functional style to think abo
by tel 7y ago
Many people seem to write DSLs these days. My goal
here was simply to encourage people who might not
typically work in a functional style to think about
how continuations might be useful in the design of
their languages, even if it seems their language is
imperative.
And since you quote a line a few after that paragraph, it's not like you didn't give yourself the opportunity to read it.
He's not trying to do very simple math. He's teaching fundamental DSL design techniques.
- neilv 7y agoContinuations are a powerful thing to know, but, for promoting DSLs, I hope no one thinks they have to wrangle continuations to implement DSLs. One way, if you're using Racket (or another Scheme or descendant), is to translate your DSL to normal syntax in Racket much like you'd write manually (and you can use imperative or only pure-functional features, your choice). For example, were I implementing the language used by the first example in the article, I might say each assignment establishes a lexical scope, and simply translate the source syntax to nested `let` form syntax: (let ((a (/ 1 (+ 1 7)))) (let ((b (* 2 (+ a (sqrt a))))) (let ((c (/ (atan b) (+ 1 c)))) (+ c (* 2 d))))) Or (since there wasn't any non-assignment code in between the assignments), how you might normally write it manually in Racket: (let* ((a (/ 1 (+ 1 7))) (b (* 2 (+ a (sqrt a)))) (c (/ (atan b) (+ 1 c)))) (+ c (* 2 d))) To get to that target syntax, I might have my parser produce intermediate syntax as a kind of abstract syntax tree, such as: (block (assignment a (/ 1 (+ 1 7))) (assignment b (* 2 (+ a (sqrt a)))) (assignment c (/ (atan b) (+ 1 c))) (return (+ c (* 2 d)))) Which, in this case, is a simple translation of syntax objects to the nested `let`s or `let*` examples above. Of course, for a real-world DSL, you might have very different semantics than in the simple example, but the above translation method still likely applies. You might start your language implementation by typing examples in your language, then the equivalents in Racket, then the intermediate parse trees of your examples... and then Racket `syntax-parse` or `syntax-case` generalizations your parse tree syntax to the Racket syntax. The examples you made might then be test cases for your translation. And if your DSL is embeddable in Racket code, you probably don't need the parser at all -- you might just need a `define-syntax`, with `syntax-parse`, `syntax-case`, or arbitrary code to do the translation.
- tel 7y agoSure, agreed. "Fundamental technique" wouldn't imply "necessary technique" in my vernacular. The CPS mindset is sure down there in fundamentals town, though, with respect to thinking about evaluation, etc.
- tel 7y agoI do want to add that Lisp metaprogramming does one big trick for DSL design in that it works at the syntactic level and ignores binding. Thus all the tricks around hygenics and anaphoria etc. Since ignoring binding means that you only deal with the call graph implicitly through working with the AS tree, you get an opportunity to pareto your way through tougher DSL nuggets.
- TeMPOraL 7y agoWhat do you mean by "ignoring binding"? That doesn't sound right. Binding is one of the things I think about the most when building DSLs in Common Lisp.
- tel 7y agoYes, the user has to since the system ignores it. A lisp macro operates, generally, over the abstract syntax tree as opposed to the "abstract binding tree" where alpha equivalences have already been factored out. Or, concretely, a lisp macro distinguishes between syntax `(my-let (x 3) x)` and `(my-let (y 3) y)` even though they are, under implied semantics, identical. In that way, lisp macros have little to say about binding.
- TeMPOraL 7y agoI still don't understand; probably because I'm not proficient in the mathematical underpinning of this. The way I see it, a (Common) Lisp macro can, but does not have to, distinguish between the two forms you've shown. Assuming `my-let' is the macro, from it's POV, `x' is a value. A reasonable implementation of "let" won't care about that value beyond it being a symbol comparable to other symbols by `eq'. This way, `(my-let (x 3) x)' and `(my-let (y 3) y)' will expand to effectively the same code - and after the compiler gets rid of symbol names altogether, the resulting code will be actually the same.