greg
2019-3-25 21:24:50

At the “edge” of a program, “accepts whatever you pass it” can be useful — as a kind of input validation, especially from humans or from messy files or APIs. “Do what I mean”.


greg
2019-3-25 21:25:47

But I get uncomfortable when that permeates the whole program. I think it actually makes it harder to catch problems near where they originate.


greg
2019-3-25 21:26:45

So ideally I like to keep the DWIM part as a pretty thin outside edge/layer. And pretty quickly try to get very “brittle”, where contracts (or static types) are being super picky about what functions accept, and loudly complain about surprises.


greg
2019-3-25 21:27:35

But. That’s just my own guideline, for myself. It’s not always so simple or easy to structure things that way. ¯_(ツ)_/¯


leif
2019-3-25 22:33:40

Is there any way to write a macro that binds an identifier the first time its expanded, but on subsequent expansions (in the same scope) it doesn’t. That is, a macro, say (define-foo x body), that can expand like:


leif
2019-3-25 22:34:18
(let ()
  (define-foo x body))

=> (let () (define foo some-expr) (define x body))


leif
2019-3-25 22:35:24

BUt if we call it twice, foo only gets bound once. So:

(let ()
  (define-foo x body))
  (define-foo y body)))

=> (let () (define foo some-expr) (define x body) (define y body))


leif
2019-3-25 22:35:25

?


notjack
2019-3-25 22:38:00

@leif no idea, but very good question. I can think of tons of use cases for that.


lexi.lambda
2019-3-25 22:39:11

You could always do it with some mutable state.


notjack
2019-3-25 22:39:48

I wouldn’t trust an implementation that did though


lexi.lambda
2019-3-25 22:40:36

I dunno, definition contexts are imperative, anyway, and to quote Sam paraphrasing Ryan, “I can’t think of a better way”. :)


lexi.lambda
2019-3-25 22:41:25

You can use the value of syntax-local-context to check if you’ve already added a definition in the current definition context or not.


notjack
2019-3-25 22:52:34

maybe it could be done with an approach similar to racket/splicing, where an intermediate step performs local expansion?

(with-definition-merger (lambda (first-id second-id) ...)
  (define-foo x body)
  (define-foo y body))

lexi.lambda
2019-3-25 22:54:18

I really do think you can reliably put the value of (syntax-local-context) in a phase 1, mutable, eq?-based hash set and use that to check whether or not you’ve “seen” the current definition context or not.


lexi.lambda
2019-3-25 22:54:42

Any situation in which that isn’t true means someone else did something wrong.


notjack
2019-3-25 22:54:57

I don’t disagree with that, but I also think it’s a solution to a very narrow set of problems


lexi.lambda
2019-3-25 22:57:00

I don’t think I really understand. It’s the recommended solution to the very general problem of asking “have I seen this definition context before?”


notjack
2019-3-25 22:58:39

What I mean is that I don’t think asking that question is something macros should be doing, because it inserts temporal dependencies between them. I don’t want to have to debug that kind of code.


lexi.lambda
2019-3-25 23:01:10

Expansion fundamentally has an arrow of time, and @leif’s request depends upon it, so I don’t think you can get away from that entirely. Though of course you could wrap it up behind some abstraction if you wanted.


leif
2019-3-25 23:04:00

@lexi.lambda Ya, you’re probably right. Although I may be doing something wrong, because I keep getting what it believes is the same internal definition context: (define-for-syntax ctxs (mutable-set)) (define-syntax-parser define-foo [(_ x:id body) (set-add! ctxs (syntax-local-context)) (writeln ctxs) #'(define x body)]) (let () (define-foo x 42) (void)) (let() (define-foo x 48) (void))


lexi.lambda
2019-3-25 23:04:31

Make sure you’re using mutable-seteq, though that might not be what’s going wrong there.


leif
2019-3-25 23:04:52

Ah, okay, that helped.


notjack
2019-3-25 23:05:15

Both macros could emit the duplicate definition within some outer macro that locally expands its body and deduplicates definitions. Then there’s no dependency on the order that individual body transformers are called.


leif
2019-3-25 23:05:15

Still, odd that internal-definition-context? is returning #f for (syntax-local-context)


lexi.lambda
2019-3-25 23:05:16

Also, if you’re in an internal definition context, (syntax-local-context) is a list. You probably want to get the first element out of the list and use that.


lexi.lambda
2019-3-25 23:05:55

The value inside the list returned from (syntax-local-context) can be anything. It’s only guaranteed to be different with respect to eq? in different contexts.


leif
2019-3-25 23:05:59

Oh yes, it is a list,with a liberal-definition-context in it.


leif
2019-3-25 23:06:18

Doesn’t the docs say its one of several symbols or a list?


leif
2019-3-25 23:06:26

AH, okay


lexi.lambda
2019-3-25 23:06:29

Yes, it is a list when you’re in an internal definition context.


lexi.lambda
2019-3-25 23:06:38

You probably want to allow 'module or a list, and nothing else.


lexi.lambda
2019-3-25 23:06:52

Well, maybe also 'top-level, if you want to support that.


leif
2019-3-25 23:07:19

THat would explain why I could never get internal-definition-context-binding-identifiers to work.


lexi.lambda
2019-3-25 23:08:17

You can put 'module directly in your mutable set without worrying about it, since there can only be expansion in one module per visit to your module. So you’ll never end up with 'module being returned for two different modules before your mutable set is emptied.


lexi.lambda
2019-3-25 23:13:07

So, to summarize, you probably want something like this: (define-syntax-parser define-foo [(_ x:id body) #:do [(define ctx (syntax-local-context))] #:fail-unless (or (memq ctx '(module top-level)) (pair? ctx)) "only allowed in a definition context" #:do [(define ctx-v (if (pair? ctx) (car ctx) ctx)) (define seen? (set-member? ctxs ctx)) (set-add! ctxs ctx)] #\| do something with seen? \|#])


leif
2019-3-25 23:14:17

Interesting. Is there any reason for the last #:do other than style?


lexi.lambda
2019-3-25 23:14:35

No.


leif
2019-3-25 23:14:36

(As opposed to just putting it in the body.)


leif
2019-3-25 23:14:42

Okay, so just style?


lexi.lambda
2019-3-25 23:14:48

Yes.


leif
2019-3-25 23:14:53

Cool.


leif
2019-3-25 23:14:56

Also thx.


leif
2019-3-25 23:15:56

oh, one more thing, why did you get the first element of the list if its an internal definition context?


leif
2019-3-25 23:16:13

Since the list itself is eq? if the context is eq?


leif
2019-3-25 23:16:48

Oh…durp….the docs


leif
2019-3-25 23:17:06

". The identity of the list’s first element (i.e., its eq?ness) reflects the identity of the internal-definition context"


leif
2019-3-25 23:17:08

thanks. :slightly_smiling_face:


lexi.lambda
2019-3-25 23:20:23

Yes, I’ll be honest, I am not certain exactly when (syntax-local-context) would be a list of more than one element.


leif
2019-3-25 23:20:43

Honestly, you’re guess is as good as mine.


leif
2019-3-25 23:20:57

If anyone knows, its one of @mflatt, @ryanc, or @michael.ballantyne


lexi.lambda
2019-3-25 23:21:30

The local-expand documentation states: > If the internal-definition context is meant to be self-contained, the list for context-v should contain only the generated value; if the internal-definition context is meant to splice into an immediately enclosing context, then when syntax-local-context produces a list, cons the generated value onto that list. However, I do not know when an internal definition context would splice into an enclosing one in a way that would still be a distinct definition context.


lexi.lambda
2019-3-25 23:21:49

When begin splices into an enclosing context, expansion of the forms in the begin are still in the same context as the enclosing one.


bkovitz
2019-3-25 23:26:48

@greg Thanks. I appreciate the voice of experience about this. Lately I have indeed been going wild with DWIM arguments, deep in internal function calls. Hopefully I won’t get burned! When I started with Racket, I kept getting burned by forgetting whether something was a list or a set (also in Clojure). If I changed which one a function accepted or returned, it was hard to track down what other code needed to be changed. How do people deal with that under dynamic typing?

Many little things are error-prone in that way, e.g. “pass the struct” vs. “pass the thing inside the struct”. So far, it’s been working out: the calling code is now simpler (e.g. no more set->list or list->set wrapping arguments to function calls) and more readable. But I’ll watch out. Do you know of something bad that results from functions that take whichever representation they’re given?


bkovitz
2019-3-25 23:36:49

@ryanc I put in the closure that accepts the class name in the “deep” syntax class. It worked easily, and even let me simplify some code by making syntax classes no longer return so many attributes. Thanks again!


michael.ballantyne
2019-3-25 23:50:27

I think I saw this in the implementation of splicing-local the other day.


michael.ballantyne
2019-3-25 23:51:32

It’s an alternative implementation of a splicing definition context that uses local expand rather than trampolining in the context to be spliced into.


greg
2019-3-26 00:03:58

So, I mean, the usual caveat applies: It depends. :smile: Early on, as I explore something new, I define some functions — without contracts. I have “types” in my head, loosely. I try calling these functions from the REPL, supplying various examples, see if they behave as I expect. As things “settle down”, I might copy some of those REPL example calls inside check-equal? forms in (module+ test ___) submodule, as tests. If the function signatures seem “obvious”, I might stop there, for awhile. Or, I might also add contracts. But I might wait until things gel, and I feel confident about the decisions. (I think “premature ceremonialization” is bad for similar reasons as premature optimization.)


greg
2019-3-26 00:07:51

> Do you know of something bad that results from functions that take whichever representation they’re given? There’s some (Alan Perlis??) quote: “If you have a function with 10 arguments, you probably missed some.” I feel like a variation of that is, “If you have a function argument that can be any of 10 different things, you probably missed some.” I just feel like it’s hard enough to make code really correct under all edge cases. So I don’t want to add even more edge cases. Also if a function expects an integer, but the caller gives it a string — the caller is probably confused about something. The sooner the callee complains, and the caller can be fixed, the easier it is. (whereas if it blows up 5 or 10 function calls away from that original confused function, it’s going to be harder to fix. Especially if you don’t notice the bug until long after the bad thing was written.)


greg
2019-3-26 00:09:23

So again, some early version of a function, maybe I hedge my bets, don’t commit to firmly to what the acceptable values are. That’s part of exploring the problem space. But I like to tighten it up as I go along. ¯_(ツ)_/¯


greg
2019-3-26 00:11:34

Also, @mbutterick described how, over time, you might break a big bunch of code into smaller modules, maybe even publish them as their own packages. Certainly by that time, it’s nice to firm things up. “This function does this, here’s what it expects from you, here’s what you can expect from it, yada yada.”


greg
2019-3-26 00:11:52

</wall-of-text>


alexknauth
2019-3-26 04:31:28

One problem with a type like DynamicAny is that its contract would be flat, but for type soundness we need make-predicate on it to fail. We cannot allow a typed function to produce true with the proposition #:+ DynamicAny on it. The contract should only be used for either cast, require/typed, or provide, and never for make-predicate. Is there a way to specify that restriction in Typed Racket’s framework?


alexknauth
2019-3-26 04:42:42

A predicate of type (-> Any Boolean : #:+ DynamicAny) returning true would be unsound, but a predicate of type (-> DynamicAny Boolean : #:+ DynamicAny) would be fine.