[racket-dev] Code micro-level organization
On 05/31/2012 02:54 PM, Jay McCarthy wrote:
> I was clapping through the majority of your email.
>
> I want define* so bad.
You can use define*; just put it inside of package-begin:
> (require racket/package)
> (package-begin
(define* x 1)
(define* x (+ 2 x))
x)
3
I don't think I like the idea of making the internal definition contexts
of racket/base forms act like package-begin, though.
Ryan
> I use compose and curry a lot (even though I know their performance
> problems) because it don't have to name things.
>
> I like the idea of the -> thing with the down and up arrows. I see a
> value in both arrows. I also like Jon's suggestion of a 'last' id...
> although I'd also want<0> through<n> or something to refer to so
> many spots back.
>
> Jay
>
> On Wed, May 30, 2012 at 3:40 PM, Eli Barzilay<eli at barzilay.org> wrote:
>> I'm going to ramble a bit about organizing code, trying to look for an
>> idea for a good solution -- so spread a few kgs of salt over the
>> following (if you care to read it).
>>
>> The problem that I'm talking about has several manifestations. The
>> most obvious one is code-drift towards the RHS. A less obvious
>> problem is how it's sometimes hard to read code. To use a cooked up
>> example:
>>
>> (let ([str (string-trim (substring "foo bar baz" 3 8))])
>> (and (regexp-match? #rx"^[a-z].*[a-z]$" str)
>> (string-append "*" str "*")))
>>
>> to read this, you start from the string literal, then read the
>> `substring' expression, then `string-trim', then the `let' binding,
>> then the `and' and finally the `string-append'[*]. To relate this to the
>> above: besides the right-drift (which is of course very minor here),
>> it takes time to "internalize" the rules of the language that leads to
>> this, which is a problem for people new to functional programming with
>> it's heavy use of nested function calls. More than that, I think that
>> it's also a problem for *experienced* hackers too -- to see what I
>> mean, open up any random piece of code that deals with an area you're
>> not familiar with, and try to read through it. Personally, I often
>> find myself in such situations "reading" the actual ordering as I go
>> through the code, and that's fragile since I need to keep mental
>> fingers at various locations in the code in question, sometimes even
>> using my real fingers...
>>
>> You'd probably recognize that there's a whole bunch of tools that are
>> trying to make things better. A few random ones that I can think of
>> are:
>>
>> * The new semantics& blessing for using `define' forms instead of
>> `let' etc makes code easier to read and avoids some right-drift.
>>
>> * There's the need (which I recently talked to at NEU) for some kind
>> of a `define*' form that can be used as a definition with a `let*'
>> scope. For those who weren't there, the summary of the issue is
>> something that Jay once said -- that he sometimes uses
>> (define x0 ...)
>> (define x1 (... x0 ...))
>> (define x2 (... x1 ...))
>> because he wants to avoid a `let*'.
>>
>> * The old `scheme/nest' is a direct attempt to prevent drift for
>> some kinds of nestings.
>>
>> * There's the related suggestion for extending the reader with
>> something like `$' or `//' that closes the rest of the sexpr in
>> its own set of parens.
>>
>> * Every once in a while there's a suggestion to invert conversion
>> functions, eg, turn `string->number' into `number<-string' so it
>> reads out better. In a similar direction, there are sometimes
>> suggestions to use `compose' to make things more readable, as in
>> ((compose f1 f2 f3 f4) x)
>> vs
>> (f1 (f2 (f3 (f4 x))))
>> and the textual mess that the latter tends to end up as with real
>> names.
>>
>> * srfi-2 defines an `and-let*' which is addressing a common pattern
>> of interleaving nested `let's and `and's. Actually, `cond' itself
>> is addressing this kind of problem too, so add here various
>> suggestions for extending `cond' with binders, anaphoric forms
>> etc.
>>
>> * Recently, I looked at some clojure pages (to hunt for new
>> extensions to `racket/list'), and I saw that they have a
>> "threading form" using `->' that expresses nested function calls.
>> See this here:
>> http://clojuredocs.org/clojure_core/clojure.core/-%3E
>> and note also the other three variants, `->>' `-?>' and `-?>>',
>>
>> * (The list goes on...)
>>
>> (One common theme in all of these is that they're tools that none of
>> them are tools that are needed -- they're all just ways to make code
>> look better.)
>>
>> I actually started thinking about this when I saw the clojure thing.
>> The first thing that is limited about it is that it has four forms,
>> where the reason for the `->' vs `->>' split is to put the nesting in
>> a different argument position. To summarize (and IIUC):
>>
>> (-> x
>> (foo 1 2)
>> (bar y))
>>
>> expands to
>>
>> (bar (foo x 1 2) y)
>>
>> whereas using a `->>' would make it expand to
>>
>> (bar y (foo 1 2 x))
>>
>> Not only does it seem to me bad to have two bindings for this, we also
>> have the usual problem of the order-defying `regexp-replace' where
>> usually the action happens in the *middle* argument... (Which is how
>> it ends up being a common example in showing these problems, as
>> happened recently.)
>>
>> In any case, this looks like an easy thing to fix by adding an
>> explicit marker to the point where the nesting happens. For example,
>> imagine a form that looks like this:
>>
>> (○ x
>> (foo 1<> 2)
>> (bar y<>))
>>
>> that expands to (bar y (foo 1 x 2)). (The reason that clojure has two
>> other forms (`-?>' and `-?>>') is something that is related to the
>> below, so I'll skip it for now.)
>>
>> The next thing that I tried is to contrast this with `nest'. The
>> difference between them is that while both lead to a simpler syntax
>> for nested expressions, they do the nesting in different directions,
>> where (*very* roughly speaking) `->' nests things downwards and `nest'
>> nests them upwards:
>>
>> (-> X Y) nests X into Y
>> (nest X Y) nests Y into X
>>
>> or more generally:
>>
>> (-> X Y0 Y ...) nests X into Y0 and nests the results with Y ...
>> (nest X Y ...) nests the result of nesting Y ... into X
>>
>> So I tried to see if I can come up with something that can kill both
>> birds -- which is why I started with the above example:
>>
>> (let ([str (string-trim (substring "foo bar baz" 3 8))])
>> (and (regexp-match? #rx"^[a-z].*[a-z]$" str)
>> (string-append "*" str "*")))
>>
>> Now, lets imagine that instead of a simple `<>' hole, there are two
>> kinds of holes with an "up" or a "down" direction -- this leads to
>> this kind of a syntax:
>>
>> (○ "foo bar baz"
>> (substring ↑ 3 8)
>> (string-trim ↑)
>> (let ([str ↑]) ↓)
>> (and (regexp-match? #rx"^[a-z].*[a-z]$" str) ↓)
>> (string-append "*" str "*"))
>>
>> where you can read `↑' as "the above" and `↓' as "the below". The
>> thing that makes me excited about this is how you can read this as the
>> above [*] reading.
>>
>> There are still some problems with this though. One problem is that
>> it can be ambiguous -- for example, I had this as one experiement:
>>
>> (○ (let ([str "foo bar baz"]) ↓)
>> (substring str 3 8)
>> (string-trim ↑)
>> (string-append "*" ↑ "*"))
>>
>> where the upward nesting could happen first -- this ambiguity is easy
>> to resolve if there's a simple rule for merging the first two
>> expressions repeatedly, stopping with an error if there's not exactly
>> one down arrow in the first or one up arrow in the second; and
>> finishing when there's one expression (throwing an error if it still
>> has arrows). Using this, the expansion of the above goes with these
>> steps:
>>
>> ... ->
>> (○ (let ([str "foo bar baz"]) (substring str 3 8))
>> (string-trim ↑)
>> (string-append "*" ↑ "*"))
>> ->
>> (○ (string-trim (let ([str "foo bar baz"]) (substring str 3 8)))
>> (string-append "*" ↑ "*"))
>> ->
>> (○ (string-append "*" (string-trim (let ([str "foo bar baz"]) (substring str 3 8))) "*"))
>> ->
>> (string-trim (let ([str "foo bar baz"]) (substring str 3 8)))
>>
>> It's also unclear if this is generic enough though. I vaguely suspect
>> that there might be cases where you want arrows from multiple places
>> in the form which makes this a kind of a literate-programming-like
>> tool for micro-level code organization (and yes, I intensely dislike
>> LP, so that's would be a bad thing). In addition, something like this
>> should really have simple rules for how it works, otherwise it not
>> something that anyone would want to use or read.
>>
>> BTW, I take the `nest' experiment as an example: the form itself is,
>> IMO, perfectly fine, but it suffered from having too much parentheses,
>> which makes it hard to use. One thing I like in the above is that the
>> explicit arrow markers make it much easier to read -- I think that
>> this is also an advantage over the clojure threading forms, where you
>> see a form like (take 10) and you have to look back at the arrow kind
>> that was used to know what this really is.
>>
>> In any case, any thoughts about this? I'd especially appreciate
>> little code layout horrors you might encounter, to see how such a form
>> can deal with them. Feel free to reply off-list to avoid premature
>> bike-shedding. (I'm *not* going to commit anything -- this is just
>> trying to roll around the idea to see if there's any point in doing
>> something like this. *If* there is enough interest, then I'll post a
>> concrete suggestion when I have one.)
>>
>> --
>> ((lambda (x) (x x)) (lambda (x) (x x))) Eli Barzilay:
>> http://barzilay.org/ Maze is Life!
>>
>> _________________________
>> Racket Developers list:
>> http://lists.racket-lang.org/dev
>
>
>