[racket-dev] Code micro-level organization

From: Eli Barzilay (eli at barzilay.org)
Date: Fri Jun 1 13:48:54 EDT 2012

[Combined reply]

Two days ago, Jon Rafkind wrote:
> On 05/30/2012 04:07 PM, Eli Barzilay wrote:
> >
> >> Having expressions come from the bottom, using the down arrow, seems
> >> sort of wierd.
> > Here's a concrete example:
> >
> >   (○ (let ([x 10]) ↓)
> >      (for ([i (in-range x)]) ↓)
> >      (for ([j (in-range i)]) ↓)
> >      ...etc...)
> >
> > Do you have a concrete suggestion for doing that?
> Well in this case its easy, just remove the closing ) from each of
> the lines so the next line is nested in the right place.

Yes, but then you get the drift which is one of the things that it's
trying to eliminate.  (It's true that strictly speaking you don't need
that -- but the premise of this whole thing is a tool for structuring
code for easier reading & writing, like many other unnecessary tools
(which are most of the language, of course).)

> If you were going to use the down arrow in a different position,
> like
> (for ([i ↓]) ..blah..)
> (let ...)
> I think things would get out of hand quickly because the physical
> gap between the for expression and the let expression could get
> quite large.

The use case for these things would be to simplify the code in a way
that puts each "point of focus" on consecutive lines.  To demonstrate
with just the above, you *could* do something like this:

  ;; print a table of square roots
  (○ (for ([i ↓]) (printf "~s² = ~s\n" (* i i)))
     (in-range 0 100))

But that doesn't make much sense, since using the range expression
inside the for loop is more readable.  This is not different from this
weird way to write this code:

  (let ([f (λ (x) (for ([i ↓]) (printf "~s² = ~s\n" (* i i))))])
    (f (in-range 0 100)))

Cases where you would use that is when that expression is that main
focus of the code, for example

  (○ (for/list ([i ↓]) (* i i))
     (in-sequences ↓ (in-range 10))
     (stop-after ↓ zero?)
     (in-naturals ↓)
     (fib 10))

> I mean I hope I'm not trivializing your issue, do you have a
> different example?

Any example where that main focus is not the last expression, like the
above.  Here's a different one:

  (○ (for ([i 10]) ↓ (newline))
     (for ([j 10]) ↓)
     (printf " ~s" (* i j)))

Yesterday, Neil Toronto wrote:
> It seems like `↑' is another way to not name expressions (i.e. a
> "pointless style" :D),

(I can't parse that.)

> and `↓' is handled just fine by internal definitions. This is
> equivalent, currently defined, has less nesting, and avoids a
> rename:
>     (define orig-str "foo bar baz")
>     (define sub (substring orig-str 3 8))
>     (define str (string-trim sub))
>     (define m (regexp-match? #rx"^[a-z].*[a-z]$" str))
>     (and m (string-append "*" str "*"))

Yes, I mentioned this hack explicitly.  Maybe it will help if I
clarify why it's a hack: it forces you to choose names where you don't
need any.  For example, see how the names you chose are bad?  -- `str'
doesn't make sense in such a line, so it would become something like
`trimmed-string', and `m' is a buggy convention since it's usually
used with the results of matching so it should be something like
`is-word?'.  And now names are repeating information that is already
clearly visible in the code, which makes me dislike it even more than
the hugarian naming convention.

Another way to deal with this is with a generic base for the name
(which is what I remember Jay posting at some point), as in

  (define x1 ...)
  (define x2 ...)

This is also a pretty bad way to do that, since the names are
meaningless labels so the machine should be dealing with them, not me.
(Note in particular how adding new intermediate expressions requires
shifting names -- in a way that can eventually lead to a convention of
`x10', `x20', etc so it's easier to debug...)

But a more high-level point is that I *am* already using a highlevel
language where names are not always necessary, and I'd like to keep
that freedom without sacrificing code readability.  The argument that
is usually made for your example is "I find that being forced to use
descriptive names help" -- that can obviously be true in some cases,
but I dislike requiring it.  A technical problem with this argument is
that it omits the fact that you really need *unique* names; the
high-level problem with the argument is that it's *exactly* the same
argument that people have been using for ages as an excuse to not
having some kind of `lambda' in their languages.

> A `define*' form like Jay (and I) want would make these kinds of
> things less error-prone and allow names to be reused:
> [...]

Right, and it looks like there's strong opposition to that, which is
why I'm trying to find something that can be done.  What I'm
suggesting is not a complete replacement -- one thing it doesn't do is
use different names for farther references (and that's why I asked for
examples), and another difference is that I'm talking about a macro so
you won't be able to use more than one `↑' (or whatever is a good name
for it).  The idea is to make many cases that seem redundantly complex
easier to read/write, but of course not all.

Yesterday, Jay McCarthy wrote:
> I was clapping through the majority of your email.
> I want define* so bad.

Me too, but the way it looks, that's not going to happen -- at least
not with some different `begin'-like wrapper around it...

> 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.

Note the above -- I'm talking about a syntactic transformation.  It
would be nice to have something that a binding and that can refer to
other things, but I don't see any way to do that and keep it very
simple -- which I think is a major point of this exercise.
(Otherwise, you can just go back to existing constructs like `let*'

Yesterday, Ryan Culpepper wrote:
> I don't think I like the idea of making the internal definition
> contexts of racket/base forms act like package-begin, though.

To repeat something that I said when we talked about this: right now
things are particularly bad with `define' being encouraged, and the
ability to make subtle scoping mistakes (subtle because you usually
end up with an #<undefined> value somewhere).  When I convert code I
usually repeat the work twice and compare the two versions to avoid
such problems, but it's easy to still have problems slip through.

But the thing that makes it really bad is that you can easily get
these problems not only when you convert code -- it's easy to run into
them when writing new code.

Yesterday, Jay McCarthy wrote:
> I know about package-begin, it's just not worth it if I need to bring
> in another require and add package-begin


          ((lambda (x) (x x)) (lambda (x) (x x)))          Eli Barzilay:
                    http://barzilay.org/                   Maze is Life!

Posted on the dev mailing list.