Past tense of alloc

April 14th, 2010

Suppose you do this:

    [NSString alloc]

Did you alloc an NSString? Yes, you did. You “alloced” the string? Or is it that you “allocked” the string?

I think allocked is the past tense form of alloc. Otherwise, you’d be pronouncing the word “allo-sed,” which would be dumb.

Also, mallocked and callocked and reallocked.

Tribes – Legacy

April 13th, 2010

EYE

April 13th, 2010

It’s really annoying on the internet when you want to be a flagrantly egotistical person and talk about YOUR opinion and what YOU think about things. And the reason that is so is that you have to talk about not what “YOU” think about things, but what “I” think about things.

And since “I” is already capitalized, you can’t shout it. You have to emphasize it through other means. Italicizing it doesn’t really help, since it’s a single letter, and embolding it doesn’t really work either, it just makes it look like some kind of monolith from Space Odyssey 2000. You can’t give it external underscores or asterisks, as in _I_ or *I*, because the former looks like a perpendicular or “bottom” symbol and the latter looks like some kind of mechanical diagram with gears and such. You can’t use /I/ either, because that’s sort of like a l33t-speek N.

So, what do you do? What do you do?

Perhaps you should say “EYE” to shout the word “I”. Well EYE think that’s a horrible idea. Aye. Maybe we should adopt the practice of using lowercase “eye” in place of “I” or “i” when chatting in IRC. When people ask why, you could point them to this blog post. Make a link.

A brief excursion

April 10th, 2010

I hate to pre-announce what I’m doing, but in the spirit of regular blogging, I’ll tell you that right now I’m working on a free Hacker News iPhone app. There are two non-free apps that cost $2.99. So instead, I’m making a free one. And maybe it will be open source.

This somewhat ties into the other project I’m working on. It’s sort of a practice project.

in-labeled-groups

April 8th, 2010

So I found myself needing to group a sequence by two different ways of grouping it without iterating through it multiple times.

;; (in-labeled-groups `((a . ,f) (b . ,g)) seq) returns a sequence that's the
;; interleaving of (in-map (curry cons 'a) (in-groups f seq)) and (in-map
;; (curry cons 'b) (in-groups g seq)), except that seq is only consumed once.
;; (Mapping and grouping separately would try to consume the sequence twice,
;; which doesn't actually work.)  This can be generalized to any number of
;; labeled predicates, of course.
;;
;; (sequence->list (in-labeled-groups `((eq . ,equal?) (lte . ,<=)) '(1 2 2 2 3 2 2 3 4 5)))
;; ((eq 1)
;;  (eq 2 2 2)
;;  (eq 3)
;;  (lte 1 2 2 2 3)
;;  (eq 2 2)
;;  (eq 3)
;;  (eq 4)
;;  (eq 5)
;;  (lte 2 2 3 4 5))
(define (in-labeled-groups binary-pred-assocs seq)
  (in-once (let-values ([(has-next? next!) (sequence-generate seq)])
             (when (has-next?)
               (let loop ([current-lists-reversed (let ([x (next!)])
                                                    (make-list (length binary-pred-assocs) (list x)))])
                 (if (has-next?)
                     (let ([next (next!)])
                       (loop (for/list ([reversed-group current-lists-reversed]
                                        [labeled-binary-pred binary-pred-assocs])
                               (if ((cdr labeled-binary-pred) (car reversed-group) next)
                                   (cons next reversed-group)
                                   (begin
                                     (yield (cons (car labeled-binary-pred) (reverse reversed-group)))
                                     (list next))))))
                     (for ([reversed-group current-lists-reversed]
                           [labeled-binary-pred binary-pred-assocs])
                       (yield (cons (car labeled-binary-pred) (reverse reversed-group))))))))))

So that was very ugly.

One nice thing about this function is that it’s an example of one where I’d feel pretty uncomfortable writing it in Haskell. What would its type signature be? How convenient would it be to use? This implementation here scales well with respect to the number of ways in which I’m grouping the sequence, since it’s just another label.

One question is: how am I going to like splitting the sequence of groups up later? I’m starting to feel the call of reactive programming. How good is PLT Scheme at having lots of tiny threads? I might need to look into that. I’m having one of those moments where you recognize threads as a tool for making cleaner code, instead of as a tool for doing two things at the same time. But this is a moment of insanity.

Holy Moley There’s So Much Stuff To Learn

April 7th, 2010

Every once in a while, I realize that there’s so much stuff I need to learn.  I need to learn how to generate code for the x86 instruction set.  How to make music on the computer.  How to do computational geometry.  Materials science.  Quantum physics.  How can I not know quantum physics?  Honestly, I barely even know multivariable calculus.  And I’m not comfortable with tensors.  I never have been!

I’m terrible at learning, but I’m good at remembering.

There’s just so much stuff I haven’t learned.  The only math I know is the kind useful for high school math contests.  I really know my 30-60-90 triangles and will never forget them.  I’m good at thinking carefully under brief spurts of high pressure not lasting more than 3 hours.  However, I’ve lately been developing into a panic because of how little I know, and how little I’ve learned, despite meaning to learn it for several years.

When I look at my interests, there is the difference between what I want my interests to be versus what they are.  In my 6th semester of college, I took a lighter courseload so that I could spend more time on personal projects.  And what did I do?  I muddled around with Haskell.  But what I wanted to do was to muddle around with math and compilers.  But I didn’t!  I muddled around with type systems.  Blah!

I barely know how to write software.  Oh hey, I want to make a software project…. and how do I do that?  I worked for 1.5 years as a programmer and I’m still fuddling along, going at a snail’s pace.  Why don’t I have the energy for this?

Do I need more exercise?  I’m going to go out and exercise right now.  I’ll be right back.  And then we’ll see how my attitude has changed.

Well!  It’s amazing how far you can run when you haven’t run for the past week and a half and you have all that stamina built up.  It’s amazing how much heavier you feel when you’ve been eating a fair bit in that period of time.  Well I’m going to take a quick shower.  And then we’ll see how my attitude has changed.

What I’ve found is that when exercising, particularly when running, that after exercising, my thoughts tend to get scattered and that they tend to go testing themselves in all sorts of different tangents.  It’s as if all sorts of different whimsical departments of the brain have all gotten reactivated and decided to call up the center of consciousness and decided to tell it what it might want to think about.  And then the center of consciousness has to cut them off and listen to other upshoots of ideas, and then cut them off.

While running, I started to think about what causes me to have opinions about things and what causes certain opinions to be written down.  You see, back when I was a teenager (I’m 23 now), I used to freely publish my opinions online.  But then, as I turned 17 or 18, I started to notice that I wasn’t a fan of my previous opinions, because my opinions had changed.  They had gotten older and more mature.  And so I got a sort of Vulcan-like persona online.  I became all logical and restricted my thoughts to matters of fact.  You see, I couldn’t leak dumb thoughts online that my older and more mature self would feel embarrassed about, when it remembers their leakage in the future.  But then, later, I felt embarrassed about how I had somewhat bottled my online self up in the past, and how this must have been a mark of immaturity, or something, and so I spent some time feeling that way.  And lately, with this blog, I’ve decided to become unbottled, and reveal my true opinions.

But what are my true opinions?  What does that even mean?  Presumably, it means that somewhere inside myself, there’s a person that has opinions, and that I just have suppressed them and have decided not to leak them.  And so my “outer” layer of consciousness has operated under the rules of, “take the inner consciousness’s opinions, and filter them.”  Lately, with this latest blog, though, have I simply let the inner consciousness’s opinions out, or have I encouraged the inner consciousness to develop ridiculous opinions and let those out?  Or has my outer consciousness decided to embellish the inner consciousness’s opinions and satirize them to an absurd extent, and write those down?

I’ve recognized, in the act of writing this blog, that to some extent, this blog has been a satire of why my true opinions are.  (And there we are back again to that central question: what are my “true” opinions?  When I’ve just drank a lot of soda, I resolve to drink only tea.  When I have refrained from soda for a while, I feel that one or two or six sodas would be a tolerable indulgence.  What is my true opinion?)  That is, I recognized, as I wrote the opinions down, that they are to some extent a satire of what my true opinions are.  Or are they a satire of what I imagined my true opinions to be?

The trouble with having opinions and memories of opinions is that you can never really tell what the true cause of your opinions are.  Often, when you come up with opinions or reasons for your opinions, you’re just telling yourself rationalizations for why you have certain opinions.  Or at least I think that’s the case for you — I know it’s the case for me, and since you seem to be human, I’m assuming you’re similar to me.  For example, when deciding not to go to grad school, I came up with all sorts of rationalizations.  And I don’t know which are the real ones.  If any of them are real.

So, when talking about the notion of “true opinion,” I mentioned that there might be some “inner consciousness” that had opinions on things.  And I mentioned that when cooling down after exercise, all sorts of departments in the brain would chime in to the center of consciousness about things they’d like to blog about.

What I’d like to know is: how do the different departments of the brain know that it’s time to think about certain things?  Like what to blog about?  It seems to me like the center of consciousness sort of broadcasts to other parts of the brain “Hey, I’m thinking about this, would you please send me ideas if you have them?”  And then the other parts respond.  Some parts are particularly specialized.  Reading, writing, face recognition, are fairly well-hardwired.  Back in high school, when I was spending a lot of time with RPN calculators, I got really good at mentally simplifying complex expressions, because I would just keep a stack of numbers in my head and pipe the argument through some parellized RPN version of the expression I was evaluating, or some near-RPN version that was more like a direct expression tree computed in parallel.  I have definite memories of deciding to compute x*(y+z) into x*y + x*z in parallel, rather than sequentially.  So anyway my point is that the brain obviously does stuff in parallel, and we can learn to do multiple things at the same time, like piping numbers through equations while listening to and parsing the words of a person speaking and pushing a buzzer.

And it seems to me like that if there’s an “inner consciousness,” located somewhere in my brain, that has its own opinions of things that my “outer consciousness” filters or exaggerates or whatever, then there’s a separate center of consciousness operating there.  And so there’s a person there — a mind! — that would like to do and say things and that always gets frustrated because its opinions and decisions never get acted upon.

And I think I’m that mind, and that there’s some outer center of consciousness that filters things.  And I think the outer center of consciousness, and me, the inner center of consciousness, both can manipulate memory.  And so the inner and outer centers of consciousness, looking back on their own memories, think that it’s their decisions that caused them to do the things they did.  And so they have no direct evidence of one another.  So there’s no way to tell whether your mind is the inner or outer center of consciousness.  They are only slightly out of synchronization.  So my decision that “I’m the inner center of consciousness” is a complete contrivance of opinion, and isn’t based on any observation.  And of course, the outer center of consciousness has let this opinion through, and so apparently it thinks it’s the inner center of consciousness.  Or maybe the outer consciousness decided that it would add this sentence about a complete contrivance of opinion.  Or maybe the inner center of consciousness managed to flip the controls and outmuster the outer.  I of course can only remember having thought of writing that, since my memories are mixed in with the other center of consciousness.

And of course, why are there only two?  If anything, there should be as many centers of consciousness as there are points in space.  Each of them is a product of an infinitesimal neighborhood of the brain receiving and sending information.  Some are smarter or duller than others, and all of them have a separate center of consciousness, or “mind,” arising out of them.  And so there are minds formed everywhere in the brain that all have opinions on things, and some of them are minds formed out of parts of the brain that get to control things, and maybe others are minds formed out of parts of the brain that are dead ends.  That are mostly overlooked.  That don’t really matter.  And so you get minds that have opinions on what to do but then immediately forget that they had such an opinion (since new information is coming in that didn’t depend on the production of that opinion) and then have new irrelevant opinions.

So there’s a good chance that my mind is completely irrelevant, and that there’s a mind a micron away that is really the master of everything.   But what about these memories?  If the same memories of thought are getting pumped through every part of the brain, it seems like every center of consciousness would end up being more or less equals.  Since they’re all mixed together, they’re virtually identical.  Of course, many parts of the brain do specialized things.  So it seems like they would have separate, duller centers of consciousness.  Somewhere there’s a 3D imagination center of the brain that imagines 3D things and calculates accelerations and frisbee trajectories.  It has a jolly good time, when it gets some use, and doesn’t concern itself with worldly problems.

Well, I hope you enjoyed this silly blog post.  It’s stuff like this that makes me a panprotoexperientialist.

in-once

April 7th, 2010

In a previous post I was complaining about PLT Scheme’s sequences.  Specifically, in-generator makes a sequence that destroys itself on the first consumption, and then silently does nothing.

I like sequences that destroy themselves on the first consumption, because it forces you to pipe things through efficiently.  And so I’ve replaced all uses of in-generator with in-once, as defined below:

#lang scheme

;; Provides in-once (and once-producer) which is like in-generator and
;; in-producer except that instead of silently not working, it raises
;; an exception should you decide that you want to iterate through the
;; sequence more than one time.  You may only iterate through the
;; sequence exactly one time.  I recommend using this if you want to
;; use "one-time" sequences.

(require scheme/generator)
(provide in-once once-producer)
(provide (except-out (all-from-out scheme/generator)
                     in-generator))

(define stop-value (gensym))

(define (once-producer producer stop . more)
  (let ([started-already #f]
        [sem (make-semaphore 1)])
    (make-do-sequence
     (lambda ()
       (if (call-with-semaphore sem
                                (lambda ()
                                  (begin0 started-already
                                          (set! started-already #t))))
           (error "Already iterated through this producer")
           (begin (set! started-already #t)
                  (values (if (null? more)
                              (lambda (_) (producer))
                              (lambda (_) (apply producer more)))
                          void
                          (void)
                          void
                          (if (procedure? stop)
                              (if (equal? 1 (procedure-arity stop))
                                  (lambda (x) (not (stop x)))
                                  (lambda xs (not (apply stop xs))))
                              (lambda (x) (not (eq? x stop))))
                          void)))))))

(define-sequence-syntax in-once
  (syntax-rules ()
    [(_ body0 body ...)
     (once-producer (generator body0 body ... stop-value) stop-value)])
  (lambda (stx)
    (syntax-case stx ()
      [((id ...) (_ body0 body ...))
       #'[(id ...)
          (in-producer (generator body0 body ... stop-value) stop-value)]])))

I decided to make it thread-safe. Or at least I think it’s thread-safe. It’s moments like these where I miss Haskell.

As far as I can tell, for comprehensions don’t try to iterate through sequences twice, so I’ve decided to trust that. (With define-sequence-syntax, I don’t really know what I’m doing, I’m just following the pattern shown elsewhere.)

Oh, and I suppose I’m distributing the above code under the LGPL Version 2, since it’s almost directly lifted from PLT Scheme v4.2.4/collects/scheme/generate.ss

Yuck, inverse bbcode

April 6th, 2010

So we now have crappy HTML-to-bbcode parsing going. It’s not elegant, but that’s okay. I’ve sworn off elegance this year. We use a dynamically scoped parameter to keep track of whether we’re inside a <pre> block, and…

If the server sends us unexpected HTML, we say “unexpected.” We try not to do too much intelligent stuff here (such as caring about illegally nested tags like <b>bold <i>bold and italic</b> italic</i> and such) because we’ll want the client to be able to quote a post and make a reply — and we don’t want unnatural bbcode constructs, justas a matter of principle.  Anyway the client will have to keep track of all this stuff anyway, so we might as well keep the server as dumb as possible.

I made a little macro for convenientizing the syntax, but I’m uncomfortable with macros, so it’s not maximally convenient.

We once had different struct types for each possible kind of bbcode tag, but that turned out to be a bit too heavyweight, so now we just use vectors.

(define currently-inside-preformatted (make-parameter #f))

(define (strip-text text)
  (if (currently-inside-preformatted)
      text
      (regexp-replace* #px"\\s+" text " ")))

(define-syntax-rule (p-pair pattern clause) (cons (list . pattern) (match-lambda clause)))

(define parse-handlers
  (list (p-pair [is-text] [(list text)
                           (strip-text text)])
        (p-pair [(is-tag #"br")] ['() #(break)])
        ;; quote
        (p-pair [(both (is-tag #"div")
                       (has-class "bbc-block"))
                 (is-tag #"h4")
                 (is-text-match #px"^(?:quote:|(.+)\\s+posted:)$")
                 (is-end-tag #"h4")
                 (is-tag #"blockquote")]
                [(list maybe-name) (make-bb 'quote maybe-name)])
        ;; end quote
        (p-pair [(is-end-tag #"blockquote")
                 (is-end-tag #"div")]
                ['() #(end quote)])
        ;; php block
        (p-pair [(both (is-tag #"div")
                       (has-class "bbc-block")
                       (has-class "php"))
                 (is-tag #"h5")
                 (is-text-match #px"php")
                 (is-end-tag #"h5")
                 (is-tag #"pre")
                 (is-tag #"code")]
                ['()
                 (currently-inside-preformatted #t)
                 #(php)])
        ;; code block
        (p-pair [(both (is-tag #"div")
                       (has-class "bbc-block")
                       (has-class "code"))
                 (is-tag #"h5")
                 (is-text-match #px"code")
                 (is-end-tag #"h5")
                 (is-tag #"pre")
                 (is-tag #"code")]
                ['()
                 (currently-inside-preformatted #t)
                 #(code)])
        ;; end php block / end code block
        (p-pair [(is-end-tag #"code")
                 (is-end-tag #"pre")
                 (is-end-tag #"div")]
                ['()
                 (currently-inside-preformatted #f)
                 #(end php-or-code)])
        ;; SA emoticons
        (p-pair [(both (is-tag #"img")
                       (has-attr-match #"src" emot-pattern)
                       (has-attr-match #"title" #px"^(.+)$"))]
                [(list url title) (make-bb 'emot url title)])
        ;; weird, an editedby marker
        (p-pair [(both (is-tag #"p")
                       (has-class "editedby"))
                 (is-tag #"span")
                 (is-text-match #px"^(.+) fucked around with this message at (.+) around (.+)$")
                 (is-end-tag #"span")]
                [(list editor-name date time)
                 (make-bb 'editedby editor-name date time)])
        ;; bold
        (p-pair [(is-tag #"b")] ['() #(b)])
        (p-pair [(is-end-tag #"b")] ['() #(end b)])
        ;; strikethrough
        (p-pair [(is-tag #"s")] ['() #(s)])
        (p-pair [(is-end-tag #"s")] ['() #(end s)])
        ;; spoiler
        (p-pair [(both (is-tag #"span")
                       (has-class "bbc-spoiler"))]
                ['() #(spoiler)])
        (p-pair [(is-end-tag #"span")] ['() #(end spoiler)])
        ;; underline
        (p-pair [(is-tag #"u")] ['() #(u)])
        (p-pair [(is-end-tag #"u")] ['() #(end u)])
        ;; italic
        (p-pair [(is-tag #"i")] ['() #(i)])
        (p-pair [(is-end-tag #"i")] ['() #(end i)])
        ;; super
        (p-pair [(is-tag #"sup")] ['() #(super)])
        (p-pair [(is-end-tag #"sup")] ['() #(end super)])
        ;; sub
        (p-pair [(is-tag #"sub")] ['() #(sub)])
        (p-pair [(is-end-tag #"sub")] ['() #(end sub)])
        ;; fixed
        (p-pair [(both (is-tag #"tt")
                       (has-class "bbc"))]
                ['() #(fixed)])
        (p-pair [(is-end-tag #"tt")] ['() #(end fixed)])

        ;; mailto.  order matters: we check for mailto before general urls.
        (p-pair [(both (is-tag #"a")
                       (has-attr-match #"href" #px"^mailto:(.+)$"))]
                [(list email-address) (make-bb 'mailto email-address)])
        ;; [url=blah]
        (p-pair [(both (is-tag #"a")
                       (has-attr-match #"href" #px"^(.+)$"))]
                [(list url) (make-bb 'url url)])
        ;; end of email / end of url
        (p-pair [(is-end-tag #"a")]
                ['() #(end mailto-or-url)])

        ;; bullet-list
        (p-pair [(is-tag #"ul")] ['() #(bullet-list)])
        (p-pair [(is-end-tag #"ul")] ['() #(end bullet-list)])

        ;; list item
        (p-pair [(is-tag #"li")] ['() #(list-item)])
        ))

(define (parse-post-body segments)
  (parameterize ([currently-inside-preformatted #f])
    (let loop ([ret '()]
               [segments segments])
      (if (null? segments)
          (reverse ret)
          (let hloop ([handlers parse-handlers])
            (if (null? handlers)
                (loop (cons 'unrecognized ret)
                      (cdr segments))
                (let* ([tail #f]
                       [report-tail (lambda (t) (set! tail t))]
                       [captures (matches-predicate-chain segments (caar handlers) report-tail)])
                  (if captures
                      (loop (cons ((cdar handlers) captures) ret)
                            tail)
                      (hloop (cdr handlers))))))))))

Character Entities

April 3rd, 2010

I finally got character entity conversion going. Now I have this ugly bastard mix of strings and bytestrings. The rule of thumb is that stuff that needs character entity conversion goes in strings.

There is now beautiful stuff like this:

(define entity-table
  '(("quot" . "\"")
    ("amp" . "&")
    ("apos" . "'")
    ("lt" . "<")     ("gt" . ">")
    ("nbsp" . "\u00A0")
    ("iexcl" . "¡")
    ("cent" . "¢")
    ...

and stuff like this…

(define entity-regexp
  (let ([unicode "#(?:[0-9]+|x[0-9A-Fa-f]+)"]
        [entity-name (string-append "(?:"
                                    (string-join (map car entity-table) "|")
                                    ")")])
    (pregexp (string-append "&(?:" unicode "|" entity-name ");"))))

;; Returns the replacement string for a character entity, or, if the entity is unrecognized, returns
;; the original entity string.  Entity strings are expected to be in correct syntax, but invalid
;; entity names are tolerated.  For example, (entity-replacement "&") => "&", and
;; (entity-replacement "&bogus;") => "&bogus;", but (entity-replacement "totallybogus") has
;; undefined behavior.
(define (entity-replacement entity-string)
  (when (not (and (char=? #\& (string-ref entity-string 0))
                  (char=? #\; (string-ref entity-string (- (string-length entity-string) 1)))))
    (error (format "invalid entity string: ~a" entity-string)))
  (if (char=? #\# (string-ref entity-string 1))
      (string (integer->char (string->number
                              (substring entity-string
                                         ;; This is a bit glib.  "#xABCD" is parsed correctly
                                         ;; string->number as hexidecimal, but with #1234 we want to
                                         ;; omit the octothorpe.
                                         (if (char=? #\x (string-ref entity-string 2))
                                             1
                                             2)
                                         (- (string-length entity-string) 1)))))
      (hash-ref entity-hash-table
                (substring entity-string 1 (- (string-length entity-string) 1))
                entity-string)))

That’s how it originally was. In entity-regexp it ensured the proper entity name. But it would be just as good to do this:

(define entity-regexp
  (let ([unicode "#(?:[0-9]+|x[0-9A-Fa-f]+)"]
        [entity-name "[A-Za-z0-9]+"])
    (pregexp (string-append "&(?:" unicode "|" entity-name ");"))))

since we already check for valid entity names in the lookup table. So I’m going to go with that.

More Monstrous HTML Grepping

April 2nd, 2010

I wasn’t able to get to work on this for the past few days. I’m sure you cared.

I now have a very ugly interface to the HTML grepping monster I’ve created.

Here’s the result:

> (take (sequence->list (in-full-thread-descriptions 202 1)) 5)
(#(struct:full-thread-description
   2779598
   #"Ask General Programming Questions Not Worth Their Own Thread"
   #"csammis"
   3625
   152656
   #"01:15 Apr 03, 2010"
   #"Haystack")
 #(struct:full-thread-description
   2836504
   #"Cavern of Cobol FAQ (Read this first)"
   #"Scaevolus"
   0
   22367
   #"19:14 Apr 28, 2008"
   #"Scaevolus")
 #(struct:full-thread-description
   3048157
   #"iPhone Development Megathread"
   #"tinabeatr"
   3888
   131476
   #"05:37 Apr 03, 2010"
   #"Dr. Glasscock")
 #(struct:full-thread-description
   2675400
   #"Python information and short questions megathread."
   #"m0nk3yz"
   3968
   170465
   #"02:33 Apr 03, 2010"
   #"UberJumper")
 #(struct:full-thread-description
   2585949
   #"Ruby on Rails Love-In"
   #"MrSaturn"
   1543
   69960
   #"02:08 Apr 03, 2010"
   #"Nolgthorn"))

Here’s the code for in-full-thread-descriptions, which shows the new “gather” feature for gathering specific results within a given match group.

(define (in-full-thread-descriptions forum-number page-number)
  (let ([chain (list (gather capture-text 'spacer (has-class #"thread_title"))
                     (gather capture-text 'spacer (both (is-tag #"td")
                                                        (has-class #"author")))
                     (gather capture-text 'spacer (has-class #"replies"))
                     (gather capture-text 'spacer (has-class #"views"))
                     (gather capture-text 'spacer (has-class #"date") (has-class #"lastpost"))
                     (gather capture-text 'spacer (has-class #"author") (has-class #"lastpost"))
                     'spacer
                     (both (is-tag #"tr")
                           (has-class #"thread")
                           (has-attr-match #"id" #px#"thread(\\d+)")))]
        [combiners (list (list text-combiner)
                         (list text-combiner)
                         (list text-combiner)
                         (list text-combiner)
                         (list text-combiner)
                         (list text-combiner)
                         car)])
    (in-generator
     (for ([item (in-match-and-combine chain combiners (get-forum-html-stacks forum-number page-number))])
       (match-let ([(list (list (list thread-title))
                          (list (list thread-author))
                          (list (list num-replies))
                          (list (list num-views))
                          (list (list lastpost-date))
                          (list (list lastpost-author))
                          thread-id) item])
         (yield (make-full-thread-description (bytes->number thread-id)
                                              thread-title
                                              thread-author
                                              (bytes->number num-replies)
                                              (bytes->number num-views)
                                              lastpost-date
                                              lastpost-author)))))))

Yuck.