I've been tweaking this vegetable and chicken stew recipe. Here it is as of today.
That's between $40 and $50 of stuff from my local grocery store. I already had the soy sauce, mustard, black pepper, and lentils. The sauces, spices, barley, corn, and lentils will last for multiple batches, but the rest will be consumed by today's recipe. It makes between one and two gallons of stew.
Usually I chop as I go, but for the sake of this picture today I prepped everything in advance:
Here's the list of ingredients in recipe order:
Three jalapeños is the right amount of spice for me: enough to notice but not enough to burn. If I were making this for my parents, I'd use one instead.
The stove will be on high heat until all of the liquids are added.
Brown the chicken. Get some vegetable oil blazing hot, tong the thighs into the pot skin side down, and watch out for splatters. Once the thighs are willing to be picked up without sticking too much, flip them.
You're not cooking the chicken, you're just developing some browning and getting schmaltzy goodness into the pot.
Don't worry about burnt bits stuck to the pot. That's the good stuff, and it will come off easily once the onions, carrots, and celery are added.
Mix, stir, scrape, and add black pepper.
After a few minutes, add the garlic.
You'll smell the garlic as soon as it goes in. Give that a minute and then add the peppers.
Stir occasionally, but don't be afraid to let some browned sticky stuff happen on the bottom of the pot. That will all come off when you add the tomatoes.
First, the okra.
Okra adds slimy mucilage, which I'm thinking thickens the stew. It might also help slow down browning, which at this point is happening fast at the bottom of that pot.
Let that go a little bit until you're worried about things sticking too much. Customize the recipe to match your level of anxiety. Then add the tomatoes and scrape the bottom of the pot as if you're deglazing.
More black pepper.
It won't take long for the tomatoes to break down and become soupy. At that point, add the parsley and basil, and scrape the bottom of the pot again. Be careful not to burn your knuckles with the steam that will billow from the disturbed liquid.
Now add the stock, scraping any remaining stuff from the bottom of the pot. When the liquid eventually returns to a boil, reduce the stove's heat to maintain a simmer. In the mean time, you can add a few more ingredients.
The potatoes, now that they can't stick:
The collard greens:
Remember the chicken? Now that it's cooled down, carve off as much meat as you can. Cut the pieces into bite sized chunks and add them to the pot. Save the bones, cartilage, and skin in the freezer for making next time's stock.
This recipe can accommodate five chicken thighs instead of four. My grocery store sells packs of four and packs of six, so I usually go for four.
Now's a good time to add the soy sauce and the mustard. The mustard smells like too much at first, but mellows out after a few minutes. The soy sauce is primarily for the salt, but also adds color and complements the umami of the mushrooms that you'll add next.
Let the mushrooms simmer for a couple of minutes, and then add the barley and lentils.
Set a timer for thirty minutes. Five minutes before it expires, add the corn.
When the timer goes off, you're done. Turn off the stove and cover the stew. It tastes best once it's cooled down a bit.
The celery had leaves on it, so I used it as garnish.
After two large servings, here are the leftovers:
You can just use vegetable or chicken stock bought from the store, or bouillon cubes. That's what I did.
But it wasn't fussy enough. I like to fuss.
Also, this recipe generates a lot of vegetable waste, and I'm not composting anymore, and I have this slow cooker, so...
Save your scraps in the freezer, and then put them in the slow cooker.
Add a sprinkling of whole peppercorns, a few bay leaves, and a bit of whichever savory spices you can spare.
Use just enough water (and/or last batch's stock) to nearly cover the scraps.
Put on the lid, set the slow cooker to "low," and ignore it for a day or two.
Your home will reek of deliciousness. You'll wake up from dreams of mirepoix. You'll leave and come back having forgotten about the stock and wonder what the hell is that wonderful smell?
Later the next day, here's what it looks like:
Strain out the solids and let it drip for a while.
After a day in the refrigerator, the remaining solids sink to the bottom, and the fat forms a layer on top. It's dark from all of the vegetable skins, I think.
I grew a bunch of weed in 2023. Most of it was destroyed by mold. Here's the story.
The first order of business was consolidating my indoor grow setup.
I replaced a rat's nest of ad hoc LED bulbs and extension cords with two proper grow lights. The grow lights are a little underpowered at 60 watts each, but they will do. They will not start a fire.
The plan was two plants indoors, and two plants outdoors. The outdoor season doesn't begin until early May, so I started all of the plants early indoors.
That's two Durban Poison, for outdoors, and one each of Blackberry Kush and NYC Diesel for indoors. That's below the legal limit of three mature plants per person, because four is one modulo three.
Seedlings are slow to start but take off once the roots are established.
The Durban Poison plants were sisters, I assume, so it's interesting how different they were. Right from the beginning, one of the sisters showed a temperamental disposition and unusual growth patterns. Her leaves curled as if she didn't like the light.
Her sister didn't mind.
After a while, I potted up the indoor plants into their final containers.
Early on, my plants always show signs of stress. See how the leaves are pale and papery, with highlights of purple? Those are deficiencies that can be caused by so many things. Maybe it's too hot and dry in that space, or maybe I don't feed them enough early on. Or maybe it's too much water, or inadequately draining soil, or too small a container. Fussy, fussy plants.
They typically grow out of it. Then the problem becomes lack of vertical space. At first I tried nylon thread tied between two bamboo rods, with a wooden spatula as a spacer. It doesn't work.
Let's try chicken wire.
And stay down!
As it turns out, chicken wire is a terrible idea. The flowers fatten up in the spaces, so that come harvest time you have to surgically separate the sharp rust-prone metal from the delicate plant tissue growing around it. Reddit warned me, but I don't listen. Some of those guys are jerks anyway.
The rest of the season carried on without event, until about halfway into flower, disaster struck.
Do you see the problem? Well, that plant on the left is dead. I woke up one morning and it was dead. That's because it was hopelessly infested with mold. It was everywhere. Not so easy to spot, it turns out. It hides deep in the flowers, consuming the plant from the inside out.
I've never had problems with mold. What was different this time? In retrospect, there were four problems:
Fortunately, only some of the branches were contaminated. I gently removed the bad branches, and harvested the rest, keeping an eye on the flowers as they dried, lest botrytis rear its ugly head in my curing stash. The grow space will have to be nuked from orbit before I can use it again.
Due to the early harvest, the finished product is not the best. Still gets the job done, though. And no mold.
The roof planters overwintered with a couple of layers of cardboard over them. This was my attempt to kill the forest of catnip that had been growing there the year before.
Catnip doesn't die. The mint mafia does not forget.
Pull it out at the roots and use its shredded remains as mulch.
I got some companion plants started. Different herbs, flowers, and grasses. They all died.
If you start your cannabis early indoors, like I did, then you can't just plant them outside as soon as it warms up. The sun is much brighter than LEDs. Plants grown indoors lack the natural sunscreen needed to endure the big fireball in the sky. So, you bring the plants outdoors for a little bit each day until they're sufficiently bronzed. It's called "hardening off."
A coworker recommended shading the plants from the midday sun. Here's the first iteration:
After a few days of that, I planted them.
A professional grower advised me to shade the plants more, so I rigged up some privacy for them. I also paid a little too much for pretty flowers.
That's all well and good, but what happens when the plants grow taller than those posts that are holding up the shade cloth? I could get taller posts, but they're already more wobbly than I'd like.
Use your imagination. Maybe this will help:
Thus began my foray into improvised shelter design.
Of course the wind blew that to shreds in no time.
Let's try a tarp instead.
Too big! That ended up being a bucket for rainwater. When the wind blew, the tarp would flap and thunder. When it rained, the tarp would sag under the weight of water collecting in the center.
I got a smaller tarp, which was destroyed by a storm. The arrangement that ended up working best was a clear reinforced tarp that I bought on Amazon, until that too was destroyed by a storm.
He piled upon the [tarp]'s white hump the sum of all the general rage and hate felt by his whole race from Adam down; and then, as if his chest had been a mortar, he burst his hot heart's shell upon it.
The outdoor plants went through some hard times. At first it was my doing:
That's an under-watered and underfed plant. The last time I tried to grow outdoors, the plants were over-watered, but this time I overcompensated.
They perked up after I dialed in the feeding, though, and for much of the season they were some happy looking plants.
They were even visited by a butterfly, and by hordes of the dreaded spotted lanternfly.
The lanternfly is a sap sucker, but the plants didn't seem bothered. I made a sport of shaking them off the plants and chasing them down with a sandal. They can jump very far.
Does anybody know what kind of bug this is? They were getting busy under one of the leaves. At first I left them alone for curiosity's sake, but after they started crawling around on the cannabis flowers I gave them the pinch.
I ended up overfeeding the plants, I think. At least one of them eventually showed signs of stress (surprisingly, it was the stronger sister who had trouble).
Here's my theory. I was feeding using Bruce Bugbee's lab recipe: 20-10-20 fertilizer diluted to 120 ppm nitrogen with every watering. What I forgot is that Dr. Bugbee practices "drain to waste," meaning that he effectively flushes out the growing medium every time. This ensures that fertilizer salts don't build up, which could "burn" the plant. I was not draining to waste, because I was concerned that would lead to waterlogged soil. For my setup, it would have been better to fertilize every other watering, maybe at a slightly higher concentration.
Anyway, the plants survived. Here they are under the clear tarp.
The second half of September was wet and punishing. The plants pulled through, a little worse for wear.
Then, there was a nasty storm that took the head clean off one of the plants.
What with all the rain and the body horror, I started watching the plants very closely. I found lots of mold.
Now, with outdoor plants, mold is not the death sentence that is can be indoors. Outdoors, water falls from the sky. Especially in September. It's a matter of vigilance keeping up with mold late in the season. These plants got to the point where I thought it best to start harvesting some of the larger branches early.
By the second week of October, I was ready to harvest the rest.
There was so much weed. These plants were small by outdoor standards, and I still ended up using every clean surface in my apartment to dry the branches.
Despite all this, there was not that much finished flower. The plants needed maybe another month to bulk up, and most of the product I did get was leafy and "larfy." I will be making edibles for generations, though.
The trim job was brutal.
I ended up with a few jars of passable flower, and two buckets of schwag.
That will suffice until harvest 2024.
<html>{{foo}}</html>
❌
['html', foo]
✅
You're writing a web server that does server side rendering of HTML for a website. Pages are mostly static, but they differ slightly from session to session.
I know, I'll write a page.template.html
that looks like HTML but contains
{{special syntax}}
or maybe <%special syntax%>
. Then my web server will
read the file, replace the special syntax sections with markup based on logic
of my choosing, and then serve the resulting string as HTML.
You're writing a Kubernetes controller, and you know that the resources managed by the controller will need their own YAML configurations, but the YAML will have to differ based on the configuration of the controller.
I know, I'll write a deployment.yaml
file that isn't actually YAML, but
instead is a mix of YAML and Go's text template syntax. I'm using Go
already, so I can just tmpl.Execute(output, myData)
, and voila,
myData.Thingy
is now part of the YAML where there was once {{.Thingy}}
.
You're writing a code generator to automate the more tedious parts of a programmatic interface. Maybe the generated code contains message types for use with an RPC framework in a statically typed language. Maybe the generated code marshals the results of known SQL statements into a library's types. Maybe the generated code encodes a set of types into some serialization format like JSON or Protocol Buffers.
I know, using string templates naively could get out of hand here since we're
working with a general purpose programming language. I'll just write escaping
functions to make sure that "
is escaped when inside of a double-quoted
string, and I'll write validation functions to make sure that the name of a
variable doesn't include a comment opening sequence (/*
).
I can use string templates to generate everything, so long as I'm careful.
Stop doing this. If your goal is to end up with a string containing language X, then you cannot begin with a string. Instead, you must begin with a representation of language X. Then, perform any desired transformations within the representation of language X, and only at the very end render the representation into a string.
It's not overkill. The problem of altering structured data is different from the problem of manipulating strings. Don't use the latter just because it's on hand or because the alterations seem trivial. Find a library that allows you to represent the target language in your programming language of choice, or otherwise write your own.
Once your project is a soup of string templates supporting an increasingly complex mini-language of syntax oblivious interpolation operations, it's too late to replace it with something better. You must start with a representation of the target language.
You don't necessarily need to have a representation that covers the entire language — it need only cover what you will generate. Add bells and whistles later as needed.
list
s and dict
s to
represent XML.In C, C++, and Java, why do variable declarations put the primitive type name
on the left and not on the right? Declaring a variable x
of type int
is
this:
int x;
instead of this:
x int;
Don't get distracted! You were about to explain how in C, declaration syntax is analogous to expression syntax, weren't you? You were about to tell me about cdecl and how Go did it differently after years of programmers complaining about the C syntax. You were going to offer an opinion too, weren't you?
That's not what I'm talking about. The fact that int x;
means
Declare
x
such that the expression "x
" has typeint
.
and that int (**x)(double, char)[2];
means
Declare
x
such that the expression "(**x)(d, c)[i]
" has typeint
for givendouble d
,char c
, andint i
.
does not explain why the int
is there on the left. It could be on the right.
Could it?
I mentioned this in a Hacker News comment thread, and someone replied:
Because parsers, like humans, read from left to right. Important parsing decisions are usually made as early as possible. An example is recursive descent parsing, which derives its simplicity from making decisions first, allowing a top-down parse. And when humans skim text, I bet it can be shown that it's easier if the syntactically significant locations are aligned near the beginning of each statement.
The idea is that a C compiler reads input from left to right, and that knowing
that the ultimate primitive type asserted by the declaration is int
is useful
to know at the beginning of parsing the declaration, as opposed to at the end.
Maybe this was important in 1972, but I don't think it is today. On the other
hand, I've never written a C parser. Maybe the presence of a type name (int
)
at the beginning of the parse lets us know that we're parsing a type
declaration, and not some expression. Maybe that's helpful for avoiding
ambiguity.
When in doubt, ask ChatGPT. Given the following prompt:
In the C programming language, why does the primitive type name appear on the left in a declaration, and not on the right?
For example, the following code declares a variable "x" with type "int":
int x;
Why is it not instead:
x int;
Why might this design have been chosen?
ChatGPT generated:
The C language's syntax for declaring variables with a type name on the left and the variable name on the right was influenced by earlier languages such as B and BCPL. This syntax was chosen for a few reasons:
- Readability: Placing the type name first makes it easy to quickly identify the type of a variable when scanning code.
- Parsing: The type name first makes it easier for compilers to parse declarations. The compiler can look for a specific pattern (type name followed by variable name) when parsing declarations, which simplifies the process.
- Consistency: Placing the type name first makes declarations more consistent with function definitions, where the return type is also placed first.
In general, the C language was designed to be simple and straightforward, and the syntax for variable declarations is no exception. The current syntax has become a standard convention and is widely used in many other programming languages as well.
It claims we have to look further back than C to the languages on which it's based. Fair enough.
The "readability" point I'll ignore, since what is readability anyway?
The "parsing" point is the same one made by the Hacker News commenter, so maybe there's something to that.
The final point, "consistency," is interesting. The idea is that since function declarations have their return type on the left, that variable declarations should follow suit for consistency.
I don't buy this point, because function declarations are declarations. Unless there's some historical explanation where function declaration syntax existed before variable declaration syntax (plausible in a world where all variables are machine words), then there's still the question of "why is the return type on the left?" which is then the same question as "why is the ultimate primitive type on the left?"
After asking one human and one bot, the reason is still a mystery to me. Maybe the answer is just "because that's the syntax they chose."
Welcome to the Land of Smiles.
The prostitutes block your path in neat formations like rows of tiny stilettoed linebackers. The one who grabbed my crotch had implausibly strong hands.
We were on our way from one fancy beer bar to another, and our route brought us through a red-light district.
As it was my birthday, I stated my preference that there would be no girlie bars on the night's crawl, nor go-go dancers, unsolicited solicitations, kickbacks from the bartender, or where are you from. Massage? A bar nearby is called "Friendship Bar." A Google review claims "nice girls, no pressure." Well that doesn't sound so bad. Nice girls, no pressure. Who doesn't like friendship?
We ate at the first pub, a sleepy spot practically devoid of humans and featuring a pool table and a dart board, and a full kitchen. The portions were small, so we ordered two entrees each. The cook would even make things off the menu by request. Pretty cool for a sports dive. We ended up there at the late end of a subsequent night, just the two of us playing pool while the staff patiently waited for us to leave.
Then it was off to an English themed pub. Here we discovered Beerlao Dark as recommended by Uncle Charlie, and watched some of whatever soccer game was on. We returned there briefly some days later but had to leave in a hurry to find relief from the earwax-melting pop duo who were saturating the airwaves with renditions of Neil Diamond's "Sweet Caroline" and Radiohead's "Creep" while shitfaced regulars howled the lyrics in discordant unison. In all fairness, the duo was quite good, but too fucking loud.
Then it was time to up the ante and head to a semi-outdoor beer extravaganza, also with a full kitchen and World Cup soccer. There are many places to drink in this part of Bangkok. All expats and tourists. It took some getting used to coming from Vietnam — the block we're on more closely resembles the apartments across the street from Costco than it does the center of the capital of Thailand. Except for all the hookers.
South Korea versus Ghana was a pretty exciting soccer game. For example, the score was not 0-0. Ghana, coming in widely outranked by Korea, scored two goals right off the bat, or whatever the analogy is in soccer. Then Korea managed to tie it up. Ghana scored again, and there just wasn't enough time for Korea to take it back. If I were judging based solely on the reactions of the Korean fans, I'd say the world had truly ended.
There are other things to see and do in Bangkok.
I didn't get a good shot of it, but that park was infested with dog-sized monitor lizards, or I think that's what they are. Godzilla.
Wednesday we saw the sights, including the palace and the reclining Buddha.
Peekaboo!
The interior walls of one of the buildings was covered in "Where's Waldo?" style murals depicting warring peoples, apes, demons, gods, and all sorts of wild stuff.
I'm getting better at aiming the discolored dollops of glue that I expel from my nose in the shower every morning. Straight for the drain, it's a mess to clean off the tile.
In Vietnam I could attempt to pronounce the writing. In Thailand there's no chance. Here's a sample of the script:
Yesterday we took the underground out to the Chinese part of town. Here things were much more like the rest of the region: narrow alley markets, fish everywhere, skinny buildings, temples, schools, and Asian people.
We passed through a neighborhood where nearly everybody must be a machinist or a motorbike mechanic.
I didn't take many pictures in Chinatown, which is a shame because it's a nice area.
I did take pictures of the river and of the golden Buddha.
Our last find in Chinatown was a church that you can only access by walking through a school playground. In the corner was a narrow spiral staircase that led up to a balcony in the rear of the church. There were instruments and mics set up for the band, and behind that was a small music room where the carillon was controlled. You could even climb a ladder up the the bells themselves, if you dared.
After a little rest by the river, we walked just shy of an hour as the sun set, headed for the party street where we had dinner and partied.
Today we slept way in and then ran some errands. Tonight we might revisit a park.
Tomorrow Pasha heads back to Pattaya and I head over the Pacific.
ลา.
As helmsman and joint captain of the USS Paddle Kitty, I'm responsible for ensuring safe and timely passage through the smog obscured waters of the Hồ Tây. An enemy ship is always nearby.
Back in Nha Trang we did get one good beach day. The water was nice and warm.
There was another temple, this time with boobs.
On the way back from the temple, we stopped at a choose-from-the-tank seafood restaurant. These were my victims.
Pasha didn't want to order anything there, so we stopped at another restaurant closer to the apartment.
There was no shortage of temples, pagodas, and citadels in Hanoi, either.
I don't know why, but entire streets are dedicated to selling Christmas decorations. This is a small sample, but imagine city blocks of blinding lights, santa hats, candy canes, and plastic reindeer.
Being a nominally communist capital, there is an obligatory statue of Lenin.
The main tourist spot is a "beer street" in the old part of the city. Rows and rows of tiny plastic chairs where you drink beer and eat hot pot and boiled peanuts. Once every couple hours a police car crawls by, and the staff all scramble to fold tables, chairs, and umbrellas to make room for the car. When the cop has passed, they put everything back.
The world cup is happening, so almost every venue had a big screen TV with the day's game. We met a couple of French guys, some Canadians, and one rather well traveled Chilean named Nacho.
The food in Hanoi was less about the soup and more about the meat, though soup was never far away.
So much for Vietnam. We're currently on our way to Bangkok.
I should look up how to say "hello."
Goodbye, Ho Chi Minh City.
Hello, Nha Trang.
Before we left, I repaired the handle on my suitcase. All it took was Pasha's Soviet era scissors, a roll of local nylon yarn, a chopstick, some packing tape, and a little patience. I got the yarn and the tape from the hardware district in Ho Chi Minh. Paid way too much for the yarn. The tape was okay.
If you're thinking "why not just buy a new suitcase?" then you are so missing the point. This suitcase was on its way to the landfill, abandoned and neglected. Now it is my pet. My precious pet suitcase. My janky socks box.
We haven't seen the sun yet, and it rains constantly. That's a little disappointing for a beach destination, but there are other things to do.
This afternoon we walked through town in the rain on our way to another pagoda, Long Sơn Pagoda. Rest assured, Nha Trang also has alleyways.
Across the tracks and up a number of steps there is a campus of shrines and statues, including this one of the reposed Buddha.
And this one of the enthroned Buddha.
I was into the views of the surrounding city, and some careful gardening by who I imagine must be some of the monks.
At the bottom of the facility is a small labyrinth of shrine cubbies, like some kind of vertical graveyard. Each occupied square contained a plaque with a picture of the deceased, their name, lifetime, some incense, and oil for flame.
We are surrounded by Russians. The restaurant signs are in Vietnamese, English, and Russian. One restaurant even had a separate Russian menu. The dishes were totally different, few in number, and each accompanied by a large picture.
Several of the restaurants prominently display non-decorative fish tanks. Who will be my next victim?
Twenty hours in the air. First leg was sixteen hours from JFK to Taipei, and then another four from Taipei to Ho Chi Minh City.
A 750 mL plastic bottle of filtered tap water costs $7.50 in terminal 1 of JFK airport. It didn't occur to me to bring an empty bottle through security, and then use one of the many bottle-filling water fountains near the gate.
The flight went very well because the seat between me and the other isle was empty. The middle seats were empty throughout the whole plane, so I'd say the flight was just below 2/3 full. I wonder how many seats need to be filled in order for the airline to break even.
After the final four hour leg to Ho Chi Minh, I was hurried onto military transport to ensure timely arrival at the palace.
Nguyễn Cao Kỳ already sat waiting at his desk when I was ushered into the office. Pasha soon followed, and the two of us sat facing each other as we received the Prime Minister's briefing in our right and left ears, respectively.
We had been summoned, as it turned out, to lend our aid to the war effort. Pasha and I noted the irony of this, but for different reasons.
My role was to intercept and decrypt all short and long-band enemy dispatches. To this end I was assigned this mid-90's Windows workstation:
Pasha, for his part, was to aid in supply lines on the northern front.
In exchange, assuming we survived, the state would grant us free passes to the Saigon Zoo and Botanical Gardens.
It's probably the best zoo I've ever visited. I expected to see forlorn, defeated animals trapped in concrete cages. Actually, that's pretty much what it was. But the place is huge! And some of the monkeys were free to roam around the park. They were silently walking across the scaffolding roof of the boardwalk overlooking the ungulates.
They have elephants and giraffes, too.
... and deer, and chimps, and pigs...
... and this guy...
... and all of the crocodiles, and iguanas, and snakes...
... and many others.
Near closing time, the goats noticed that a caretaker was hosing down the adjacent hippopotamus enclosure. They voiced, in unison, a reminder that it was dinner time.
The grounds are beautiful.
Ho Chi Minh City is its own sort of zoo, packed with motorbikes and lined with nooks and alleys,
temples, pagodas, ancient tablets, and a very classy post office.
There is also a war museum, but it's full of things that you don't want to put in your blog.
Pasha and I walk everywhere. Blisters on my toes. At this rate, soon I will no longer be the fattest person in Vietnam.
Every third storefront is a cafe. The places that cater to tourists want as much as $3 for a coffee, but my man with the cart charges 60¢.
Tomorrow we'll be meeting my uncle's step daughter and her husband for dinner. I have yet to find bad food here, so I must keep searching.
Hẹn gặp lại.
I'm a better catnip gardener than I am a cannabis gardener.
Growing cannabis outdoors in New York is not easy, no it is not.
The plant containers began their life as newly purchased black Rubbermaid Roughneck 31 gallon storage totes.
I drilled holes in the bottoms and sides, gave the exterior a couple of coats of the cheapest white paint I could find, bought a shovel and a tarp, and filled the containers with a soilless "Coot's" mix.
They're sitting on what used to be the back of an Ikea futon.
I put some straw mulch on top and scattered some seeds of white clover, German chamomile, lemon balm, and catnip.
Meanwhile, I got some seedlings going indoors. Two of them are Durban Poison and one is Master Kush.
You can't just put an indoor plant outside. The sunlight will kill it. Instead, you take the plants outside for a short time each day, gradually increasing their exposure to full sun.
After doing this for a while, I transplanted two of the young plants into the totes. I don't remember which strain was which.
A coworker recommended that I give the young plants some shelter from the noon sun, so I found some light metal polls on garbage day, and used them to prop up the tote lids as little roofs.
On a windy day I noticed that one of the plants was being blown around to the extent that the roots were moving. Anchoring the middle of the stem to the roof supports helped limit the wobble.
At this point the plants were well established and began to thrive.
I chopped off the top portion of each plant in order to promote more lateral growth. This increases yield for indoor plants, and may increase yield or discourage mold for outdoor plants.
There was quite a growth response.
That week I noticed that a weed that had been growing alongside each plant since the start began to take over. In an act of anticompetitive crony favoritism, I pulled the weeds out at the roots.
After a particularly windy day, I noticed that the left plant had suffered a stem injury. Not long after, the plant took a turn for the worst, and within a few days it was dead.
How quickly things can go south.
Then, less than a month after the first plant died, we go a spat of heavy rains, and so went the second plant.
At least the catnip is thriving.
My best guess at the problem these plants faced is inadequate drainage of the growing medium. The mix is approximately 1/3 perlite by volume, but perhaps after a heavy rain even that is not enough to keep the roots from rotting.
I could remix the medium for next season, as I am now the proud owner of four cubic feet of vermiculite. However, that would mean that I throw away an entire season's worth of soil development. The growing medium is now full of roots and fungi and life, and is covered by a thick mold-crusted layer of straw mulch and fallen cannabis.
On the other hand, leaving the totes as is risks next season's plants meeting the same fate. I have all Autumn and Winter to think it over.
In the mean time, the third plant has been surviving indoors. It's a Durban Poison, and sure stinks up the place.
I grew some plants. Third time's the charm.
They began, as we all do, as little spindly nubs.
That third one is a runt that didn't make it. It grew, but slowly, and its leaves were distorted and small.
The laboratory includes a humidifier, webcam, and digital thermometer/hygrometer.
After discarding the runt, I potted up the survivors and added another humidifier.
Soon, though, the plants began to ail. They drooped and began to yellow and shrivel.
It could have been any number of things:
Reasoning that the problem had something to do with the container geometry or its contents, I "potted on" into larger containers with fresh mix.
My grow space lacks height, and I've read that cannabis roots like to stretch out laterally. So, I found this tub in the local hardware store, drilled some holes in the bottom, and placed it suspended on a rack above a baking sheet.
This time I used sexy bark mulch.
The other plant I potted up to a larger conventional pot, though after a while I ended up putting it in a tub too.
The plants were much happier in these new containers.
As you can see, they're starting to get close to the lights, so it was time to train the plants by tying them down.
After adjusting to their bondage, the plants looked as healthy as I've ever kept.
In late February I switched the light schedule from 18 hours on down to 12 hours on. This signals to the plants that Winter is coming and that they should begin producing flowers to reproduce. Except that they won't reproduce. I will smoke them instead.
In March I visited Paris for a week for work. In that time, Pasha kept the plants alive.
When I returned, I was greeted by very happy flowering plants.
How do you know when a plant is done and ready for harvest? Opinions differ online, but what I gather is the following:
Trichomes are difficult to see with the naked eye. Growers use a jeweler's magnifying glass, called a "jeweler's loupe," to inspect the trichomes.
Here are the plants well into flowering, but a while before harvest:
Taking a phone picture with the loupe works better than I expected, but I still haven't gotten the hang of it.
In May I returned to Paris for work. Beforehand, though, I harvested the plants so that nobody else would have to care for them so close to the end. It was close enough, I think.
I snapped off the branches one at a time and roughly trimmed off the leaves using special trimming scissors. I'll do another trimming after the flowers have dried. For now I'm just removing most of the vegetation.
That's most of the first plant. Paper bags in the refrigerator? I'll explain later.
The following morning I harvested the other plant.
I'll weigh the harvest after it's dry. Later that afternoon I departed once again to Paris.
This is my first time attempting to properly dry a harvest of weed, and I've chosen an unorthodox and possibly risky method.
The reason is that I was going away for a week right after harvesting, so I sought a "hands off" method that would dry the buds slowly while I was gone.
Usually what people do is hang the branches (or the entire plant) upside-down for a week or two in a temperature and humidity controlled environment with some airflow until the branches snap without bending. Then the buds go into jars that are periodically opened ("burped") until the desired relative humidity is reached inside.
It's getting humid and warm in New York these days, so while the standard method probably would have worked fine, I sought an alternative.
Some people (i.e. at least two people on the internet) dry their weed in the refrigerator. The idea is that the fridge is a self-cooling and dehumidifying cave of sorts. To regulate the humidity in the immediate environment of the weed, you put the buds in paper bags and stick them in the fridge. Then you regularly give them a shake, and less regularly put a sample into a jar with a hygrometer to see how the dry is coming along.
Others on the internet have told me that this is foolishness and that all of the weed will smell like refrigerator.
Well, it's now been two weeks, and "refrigerator smell" has not been an issue. However, the weed is not drying quickly enough. I put some buds in a jar on the counter for several hours the other day, and the humidity settled at 99%. There was condensation on the inside of the jar.
Granted, everything happens more slowly at 34°F, but I'm concerned about mold developing.
I'll keep giving the bags a shake and take a jar sample again in a few days. If they haven't begun to dry more by then I'll consider putting them on racks outside of the fridge to dry instead.
I bought a six pack of those giant Ball jars, and I'm wondering if that will be enough. Assuming the buds don't mold into the world's most expensive compost pile addition, I'll weigh them once they're curing.
The worms are still alive and pooping. They have pooped a lot.
It's been tricky to get all of the variables right, but I think I'm getting the hang of it:
Mostly I feed them used coffee grounds. Sometimes I add vegetable scraps.
Here's the system.
Three buckets. The top one has a lid with holes, and holes in the bottom.
The worms mostly live in there. Previously, they lived in the bucket below. The idea is that the worms will wiggle up to the top bucket (through the holes in the bottom) in search of food.
That's mostly happened, but I still see a lot of worms in the lower (middle) bucket.
The middle bucket is now filled with castings, and some worms. There are holes in the bottom of the middle bucket, allowing excess moisture to drain into the bottom bucket. There's also a mesh screen to prevent worms from wandering off.
The castings resemble crumbly, lumpy topsoil. There's a shimmer of vermiculite left from the potting mix they were originally living in.
I'll use this in place of, or to supplement, compost in my soil mixes.
It's been about six months since I got the worms, so they haven't pooped as much as I expected. I've probably been underfeeding them.
Their bedding is paper bags that I've shredded by hand with scissors. Come sit-on-the-couch-and-watch-youtube time, I grab some brown bags and shred them into this plastic bag.
That's about all there is to it.
Soon I'll be mixing a large batch of soil for my planned rooftop garden. Most of the castings in the middle bucket (worms and all) will go into that soil. Then I'll dump the contents of the top bucket into the middle, and place the lid on the middle bucket. Back to square one for another batch.
Wiggle on.
I've been reading Denotational Semantics, a textbook describing a theory of programming languages. It was recommended to me years ago by a colleague. He said something along the lines of "this book taught me everything I know about programming." He seemed to know a lot about programming.
The first few sections introduce a formula rewriting system known as the lambda calculus, which was familiar to me due to my previous dabbling with Scheme, a programming language invented to study the lambda calculus.
In particular, one of the ideas introduced in the lambda calculus section of Denotational Semantics is illustrated differently in another book I read, The Little Schemer. The idea is how an anonymous procedure can nonetheless refer to itself by name.
This post is my attempt to explain that idea.
Instead of using the lambda calculus, I'll use Scheme. I like Scheme.
Here's the definition of a procedure:
(define collatz-end
(λ (n)
(cond
[(= n 1) 1]
[(even? n) (collatz-end (/ n 2))]
[(odd? n) (collatz-end (+ 1 (* 3 n)))])))
Here n
is any nonzero natural number, so n
could be any of 1, 2, 3, 4, ...,
44989933, etc., but not -4, and not 0.
The procedure collatz-end
is equivalent to the following procedure, provided
that the Collatz conjecture is true:
(λ (n) 1)
The Collatz conjecture is almost certainly true, but it might be impossible to prove.
Anyway, in Scheme, (λ (x) Expr)
is a procedure of one parameter, x
, that
evaluates to whatever Expr
would be if x
s within it were substituted for
whatever we supplied for x
.
For example, (λ (x) (+ 3 x))
, when applied to 7
, is (+ 3 7)
, which is
10
. The expression that says "apply (λ (x) (+ 3 x))
to 7
" is the
procedure and its argument next to each other in parentheses. So,
((λ (x) (+ 3 x)) 7)
is (+ 3 7)
is 10
.
It's a bit trickier than that, though, because the body of the λ
could
contain other λ
s, and those might introduce a parameter called x
, too. In
that case, the inner x
is really a different x
from our x
, and so we'd
have to rename one of them to accommodate the other. Let's not worry about
that here.
cond
is a shorthand for nested if
expressions. An if
expression has
the form
(if Predicate Consequent Alternative)
Each of Predicate
, Consequent
, and Alternative
is an arbitrary expression.
If the result of evaluating Predicate
is not falsy (I won't bother
describing falsiness in Scheme), then the value of the if
expression is the
result of evaluating Consequent
. In that case, Alternative
is not evaluated.
On the other hand, if the result of evaluating Predicate
is falsy, then
the value of the if
expression is the result of evaluating Alternative
. In that
case, Consequent
is not evaluated.
cond
, then, is just a macro that nests its [Predicate Consequent]
arguments.
I'll define it here using syntax-rules:
(define-syntax cond
(syntax-rules (else)
[(cond)
(raise-user-error "Unmatched cond alternative")]
[(cond [else expr])
expr]
[(cond [predicate expr] rest ...)
(if predicate expr (cond rest ...))]))
Note that square brackets ([]
) and parentheses (()
) have the same meaning,
and are varied as a matter of style.
Now let's look at the definition of collatz-end
again:
(define collatz-end
(λ (n)
(cond
[(= n 1) 1]
[(even? n) (collatz-end (/ n 2))]
[(odd? n) (collatz-end (+ 1 (* 3 n)))])))
If n
is 1
, then we finish at 1
. If instead n
is any even number, then
we divide n
in half and see what collatz-end
gives for that number. So,
collatz-end
is defined in terms of itself. If n
is an odd number, then we
take triple n
and add 1
, and see what collatz-end
gives for that number.
It's not at all obvious that this procedure always evaluates to 1
, but the
smart money says it does.
I defined procedure application in terms of parameter rewriting, e.g.
((λ (x) (+ 3 x)) 9)
means "use 9
instead of x
in the body of the λ
",
i.e. (+ 3 9)
, which is 12
.
Then what is define
?
define
introduces a name (collatz-end
), and then associates a value with
that name ((λ (n) (cond ...))
), where the name is visible inside the
definition of the value.
define
is a facility provided by the execution environment. We cannot
get that behavior using λ
s only. Or can we?
Consider this anonymous cousin of collatz-end
:
(λ (f)
(λ (n)
(cond
[(= n 1) 1]
[(even? n) (f (/ n 2))]
[(odd? n) (f (+ 1 (* 3 n)))])))
This is not the same as collatz-end
. Instead, it's one "step" in the
execution of collatz-end
. Give me a "next" procedure (f
), and I'll give
you a procedure that takes an n
, does one Collatz step on it, and then passes
the resulting number to f
.
Observe how this entire expression is nearly what we want for f
.
Here's another cousin:
(λ (f)
(λ (n)
(cond
[(= n 1) 1]
[(even? n) ((f f) (/ n 2))]
[(odd? n) ((f f) (+ 1 (* 3 n)))])))
You are forgiven if your head is starting to hurt. This is like the previous
example, except that now the f
that we accept is a procedure that takes
a procedure and returns a procedure that takes a nonzero natural number.
That is, f
is something that looks like the most recent example.
If we apply this procedure to itself, do we get collatz-end
?
((λ (f) (λ (n) (cond [(= n 1) 1] [(even? n) ((f f) (/ n 2))] [(odd? n) ((f f) (+ 1 (* 3 n)))])))
(λ (f) (λ (n) (cond [(= n 1) 1] [(even? n) ((f f) (/ n 2))] [(odd? n) ((f f) (+ 1 (* 3 n)))]))))
This evaluates to a procedure that takes a nonzero natural number, and ...
does the same thing that collatz-end
does.
Whether an execution environment will demonstrate this fact depends on the
evaluation order used in the execution. Thanks to the "short-circuit"
evaluation property of if
, which underlies our cond
, this procedure really
is the same as collatz-end
.
The self-applicative monstrosity above is not yet satisfactory, though. Let's look at the original procedure again:
(define collatz-end
(λ (n)
(cond
[(= n 1) 1]
[(even? n) (collatz-end (/ n 2))]
[(odd? n) (collatz-end (+ 1 (* 3 n)))])))
This entire snippet of code is the same no matter which name we choose for
collatz-end
, as long as our choice is not already taken
(λ
, cond
, even?
, ...).
So, collatz-end
is reasonably thought of as a parameter. Let's call it f
.
(λ (f)
(λ (n)
(cond
[(= n 1) 1]
[(even? n) (f (/ n 2))]
[(odd? n) (f (+ 1 (* 3 n)))])))
We're now back to where we were at the beginning of the previous section. This
procedure is not collatz-end
, but it's the natural non-recursive analog to
collatz-end
. Let's call this procedure E
.
Can we define a procedure that, given E
, returns collatz-end
?
Earlier, we modified E
, replacing appearances of f
with (f f)
. This
allowed us to describe collatz-end
as the modified procedure applied to
itself.
It would be nicer if such modification weren't necessary. Maybe there is a
way to encode that transformation in the operation that takes this anonymous
procedure and gives us collatz-end
.
Well, we have a procedure that looks like this:
(λ (f) (λ (n) ...))
where the ...
uses f
as a procedure taking a natural number, i.e. some
(λ (n) ...)
. We want that inner f
to be based on the overall procedure.
Consider this procedure:
(λ (f) (λ (n) ((f f) n)))
This is an attempt to encapsulate the transformation we performed in the previous section; namely, to take a procedure that returns a Collatz-like procedure, and transform it into a Collatz-like procedure whose inner "next" procedure is the original procedure applied to itself.
It's hard to say in English.
Let's call this procedure Sb
, for "antimony." No, "self-bind."
Then, we can combine E
and Sb
to produce an anonymous procedure that might
get us closer to collatz-end
:
(E (Sb E))
Remember that E
is
(λ (f)
(λ (n)
(cond
[(= n 1) 1]
[(even? n) (f (/ n 2))]
[(odd? n) (f (+ 1 (* 3 n)))])))
So, we're looking at (expanding E
and Sb
)
((λ (f) (λ (n) (cond [(= n 1) 1] [(even? n) ((f f) (/ n 2))] [(odd? n) (f (+ 1 (* 3 n)))])))
((λ (f) (λ (n) ((f f) n)))
(λ (f) (λ (n) (cond [(= n 1) 1] [(even? n) ((f f) (/ n 2))] [(odd? n) (f (+ 1 (* 3 n)))])))))
Is this collatz-end
?
Not quite. I made a mistake. Close, but no cigar. Look at (E (Sb E))
again, expanding only Sb
:
(E
((λ (f) (λ (n) ((f f) n))) E))
That is
(E (λ (n) (E E) n))
That λ (n) ...
is passing E
as the f
argument to E
. But E
and f
have incompatible types!
This construction "(E (Sb E))
" only works "one layer deep," and then becomes
invalid.
The thing that we pass as the f
argument to E
can't be E
itself, it has
to be the thing we transformed E
into.
Mind bender!
Alright, let's go back to the thing that works, but is not ideal:
(λ (f)
(λ (n)
(cond
[(= n 1) 1]
[(even? n) ((f f) (/ n 2))]
[(odd? n) ((f f) (+ 1 (* 3 n)))])))
Remember that this thing applied to itself is collatz-end
. We need to get
that inner (f f)
working for us without having it in there.
Here's a transformation to consider:
(λ (f) (f f))
Well, we want to double up the f
s and then pass that to E
, so
(λ (f) (E (f f)))
Let's expand E
in that. Do we get the same thing as "the thing that works,"
above?
(λ (f) (E (f f)))
(λ (f)
((λ (f)
(λ (n)
(cond
[(= n 1) 1]
[(even? n) (f (/ n 2))]
[(odd? n) (f (+ 1 (* 3 n)))])))
(f f)))
(λ (f)
(λ (n)
(cond
[(= n 1) 1]
[(even? n) ((f f) (/ n 2))]
[(odd? n) ((f f) (+ 1 (* 3 n)))])))
Yes, we do.
Ok, so the operation (λ (f) (E (f f)))
is the secret sauce. That gives us
something that we can apply to itself, giving us collatz-end
.
That is, collatz-end
is the same as:
((λ (f) (E (f f))) (λ (f) (E (f f))))
A nicer way to look at it is to consider this operation as a procedure applied
to E
. Let's call the procedure R
for "recursive."
(define R
(λ (E) ((λ (f) (E (f f))) (λ (f) (E (f f))))))
Note how now E
is a parameter.
Then collatz-end
is the same as
(R
(λ (f)
(λ (n)
(cond
[(= n 1) 1]
[(even? n) (f (/ n 2))]
[(odd? n) (f (+ 1 (* 3 n)))]))))
A combinator is a procedure within which every variable has a matching λ
.
R
is a combinator. E
and collatz-end
are not, because they depend on the
"free" variables collatz-end
, even?
, =
, etc.
R
is a particularly famous procedure known as the Y combinator.
Fun stuff, right? Don't forget to floss your teeth, pay your taxes, take off your shoes, and be well!
I made weed cookies. They're devastating.
The basic idea is to dissolve weed's active ingredients into butter, and then use the butter to bake cookies.
But first, you have to turn this:
into this:
The first molecule (tetrahydrocannabinolic acid) won't blow your mind, but
the second molecule (tetrahydrocannabinol) might. You can convert the
first into the second by removing the COOH
group attached to the rightmost
ring. It's called decarboxylation, and will happen eventually given some
heat.
When you smoke weed, the fire supplies the heat for the reaction, but these cookies will not be on fire, so we have to do something else.
I went digging through my plastic bag of stems and whatever else was left over from harvest, picked off what flower material I could, ground it up in an herb grinder, and put it into a mason jar.
You can do this in the oven, but I prefer an odorless, zero-oversight, overnight approach. So, I filled my slow cooker with water. After sealing the jar tight, I placed it on its side in the water. Then I turned the slow cooker on "high" and went about my business. When later I saw that the water was nearly simmering, I turned the setting down to "low" and went to bed.
The next day, I used some tongs to pick the hot jar out of the slow cooker, and set it on the counter to cool. After a while, I opened it for a sniff. Smells like oregano, or something that'd be in a tomato sauce. It was surprisingly pleasant and un-dank.
Ok, butter. But first, we need a cookie recipe.
Google brought me to reddit, and reddit brought me to this 2017 recipe, which is hailed "reddit's best cookie recipe."
Here are the ingredients:
I followed the recipe exactly, except that I first modified the butter to make it adult-only.
I stirred the ground cannabis into the melted butter, and let it just barely simmer, covered, for like an hour. I read multiple recipes for "cannabutter" online, and their cook times varied from a couple hours to a couple days. Longer is probably better, but on the other hand:
Once the cannabutter was done cooking, I used a cheesecloth to separate the butter from the herb.
Now it's time to make cookies. I added the sugars to the hot butter and stirred until they dissolved as much as they would. This also cooled the butter down to near room temperature.
It looked like there wasn't enough moisture, but after mixing in two eggs, the salt, and the vanilla extract, the wet side was wet.
Then I gradually stirred in the dry ingredients until I had a dough. Then I folded in the chocolate chips and was ready to start shaping cookies.
Each cookie is a ¼ cup scoop of dough rolled into a ball and then squished slightly onto the baking pan.
I preheated the oven to 350°F and baked the cookies for 12 minutes, et voilà!
Suggested serving size: ¼ cookie.
There is no God.
Four months resulting in three jars. It could all fit into one jar, but I want to keep the strains separate.
The most promising product is from the NYC Diesel plants. It smells vaguely of lemon gasoline and will catapult your mind into diamond-grinding rumination. It will also give you bronchitis, because it was harvested prematurely, dried too quickly, and has not yet cured. It will probably never cure.
The other stuff smells like hay. Maybe I'll bake cookies with it.
Relative humidity is a funny thing. Actually it's not funny at all. Nothing is funny. Life is pain.
Since our last installment, I re-potted each plant into its own small container. The Grape Ape plants (left) took to their new homes immediately, springing back to life with a growth spurt. The $100 O.G. plants (center) took longer to acclimate but also eventually thrived. The NYC Diesel plants (right) looked like they were going to stay lanky and spent, but after a few doses of nutrients by soil and by spray, they too perked up.
I planted some spicy peppers in solo cups, to make better use of the lights. They all died.
After a while, the plants had grown taller and largely healthy. They had been receiving eighteen hours of artificial light per day, followed by six hours of darkness. Kept this way, the plants will continue to grow indefinitely, without ever trying to reproduce.
The stuff that gets you high occurs with the highest concentration in sticky resins secreted by the female sex organs in their fruitless attempt to capture pollen.
Cannabis has been selectively bred by humans to produce "flowers" that are:
It's all very unnatural. But organic! It's not a big truck.
Outdoors, cannabis will switch into reproductive mode ("flowering") when there is less than thirteen hours of sunlight each day.
Indoors, we change the timer on our lights to be on for twelve hours and off for twelve hours. The plants don't know the difference, they're so stupid.
Cannabis doesn't survive Winter, so the shortening photoperiod tips off the plant to its impending death, which it copes with by ramping up cannabinoid production as a side effect of attempting to reproduce. Bless them.
Before the plants go full on and get really into sex mode, you have to bend them over and tie them down.
The change in photoperiod sets off a hormonal change in the plant that will eventually cause it to focus most of its energy on sex organ enlargement. However, the first phase of that hormonal change instead stimulates the plant to suddenly grow taller. The plant can roughly double in height over the first week or two of the reduced photoperiod. This is called "the stretch."
My guess is that if you reproduce by catching sperm in the wind in a forest, your chances are improved if you're above the brush.
What it means for indoor growers is that you're going to run out of room, or your lights are going to burn your plants, or whatever.
So we tie those girls down. As close to ninety degrees as you can manage, which is nerve-racking, but doable.
Cannabis plants are apical dominant, which means that the highest point on the plant undergoes the greatest growth. What is the highest point on the plant when you've bent it sideways? The plant cannot decide, and so it stimulates growth in all of the nodes fully exposed to light (until one becomes dominant).
The point of plant bondage is to continually thwart apical dominance. Every time an eager branch breaks the mold, tie it down. The result of a few weeks of this so-called low-stress training is a shorter, bushier plant with multiple bud sites that will one day grow into giant chunks of weed. If you don't train the plant, then you end up with one giant chunk of weed at the top, and not much elsewhere.
In my case, I was going to run out of vertical space unless I trained the plants. So I tied them down against their wishes.
After a few weeks of reduced photoperiod, I saw flowers beginning to develop.
Those white "hairs" grow long and then fill in at the base with sticky, spongy flower tissue. Then the hairs turn reddish brown and curl flat onto the flower. The result is weed.
I can't show you what ready-to-harvest weed looks like, because around when that picture was taken, the plants began to ail. It's natural for cannabis to shed leaves and show autumn colors towards the end of their flowering period, but these plants were aging too soon, and without having put on any weight of flower.
Then I left the plants for a week without water, and because I did not leave a window cracked open, the building's steam heat cooked the plants to death and dried them to a crisp right in their pots.
Problems began before then, though. At one point, I discovered symphylids crawling through the soil of one of the Grape Ape plants. Symphylids look like centipedes, but rather than preying on insects, they eat the roots of cannabis plants, which then nearly always die. After a soil drench with permethrin, I didn't see them anymore.
When I disposed of the plant roots today, I noticed green pockets of what I assume to be mold.
There was also the problem of low humidity. Here's a picture of a hygrometer in the grow space before I left for a week:
88°F and 26% relative humidity. That's an oven. Flowering cannabis plants like it somewhere in the 70s and 50% relative humidity.
The temperature problem can be alleviated with better air circulation, and the humidity can be increased with a humidifier. Then the problem is that the increased airflow makes the humidifier ineffective. The grow space would start at around 30% relative humidity. Then I would have a humidifier billow clouds of water droplets into the air for twenty-four hours. The grow space would end up at around 30% relative humidity.
I might be able to solve both the heat problem and the low humidity problem with something called a swamp cooler, but really I think that the underlying problem is the location of the grow space. It's at the top of a high ceiling in the back of an apartment with no ventilation, and is under intense light. Even before I had the grow setup, my bedroom could be ten degrees warmer than my kitchen. I would need yards of winding ductwork and a powerful inline fan to move enough air to keep that space cool. I might as well run a snowblower in my bathroom.
Woe is me!
Though this crop was an exercise in waste and failure, it was not without its lessons:
It's all about the roots.
While I mull over what to do about the temperature and humidity in the grow space, I'll start some new seeds germinating.
Better luck next time.
My dad recently pointed out that we're all basically worms.
Niches have filled and tissues have specialized, but the overall body plan is the same. Food goes in one hole, and comes out the other. Fatty outer casing. Bilateral symmetry. Fun sexy bits. Worms.
The body plan wouldn't work in two dimensions. How could you connect the inside parts to the outside parts without blocking either the mouth or the anus? Others have imagined a zipper-like mechanism where the flat animal is zipped shut except for in cavities of food that propagate backwards. I don't know whether nature could produce such an organ. I also don't know whether a universe could exist in two dimensions.
Fortunately, we live in at least three dimensions, and the food that comes out is called poop, except that when it comes out of a worm, it's called "castings." This is strange, because the "cast" left by the worm is the void in the soil due to the worm's having eaten through it, not the resulting poop. My guess is that it sounds better than "poop," and we can't call them "droppings," can we?
I tried composting, it's harder than it sounds.
(1) was a five gallon bucket on the roof. (4) was a loosely fit lid on the bucket. (5) wasn't a problem because I wasn't in a hurry.
(2) and (3) are the hard parts. I inoculated some used potting soil with over ten pounds of spent malted barley from a batch of beer that I had brewed. That's way too much nitrogen. The bacteria went apeshit, liquefied the organic mass, which then became compacted in the bucket, and so went anaerobic, i.e. septic.
I took a peek under the lid about a month later, and there were mushrooms sprouting from the surface. There were also hundreds of flies, wriggling maggots, and various crawling insects.
Yeah, I could have mixed in more shredded cardboard, and I could have stirred the pile regularly to keep it aerated, but who could be bothered? The roof is far.
It's been a good while since then, so that now the compost has mostly stabilized. It smells like ammonia, and the pH is way too high. Never again.
I read an article online that said compost is for suckers. Vermicompost is where it's at.
The pitch goes like this. Why wait months for the simplest known organisms to microscopically chew away at your food scraps? Instead, get something with a real mouth to chew away at your food scraps, right now.
Think about it. It takes your body a couple of days to turn food into brown biomass. Days, not months. But your brown biomass is nasty. Primates are terrible. A red wiggler worm, on the other hand, puts out its entire body weight in poop every day, and the resulting "castings" have no foul odor.
It's not just randos on the internet, either.
Here's one of the bottles of liquid fertilizer that I bought:
It's shouting right there on the bottle:
EARTHWORM CASTINGS & BAT GUANO
The company whose soil I'm using this season, Coast of Maine, also makes a special cannabis-specific potting soil called "Stonington Blend."
This soil incorporates mycorrhizal fungi, kelp, fish bone and alfalfa meal, as well as worm castings, peat, coir and lobster compost. When growing in 15 gallon containers, there is no need for additional nutrients.
Fifteen gallons? I'm growing in a crawl space, people.
Finally, there's a company called Build a Soil that's become popular due in part to a YouTube channel hosted by the company's CEO, where he proselytizes for The Gospel of Organic No-Till Living Soil Cannabis Gardening. I grow in tiny pots of mediocre soil amended with chemical fertilizers, but his insight is still helpful. Here's what he has to say about adding solid organic fertilizer to the top of the soil:
There are many ways to approach top dressing a plant but here is a method that I really like.
For every 5 Gallons the plant is in I would add the following:
- 1 Tablespoons Kelp Meal
- 2 Tablespoons Fish Bone Meal
- 4 Tablespoons Gypsum
- Cover with up to 1/2" - 1" layer of worm castings across the whole container.
- Put the mulch back, or cover with fresh mulch etc.
These people love worm poop. It's all the rage. Let's make some.
The LES Ecology Center will sell you some worms, just call (212)-477-4022 to order in advance and then pick up at one of their farmers' market tents in Union Square or Tomkins Square Park.
I called and they told me that the worms are done this year. Call back next Spring.
So, I ordered worms online. Uncle Jim's Worm Farm will ship you a drawstring bag of 500 worms from Pennsylvania for $45.
You can't just keep them in a bag, though. I bought some more five gallon plastic buckets from the local hardware store, and built a worm bin.
Here's the idea. One of the buckets has a bunch of holes in the bottom and along the rim. You line the bottom with some newspaper and fill it a few inches deep with wetted shredded paper and food scraps. The worms live in there. The lid goes on, itself with holes, and also I put a cut up t-shirt under the lid to discourage fruit flies. That bucket then stacks inside of a bucket without holes, to catch any liquid that gets through the newspaper and out the bottom holes.
Feed the worms at least once a week.
Here's the cool part. What about when you want to harvest the poop from the worms? Do you go digging through the poop, picking out worms with your hands like a barbarian?
No, you don't. Instead, you have another bucket with holes. You put fresh food and bedding into the new bucket and place it on top of the old. The worms, running out of food in their bucket, will wiggle their way up into the top bucket. After a few days, you harvest the castings from the first bucket, dump the contents of the new bucket (worms and all) back into the first bucket, and you're ready to make another batch of worm poop. Brilliant!
The worms arrived just this morning. Here they are squirming in their new home of shredded paper, trimmed cannabis leaves, and spent coffee grounds:
Let the pooping begin.
Over the years I've garnered a collection of somewhat broken old laptops. One has a broken keyboard and no battery. Another has a faulty SSD and a flaky motherboard. Both are slow when running modern systems.
For general computing, though, old laptops have some advantages:
If you have an old laptop lying around that you'd like to turn into a home Linux server that you control over ssh, here's how I set it up.
Download an .iso
file of your favorite Debian-based Linux distribution.
Ubuntu Server is a tempting choice, but if your laptop lacks a wired ethernet
card (like my most recent one), then the server distribution might not have
your wireless drivers. Here be dragons.
Instead, we'll install a "desktop" flavored distribution and strip it down afterward.
I used Ubuntu Desktop 20.04.3 and flashed it onto a 32 GB USB flash drive using Startup Disk Creator.
You need to tell the laptop to boot from the USB flash drive. How to do this will depend on the specific laptop, but it will involve one or more of the following:
With luck, the next boot will start some GUI/shell for the Linux distribution's installer.
Install the Linux distribution. To keep things simple, I configure the main user to have the same name as the main user on my primary computer ("david"). I like short names for the to-be server (e.g. "lenovo" for my old Yoga 2).
Once that's done and you've restarted the computer without the USB flash drive and verified that it boots the system you installed, you're ready to strip it down.
Our first order of business is installing an SSH server so that you can configure the computer remotely. For example, I have mine set up with the lid closed on a shelf somewhere. Don't close the lid yet, though.
$ sudo apt update
$ sudo apt install -y openssh-server
$ sudo apt upgrade -y
Now you can connect to the laptop remotely over ssh (see below). Keep the lid open, though, so that the laptop doesn't sleep. We'll change that later.
Switch to your favorite computer for computing and connect to your
soon-to-be-server laptop. You can use the laptop's local IP address or
hostname (if it advertises one). In my case, my user name is david
and the laptop's name is lenovo
, so:
$ ssh david@lenovo
david@lenovo $ echo 'we are now remoted into the laptop'
we are now remoted into the laptop
It will ask you for a password, and possibly multiple confirmations. The
password is whatever you configured for the user (david
) on the laptop.
The remaining commands in this section are to be run on the soon-to-be-server
laptop, over your ssh connection. I'll use the shell prompt david@lenovo $
to remind you.
If I happen to have the lid open when the laptop boots, I don't want to see
a splash screen. I want to see Linux's log, and then systemd
's once init
starts. Here's how:
david@lenovo $ sudo vi /etc/default/grub
That file might have a variable whose value is set to "quiet splash"
or
similar. If so, remove "quiet" and "splash," e.g. so that value is ""
.
Save and exit. Then, regenerate the bootloader with the modified configuration:
david@lenovo $ sudo update-grub
Now change the "run level" to the non-graphical one:
david@lenovo $ sudo systemctl enable multi-user.target
david@lenovo $ sudo systemctl set-default multi-user.target
Now your desktop Linux will feel more like a server.
Lid open/close events from the hardware are handled by the login service. Let's configure the handlers to ignore the events, rather than whatever they do by default:
david@lenovo $ sudo vi /etc/systemd/logind.conf
That file will have commented-out lines beginning with HandleLid
, e.g.
#HandleLidSwitch=suspend
#HandleLidSwitchExternalPower=suspend
#HandleLidSwitchDocked=ignore
Uncomment them and change their values to ignore
, e.g.
HandleLidSwitch=ignore
HandleLidSwitchExternalPower=ignore
HandleLidSwitchDocked=ignore
Save and exit.
Then restart the service to pick up the changes:
david@lenovo $ sudo service systemd-logind restart
Now you can close the lid on the laptop, and nothing will happen.
It's a pain to type my password every time I want to connect to the server. The ssh server supports a per-user allow list of public keys to permit without falling back to password authentication. That file probably does not yet exist on the server, so let's copy the contents of our public key to that file on the server.
My public key is called ~/.ssh/id_ed25519.pub
. Yours might have a different
name.
david@lenovo $ mkdir ~/.ssh
Run this next command on your real computer, not on the server.
$ scp ~/.ssh/id_ed25519.pub david@lenovo:/home/david/.ssh/authorized_keys
Now you can connect to the server without a password.
$ ssh lenovo
david@lenovo $
Congrats, you now have a server in your house, you budding sysadmin, you.
My next steps are usually to:
crontab -e
).Happy hacking.
How many ways can we search through a tree? Let us count the ways.
#include <vector>
template <typename Value>
struct Tree {
Value value;
std::vector<Tree> children;
};
Visit all of a node's descendants before visiting any of its siblings.
template <typename Value>
const Tree<Value>* depth_first_search(const Tree<Value>& tree, const Value& value) {
if (tree.value == value) {
return &tree;
}
for (const auto& child : tree.children) {
if (const auto result = depth_first_search(child, value)) {
return result;
}
}
return nullptr;
}
Visit all of a node's siblings before visiting any of its descendants.
#include <queue>
template <typename Value>
const Tree<Value>* breadth_first_search(const Tree<Value>& tree, const Value& value) {
std::queue<const Tree<Value>*> to_visit;
to_visit.push(&tree);
for (;;) {
const auto current = to_visit.front();
to_visit.pop();
if (current->value == value) {
return current;
}
for (const auto& child : current->children) {
to_visit.push(&child);
}
if (to_visit.empty()) {
return nullptr;
}
}
}
This required a queue, and did not require recursion.
We could rewrite the depth-first search without recursion. After all, the call stack is a... stack.
#include <stack>
#include <vector>
template <typename Value>
const Tree<Value>* depth_first_search(const Tree<Value>& tree, const Value& value) {
using Ptr = const Tree<Value>*;
std::stack<Ptr, std::vector<Ptr>> to_visit;
to_visit.push(&tree);
for (;;) {
const auto current = to_visit.top();
to_visit.pop();
if (current->value == value) {
return current;
}
for (const auto& child : current->children) {
to_visit.push(&child);
}
if (to_visit.empty()) {
return nullptr;
}
}
}
This visits a node's children in the opposite order as before, but it's still a depth-first search.
This also is technically less efficient than the first depth-first search, because
we copy a node's children into to_visit
before visiting them.
The benefit of this second version is that it can accommodate trees that are
as deep as memory will allow, whereas the first version can only go as deep as
the call stack, which is usually much smaller. Also, a call stack frame is
larger than a pointer, so we use less space by storing only the Tree*
instead
of an entire stack frame.
The breadth-first search algorithm and the second depth-first search algorithm
are very similar. The only difference is in the definition of to_visit
.
Let's rewrite them as adaptations of the same underlying algorithm.
#include <queue>
#include <stack>
#include <vector>
template <typename Value, typename Container>
const Tree<Value>* search(const Tree<Value>& tree, const Value& value, Container&& to_visit) {
to_visit.push(&tree);
for (;;) {
const auto current = to_visit.next();
if (current->value == value) {
return current;
}
for (const auto& child : current->children) {
to_visit.push(&child);
}
if (to_visit.empty()) {
return nullptr;
}
}
}
template <typename Value>
struct Stack : public std::stack<const Tree<Value>*, std::vector<const Tree<Value>*>> {
const Tree<Value>*& next() const {
return this->top();
}
};
template <typename Value>
const Tree<Value>* depth_first_search(const Tree<Value>& tree, const Value& value) {
return search(tree, value, Stack<Value>());
}
template <typename Value>
struct Queue : public std::queue<const Tree<Value>*> {
const Tree<Value>*& next() const {
return this->front();
}
};
template <typename Value>
const Tree<Value>* breadth_first_search(const Tree<Value>& tree, const Value& value) {
return search(tree, value, Queue<Value>());
}
What other data structures could we use for to_visit
, other than a stack or a
queue?
If we don't know anything about the structure of tree
and the values within
it, then I see no reason to use any other data structure in an exhaustive
search.
Still, it's interesting to consider what happens when we use some other data structure.
What about a heap? The next element visited is always the smallest (or the largest) among those to_visit, according to some comparison function.
#include <queue>
#include <vector>
template <typename Value>
struct Greater {
bool operator(const Tree<Value>* left, const Tree<Value>* right) const {
return left->value > right->value;
}
};
template <typename Value>
struct PriorityQueue : public std::priority_queue<const Tree<Value>*, std::vector<const Tree<Value>*>, Greater<Value>> {
// Return the smallest element in this queue.
const Tree<Value>*& next() const {
return this->top();
}
};
template <typename Value>
const Tree<Value>* least_first_search(const Tree<Value>& tree, const Value& value) {
return search(tree, value, PriorityQueue<Value>());
}
This is more costly than the vanilla depth-first or breadth-first algorithms, because operations on the heap (priority queue) are logarithmic in its size rather than amortized constant.
We could also visit nodes in an expanding pseudorandom order by using a hash-based container.
#include <cstddef>
#include <functional>
#include <unordered_set>
template <typename Value>
struct HashValue {
std::size_t operator()(const Tree<Value>* node) const {
return std::hash<Value>(node->value)();
}
};
template <typename Value>
struct HashMultiset : public std::unordered_multiset<const Tree<Value>*, HashValue<Value>> {
const Tree<Value>* next() const {
return *this->begin();
}
void pop() {
erase(this->begin());
}
void push(const Tree<Value>* node) {
insert(node);
}
};
template <typename Value>
const Tree<Value>* bogo_search(const Tree<Value>& tree, const Value& value) {
return search(tree, value, HashMultiset<Value>());
}
The hash is of a node's value, which is strange. Stranger still would be to hash the node's address instead.
Treeeeeessss.......
I can't keep these plants alive. They are very unhappy.
The soil holds too much water. I wait for several days between waterings, and in that time the top few centimeters of soil are bone dry, while the soil below is still moist.
There is an epidemic of fungus gnats. "Just let the soil dry out," they say. "You're just over-watering," they say. "The soil isn't draining," they say. Are you sure? Do you mean to tell me that if you put a plant in soil and water it once a week, it will die from over-watering?
"But the gnats proliferate due to the presence of fungus, and the fungus only grows when the soil is moist too long!" they exclaim.
"There's too much soil! Plants that size potted in that much soil will die from root rot!" is the warning.
Here's a question. Does vermiculite cause water to pass through the soil more easily, or less easily? Does it improve drainage, or hinder drainage?
The answer, as far as I can read, is "both" or "neither." The first couple of pages of google results are all cooking recipe style articles that rephrase, sometimes verbatim, the same unhelpful claims about perlite, vermiculite, what they are, what they're for, how they're similar, and how they're different. If you instead look at message boards or reddit, you'll see claims that perlite and vermiculite are polar opposites — that perlite improves drainage, while vermiculite retains moisture. If you read articles for DIY potting soil mixes, you'll see things like "2 parts perlite and/or vermiculite in any proportion."
I've been spraying a dilute neem oil mixture onto the soil of the plants in an attempt to terrorize the fungus gnats. It doesn't seem to be helping, but apparently this takes time.
My mother advised me to take one or two of the plants out of their current container and plant each individually into a small pot. The goal is to improve soil drainage. I took this opportunity to mix up some new soil, this time with the following ingredients:
It's vermiculite rather than perlite, because the local gardening store's perlite included Miracle Grow fertilizer, and I'm trying to control the "drainage" variable only.
I mixed the soil, roughly, by volume using a quart sized measuring cup as a scoop, and worked the mixture with a big metal paddle and a little wooden spoon until I had broken up most of the clumps. The soil has the consistency of wet sand, but lighter.
The unamended potting soil really is very dense and moist. Probably adding anything (perlite, vermiculite, sand, styrofoam) would improve it. That's not to mention nutrients, which I'm not addressing until I can get the plants to stop dying. Besides, the soil and the compost probably have enough nutrients to get the plants further along into the vegetative stage. I could always supplement with liquid fertilizers later if necessary.
I accidentally destroyed most of the root system of one of the plants that I transplanted today, so that one I expect will die. The other one looked like it was going to die anyway, so I wish it the best of luck.
It seemed like a fun idea to use an old Android phone as a time lapse camera to monitor the growth of the plants, but it turns out it fucking sucks and is a total pain in the ass.
Assuming the camera app hasn't spontaneously stopped taking pictures without leaving a hint as to why, you can see the most recent picture here. There's also a less frequently generated everything-so-far time lapse video.
Here they are inline:
Yes, it's blurry. It focuses on whatever it wants to. I could make the focus fixed, but then the next time I nudge the camera or move anything in the shot I'll have to set it again. Better to let it focus itself on something.
You can see the whole system here.
As you can see, everything was fine until the sudden cut to brown at the 32 second mark. The interim period was a week that I spent at my parents' house, during which the phone decided to stop taking pictures. There's another used camera in the mail that I'll flash Ubuntu Touch onto in an attempt to wrap my own camera intervalometer. Existing Android solutions work very well until they stop working entirely.
Before leaving my apartment for the airport, I watered the plants with a little more water than usual and added in some of the dilute neem oil mixture. After a week of then leaving the plants well alone, you can see that they're all dying.
If the plants recover as is, great. Nothing to do.
If the re-potted plants do better, great. I'll re-pot the rest.
If everything dies, well then shit. I'll buy more seeds, this time keeping each plant in its own tiny container full of super draining soil mix, and I won't water them one fucking drop until they beg for mercy.
It's all explained here.
Go ahead. Click the link, and read all of it. It's not long.
No?
Ok, then. Here are three of the diagrams from the above link.
Notice how, in the second diagram, different trees can share file objects and
subdirectory (tree) objects, if those files and subdirectories are exactly the
same (test.txt
and d8329f
).
That, to me, is what is so cool about git. It's not bag of diffs. It's an immutable directed graph.
The only thing even resembling diffs in git is the packfile that it will create, as an optimization, when the repository becomes very large or needs to be sent over the network. Even then, it doesn't use diffs per se, but a clever encoding that describes objects as data interspersed with excerpts from other objects.
The next time you're thinking of throwing your laptop out the window out of frustration with git, just stop to think how pretty it is.
My first time around I made all of the mistakes, and the plants died as seedlings.
This time, I will make different mistakes.
Start with the seeds.
Three strains:
I dropped the seeds into some tap water and waited for them to sprout. They all sprouted more or less after a couple of days, after which I planted each in a plastic cup of potting soil.
Before long, they all poked out of the soil and started reaching for the lights.
Well, one of them didn't. I went digging through the soil after a few days, and there was no evidence of the previously sprouted seed. There was, however, a very fat looking ant, who seemed relieved to be freed from the soil. She leapt from the side of the cup onto the kitchen counter and scurried towards the refrigerator and out of sight. Did she eat the young plant? Did I accidentally not plant that one? In any case, 8/9 ain't bad.
That's my stove. I bought a dowel, cut it to size with a hacksaw, and jammed it into the hood above my stove. That way I can hang "drop light" sockets from it, and screw in one of the million LED bulbs that I bought on Amazon a while back.
When I cook, I just move the plants onto the refrigerator until I'm done.
At first the plants were doing well; decent foliage and lots of roots.
Not long after, though, I started to notice brown spots on the leaves, burnt-looking tips, and general wasting in some plants. It seemed to get worse any time I'd water the plants. Was I over-watering again?
Google searches only revealed that these symptoms could be caused by anything, and that I should do something now, or else the plants will die, again.
So, I asked for help on reddit. Somebody replied with the following advice:
Ph your water. You’d want it to be between 6.5-7.
Also check if your city uses chloramine and chlorine in their water, which they most likely do. You’d either filter your water or use store bought.
Also, change to non-white cups. Light hitting the roots can damage them.
I’d also change the lighting schedule to 18/6 during veg.
Aha, maybe it's not the quantity of water but the quality.
Another possibility was that the plants had outgrown these cups, and were now near exhausting the available nutrients ("root-bound"). For plants this young in soil this rich, that is unlikely. Still, in addition to taking the redditor's advice, I also planned to transplant into larger containers.
Now I prepare the water for my plants in a pasta pot. Tap water goes in, gets boiled for about fifteen minutes, and then sits out overnight, or longer. Then I adjust the pH using pH test strips and tiny additions of either citric acid or potassium hydroxide. The redditor had a point: my tap water (pure though NYC tap water is reported to be) has a pH of around 8 and is full of chlorine. One pot of water lasts a long time, because don't over-water your plants.
Things were getting cramped growing everything on the stove. I have this storage space above my bathroom, let's use that instead.
I went to the local gardening store looking for cheap plastic pots about a gallon in volume. They don't sell cheap plastic pots (though every one of the hundreds of plants they sell is potted in one). They only sell terra cotta pots for $40 each.
I didn't buy any pots, but as luck would have it, somebody decided to throw away these plastic drawers on the curb right next to the gardening store:
The plants will be pretty cozy, but I think that will work! Just drill a bunch of holes in the bottom.
About an afternoon later, I had the plants transferred into the bins; one bin per strain.
Now for the lighting. At first I thought that I'd string the "drop lights" on thread like a clothesline, or maybe I'd use longer dowels. That would be a pain in the ass. Also, whatever I decide, it will need to accommodate future additions of more lights as the plants grow.
Then I found this cardboard box in my storage space:
I traced out socket-sized holes in the box and cut them out. The idea is that the cardboard will be a "roof" over the plants, with the bulbs coming in the top and pointing down onto the plants.
The weight of the light fixtures might make the cardboard collapse inward, though, so first I tied some chopsticks to the underside to resist the tendency of the box to bend downward.
I have plenty of aluminum foil, so let's add that to the underside, too.
Here's the setup with some bulbs in place:
The cardboard is supported on the right by the plastic shelving from which I took the drawers, and on the left by a wooden step stool that I also found on the curb the same day.
The power runs over the top of the setup and off to the side, where I have a power strip with a timer plugged into an extension cord that runs to the kitchen. Per the redditor's advice, I'm keeping the plants under light eighteen hours per day.
It's all a serious fire hazard. At least it's surge protected.
Here the plants are in place the day I set them up:
The fan on the right keeps a gentle breeze blowing over the plants at all times. The owls keep watch at night.
It's been a week since the transplant, and I think the plants might be recovering. It's hard to tell, because the nutrient deficiency is probably "mobile," meaning that the plant can prioritize healthy growth of new leaves, even as the death knell intensifies.
Here are some blurry pictures from today.
Hang in there, my little green ones!
What's the output of the following program?
package main
import (
"fmt"
)
func foo() (result string) {
defer func () { fmt.Printf("foo result = %q\n", result) }()
return "hi"
}
func bar() (result string) {
defer fmt.Printf("bar result = %q\n", result)
return "hi"
}
func main() {
fmt.Printf("foo() = %q\n", foo())
fmt.Printf("bar() = %q\n", bar())
}
First foo
is executed, which will print something, and then the first line of
main
will print something.
Then bar
is executed, which will print something, and then the second line
of main
will print something.
Here's the output:
foo result = "hi"
foo() = "hi"
bar result = ""
bar() = "hi"
Does the third line surprise you? It sure surprised me. Go ahead, try it yourself.
Reading StackOverflow didn't help. Instead, I had to read the language specification, which is what I ought always to do.
Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked.
The parameters are evaluated when the defer statement executes, not when the deferred function is invoked.
This is confusing, because one way to defer code is to wrap it in a closure
(func
) and invoke the closure later on.
Go's defer
statement is more general. After thinking about it, I prefer the
Go way, except that now I have this hole in my foot.
Be careful out there.
You've been getting away with it for years, and nobody called you out.
Well, I'm here to call you out. Your documentation sucks, because you don't care, and neither does the person who cuts your paychecks.
But I care. I care so much I could kiss you on the lips. Here's how you can improve your documentation.
All you need do is follow these six rules.
Every source repository has a README file at its root that explains first why the code exists, then what it is, how to use it, and finally where to learn more.
Every source repository has a "map" or "tour" that presents an overview of the repository's subsystems (e.g. source, utility scripts, build systems, continuous integration, documentation generation, etc.).
Every subdirectory (every single one) within a source repository has a README file that briefly explains what's in that directory. Sometimes it's a file-by-file summary, and sometimes it's just a paragraph saying why that directory needs to exist (i.e. why the files are there and not elsewhere — what they're for).
Every source file has a section at the top that summarizes what the file is for, what's in it, and possibly other documentation (e.g. example usage).
Every subprocedure (function) in a source file has a contract written in a human language in a comment. The contract will also hint at the reason for the function's existence, e.g. a note about what other code might call the function and why.
Every significant "section" of code within a long subprocedure (function) is similarly documented. Better, refactor it into its own subprocedure.
This applies to unit tests as well. Too often, unit tests are completely undocumented, or are missing motivating documentation at the top. No, the unit tests don't document themselves. Cut the shit.
This also applies to your steaming piles of YAML, Dockerfile
, .gitignore
,
shell scripts, Makefile
, CMakeLists.txt
, and anywhere else the real truth
about your system is hiding, undocumented.
All of this must be kept up to date with the code. That's the hardest part.
Reading prose and then modifying it to say something slightly different, all without making an incoherent mess, is hard. It takes time and concentration and thought and review. It doesn't affect compilation, testing, releases, sales, or promotions. Do it anyway. Don't let it get out of date, work on it! Do it for me.
Here's a checklist to help cover your corner cutting ass.
When you add or modify code, find the nodes closest to your changes, and then for each, trace through all ancestors, checking at each level whether your changes warrant changes to the documentation at that level.
Repo README
/ \
/ \
Repo Tour \
\_____ Subdirectory README
/ |
/ +--- File summary
/ | |
Subdirectory README | +--- Procedure contract
... | |
| +--- Procedure contract
| ...
+--- File summary
| ...
...
Happy coding! Remember, your job as a programmer is to explain systems. First to yourself, then to the computer and everyone else. Don't leave us hanging.
I'm trying my hand at growing weed. It's legal in New York now, so it's a good time to start.
It's a bit late in the season to start, but if one of the plants survives, maybe I'll have some weed by the time it gets cold outside.
The plants started as five seeds that I bought online, $10 each.
I put them in a cup of water for a couple of days. When they began to sprout, I planted each in a plastic cup of potting soil.
That platform is a bunch of chopsticks superglued together.
Two of the plants never "opened up" to produce leaves, and so died. I put the surviving three out back on a stool to grow in partial shade until they were mature enough to transplant into larger pots.
Today I bought $50 in potting soil, more than enough to fill three 5 gallon plastic buckets, and replanted into the buckets. First they spent a little while in the shower, draining water through the soil and out the holes that I drilled in the bottom of the buckets.
Then I lugged the filled buckets up five flights of stairs to the roof. I recently replaced my old broken futon with a new couch, and so I had many slats of wood to work with (well, Ikea "wood"), which allowed me to set up this raised pallet to support the plants and allow them to drain.
It's supposed to rain for the next three days, so that'll be an opportunity to see how the buckets fare and drain. It also means that I won't immediately have to start lugging gallons of water up the stairs to the roof every day to water the plants.
Here's the new couch.
I'll update this post when the plants have grown more.
Here's a table of what I've spent, so far, on this project:
Item | Unit Cost | Quantity | Subtotal |
---|---|---|---|
seeds | $10 | 5 | $50 |
buckets | $6 | 5 | $30 |
soil | $25 | 2 | $50 |
jars | $7 | 3 | $21 |
total | $151 |
Yep, all three plants are dead. Here are some pictures that I took shortly before the end.
Where did the third one go? I found a bird feather in one of the buckets, so I have my suspicions.
There was no saving the other two plants. The issue, I think, is that those buckets are too big for such small plants. When it rains, the soil holds water that the plants can't take up, and they "drown."
So, better luck next year? No, let's try to grow indoors! Check out part two.
Today I brewed a porter recipe from the internet. It says:
The recipe is one of Ron Pattinson's historic recreations based on Whitbread brewing logs.
It's supposedly from 1880 and is called "Whitbread Porter." I'm going to call it "Boxy Brown Porter."
Grain | Quantity | Notes |
---|---|---|
Pale 2-Row | 9 lbs. | |
Brown Malt | 1 lb. 12 oz. | |
Black Malt | 12 oz. | |
Hop | Quantity | Boil Time |
East Kent Goldings (4.4% AA) | 3 oz. | 60 min. |
Yeast | Quantity | Notes |
Safale S-04 | 1 pack dry | stirred into some wort and pitched |
Here's what the strike water temperature calculator had to say:
Grain Weight | 11.5 pounds |
Grain Temperature | 82° F |
Strike Water Amount | 4 gallons |
Target Mash Temperature | 154° F |
Equipment Loss | 0° F (preheated) |
Strike Water Temperature | 164.4° F |
In practice, I might have mashed in a little too hot. It's hard to tell, because temperature readings vary widely depending on where in the mash I stick the thermometer. My notes say that the "mash is somewhere between 151°F and 157°F, depending on where I measure it."
After mashing for an hour and a half (went out to dinner), the mash was "between 149°F and 153°F, depending on where I measure."
An iodine tincture test showed that there was no starch remaining.
I did a fly sparge, collecting seven gallons of wort in about an hour.
[water goes in]
[wort comes out]
I didn't write down the gravity of the runnings.
Don't brew in my apartment during the Summer. The windows don't open much, and the only windows are in the front, so there's no way to create a cross breeze. The window air conditioning unit can't keep up with the stove, especially with two burners going full blast. The result is that the temperature and humidity in the apartment continually rise throughout the boil. It must have been over 100° F and 100% humidity by the time I turned off the burners.
I started heating the wort as I was collecting it during the sparge. My notes say that there was a rolling boil at 8:26 PM, though it was probably already boiling before then. I started the first (and only) hop addition at 8:41 PM, so that's at least 15 minutes of boiling before the 60 minute addition.
I added the 3 oz. of East Kent Goldings pellets (4.4% AA) and boiled them for 60 minutes. Then I turned off the burners, started the chiller, and turned on the air conditioner...
The result was five gallons of wort with a starting gravity of 1.052. If that ferments down to 1.010, which it usually does, then it'll be 5.5% ABV.
The yeast was a fresh packet of S-04 ale yeast, stirred into some wort and then poured into the fermentor.
I affixed a blowoff tube to the carboy and put it into the chest freezer set to 60° F.
Two days later, this is what I had:
Lessons learned:
I cleaned up the mess and replaced the tube with an airlock.
We got 42 bottles of porter, plus one partially filled. Not bad, considering that the fermentor exploded.
The hydrometer read 1.014 at 64° F, which means the final gravity was 1.014, which means the beer is 5.0% ABV.
I primed the beer with 1¼ cups of pale dried malt extract boiled in about a quart of water and cooled.
The beer tastes good flat. Let's give it a few weeks to carbonate before the first tasting.
The time has come for me to blog about my blog on my blog. It's bound to happen eventually with any hand-coded blog.
I've enjoyed the exercise, because the goal is tangible and easy to state,
Create a static website generator suitable for programming and travel blogging.
there is no deadline, I am unconstrained by existing code or people, and it's an opportunity to learn what really goes on underneath premade offerings.
What I've learned is that I'm more of a maker than an engineer, and that I more value what is created than why it is created.
This website is completely static. The server doesn't generate content on demand, and there are no client-side scripts. It's HTML, CSS, and media (images and videos). Nothing else.
If you disable javascript in your browser, the website is unchanged. If you disable CSS, the website looks a little bland, but still fine.
There aren't any cookies or modal overlays or scroll-activated banners or floating menus or asynchonously loaded assets. It works on your browser, no matter which browser you use. There aren't any Google Analytics beacons or off-screen tracking images. You won't be asked to sign in with Google, connect to Facebook, give a thumbs up, or leave a review, and there's no way to tweet, repost, or add a comment.
It's just HTML, CSS, and media. My words into the void.
The configuration language of the site generator is the file system.
.
├── bin
├── config
├── content
├── posts
├── series
├── site
└── Makefile
Pages are written in markdown (.md
) and placed in the content/
directory. Markdown files will be converted into HTML. Other types of files
in content/
will be included in the site verbatim.
The directory structure within content/
can be whatever you like (e.g. to
organize pages with their images in directories).
The posts/
directory contains symbolic links to .md
files under content/
.
Each symbol link is a post, and its name is the ISO 8601 date of the post, e.g.
2021-06-03
for June 3rd, 2021.
The config/
directory contains pieces of the site that don't vary between
pages, such as the style sheets and the HTML of the navigation bar that appears
at the top and bottom of every page.
The series/
directory contains one subdirectory per "series." A series is a
group of related posts, e.g. a series of posts on restoring a car. The name of
each subdirectory under series/
is the display name of the series (e.g.
"Programming Languages"), and the contents of each such subdirectory are
symbolic links to links in posts/
, where the name of each symbol link is the
nickname for that post in the series, e.g. "Day 12" in a series on "Peru Trip."
.
├── bin
├── config
│ ├── common.css
│ └── navigate.html
├── content
│ ├── cheesecake.md
│ ├── i-love-scheme.md
│ ├── in_the_oven.jpg
│ └── yay-python.md
├── posts
│ ├── 2021-06-03 -> ../content/yay-python.md
│ ├── 2021-06-09 -> ../content/cheesecake.md
│ └── 2021-07-15 -> ../content/i-love-scheme.md
├── series
│ └── Programming Languages
│ ├── Python -> ../../posts/2021-06-03
│ └── Scheme -> ../../posts/2021-07-15
├── site
└── Makefile
The bin/
directory contains scripts used when generating the site.
The site/
directory contains the generated static website, where most of the
site's assets are symbolic links to files in content/
and config/
.
To generate the site, run make
. Then you can run a web server with site/
as the domain root, e.g.
$ cd site
$ python3 -m http.server
I deploy the site by deep copying site/
to another directory (no more symbolic
links), and shipping that directory to wherever the blog is hosted.
At first, the code generator was a single python script, bin/generate.
Now it's a Makefile supported by a menagerie of scripts, and as the
final step of the build a descendent of bin/generate is called to
copy/link files into site/
.
The current Frankenstein's monster state of affairs arose gradually, as with all software projects, due to an accumulation of features implemented by an inadequately disciplined developer learning things as he went along.
If I could figure out how to end up with cleaner code, documentation, and tests once I get to this point, I'd be a better developer.
It's a journey! Now on to the details...
The commit history tells the story of features added.
*_small.*
)content/
width
and height
attributes in <img>
tags.Those are just "user facing" changes. There was also substantial toil moving things around, optimizing the generator, writing scripts that generate various intermediate files, changing how the generated site was structured and deployed, and packaging the build process into reproducible isolated environments (like Docker, Guix, and Nix).
Should have just used Wordpress, right?
I'm proud enough to say what I ended up with is better and lighter than other generators out there, but I'm wise enough to know that isn't true.
My static site generator is better than anything else out there for generating my site, and nothing more. If it had to support the myriad use cases of even the smallest popular offerings (e.g. Jekyll), and if my past development process is any indicator of how it'd go in the future, then what I'd end up with is a smoldering dumpster fire of bugs, and I'd end up moving to Squarespace or something.
If I wanted to do the experiment, I'd clean up (read: rewrite) the code and market the generator online as "yet another static blog generator," and either the project would be duly ignored, or unanticipated difficulties encountered while implementing requested features would ignite the dumpster anew.
In summary: Software is hard. To claim anything else is either arrogance or a sales pitch. Still, it's fun. Look, I made a website!
I've been making lemonade. Here's how.
This recipe is based off of (almost exactly) a Reddit post.
Cut all of the lemons in half and squeeze their juice into a pot.
Cut each squeezed half lemon into fourths and put into the pot with the juice.
Evenly sprinkle one cup of sugar over the lemons.
Cover the pot and let sit for thirty minutes. No heat.
While that's happening, bring a half cup of sugar to a boil in a little bit of water in another pot. Let boil for a few minutes, and then turn off the heat and cover the pot.
When the thirty minutes is up for the lemons, add six cups of water to the lemon/juice/sugar pot, stir, and bring to a boil. Let that boil for ten minutes.
After boiling for ten minutes, turn off the heat and strain into the other pot (the one containing the boiled sugar water), removing the lemons. I like to give the lemons a firm poke with a wooden spoon to get any remaining liquid out of them.
Using a strainer, filter the liquid back and forth between the two pots, at least three times (I do five or six times).
Cover whichever pot the lemonade ended up in, and put the pot into the sink surrounded by cold water (and ice packs, if you have them).
When the lemonade has cooled to room temperature (takes a while), add ice cubes and serve with a ladle. Or, scoop your glass into the pot because life is short.
Happy almost Summer.
I'm making a beer for the first time since the end of 2017, so I thought I'd blog about it. This will be the first post in a "beer" series.
I brewed this batch on May 8, 2021. Grace was here and helped out.
I made a big IPA based on the ingredients I had on hand. They're all old ingredients, which might have been problematic, but I think I'll get a solid beer out of it anyway.
Grain | Quantity | Notes |
---|---|---|
American 2-Row | ≈14 lbs. | might have been more |
American Pilsner | 2 lbs. | |
Caramel 40L | ≈8 oz. | |
Roasted Barley | ≈2 oz. | "a handful," for color |
Hop | Quantity | Boil Time |
Citra (12.7% AA) | 1 ⅓ oz. | split: 60 min., 15 min., and 2 min. |
Centennial (12% AA) | 1 ⅓ oz. | split: 60 min., 15 min., and 2 min. |
Cascade (7.3% AA) | 1 ⅓ oz. | split: 60 min., 15 min., and 2 min. |
Belma (5% AA) | 1 oz. | split: 60 min., 15 min., and 2 min. |
Yeast | Quantity | Notes |
Safale US-05 | 1 pack dry | sprinkled directly into wort |
Saflager W34/70 | 2 packs dry | added after ≈20 hours; rehydrated in diluted/boiled/chilled wort |
I used my bathroom scale to weigh out the bulk grains from the bags that had been sitting in my refrigerator for five years. At one point, my body weight (which I was using as a zero for weighing the grains) showed a different value, so that's why I suspect the ≈14 lbs. of 2-row might actually have been more. That, or I got a much better mash/sparge efficiency than I've previously measured.
I crushed the grain out back using my trusty modified Victoria grain mill.
Instead of letting the crushed grain collect in a garbage bag, I cut the bag so that it acts as a chute, and the grains fall into a plastic bucket below. Much better. The grain generated a lot of dust, because it was so old and dry.
I mashed thick, using ≈4 gallons of water for ≈16.5 pounds of grain (just less than 1 qt./lb.). I heated the mash water up to 190°F, and dumped it into the empty mash/lauter tun to heat the equipment, with a blanket on the lid.
When the water had cooled to about 170°F, I slowly stirred the grain into the tun. After stirring and letting the mash sit for a few minutes, I measured its temperature to be between 147°F and 149°F, which was below my target of 154°F. However, when I later checked the temperature an hour later, it read 154°F. I trust the second reading, and so the initial mash temperature was likely over 154°F.
After mashing for an hour, an iodine tincture test showed that there was still starch in the solution, so I added about a quart of hot water, stirred, and let it mash longer.
After another twenty minutes, an iodine tincture test still showed starch, so I stirred and covered again at 152°F. Old grain whose enzymes have worn off? Or did I denature them with a too hot initial mash?
After another thirty minutes of mashing, there was still starch in the solution, but at 136°F I gave up and started the sparge.
So, that's a 110 minute mash from somewhere above 154°F down to 136°F, never showing complete conversion. There is a long fly sparge ahead, though, so maybe I ended up getting all of the sugar after all.
Here's my fly sparging setup.
I don't think there's really any reason to fly sparge. Batch sparging and no-sparge brewing proportedly work just as well at the homebrew scale, but I don't know... I like it. It's the most Rube Goldberg part of the process, and in a nerdy hobby, it fits.
I adjust the flow rate both into and out of the lauter tun based on the level of water above the mash as indicated by sharpie markings on the inside of the cooler.
Time | Level | Notes |
---|---|---|
4:06 PM | 16 cm | runnings at 27°Bx ≈ 1.115 SG |
4:14 PM | 17 cm | let's speed up the flow a bit |
4:23 PM | 16 cm | |
4:37 PM | 14 cm | refilling sparge bucket with hot tap water |
4:42 PM | 16 cm | back at it again |
4:49 PM | 16 cm | runnings at 8°Bx ≈ 1.0318 SG |
By 4:59 PM (about one hour of sparging) I had collected 8 gallons of 15°Bx ≈ 1.0611 SG wort. I was concerned that if I let this boil down to 5 gallons, that the resulting wort would be way too strong. I decided to let it boil off for a while, to gague the boil off rate, and then discard some wort so that the target 5 gallons of wort at the end of the boil had a reasonable gravity.
During the sparge, I collected the wort between two small pots. When one began to fill up, I'd switch over to the other one and then pour the fuller pot's contents into the brew pot. After collecting about three gallons this way, I fired up the burners under the brew pot so that I could get a head start on bringing the total volume of wort to a boil.
The hot break happened around 5:20 PM, about twenty minutes after I stopped sparging. What a mess.
At some point, I scooped a few quarts of wort out of the kettle. Some I saved in a tupperware container for priming bottles, and some I discarded. The wort that I saved was at 1.068 SG.
The recipe started with one ounce each of the four hop varieties listed in the ingredients section. I planned to blend them together and do 2 ounces at 60 minutes, 1 ounce at 15 minutes, and 1 ounce at 2 minutes. Then I wanted to increase the IBUs by upping the addition at 15 minutes, but I only had one ounce of Belma total. So, I added an additional ⅓ ounce of each of the centennial, citra, and cascade hops to the 15 minute addition.
The actual hop schedule was the following:
Hop/Quantity | Boil Time |
---|---|
½ oz. Citra, ½ oz. Centennial, ½ oz. Cascade, ½ oz. Belma | 60 min. |
7/12 oz. Citra, 7/12 oz. Centennial, 7/12 oz. Cascade, ¼ oz. Belma | 15 min. |
¼ oz. Citra, ¼ oz. Centennial, ¼ oz. Cascade, ¼ oz. Belma | 2 min. |
15 minutes before the end of the boil (around the second hop addition), I added the wort chiller to the kettle to sanitize it.
At flameout, I turned on the cold water faucet and continually swirled the chiller in the wort to cool it.
Be sure to fasten the input line to the faucet using a wrench, or it's likely to pop off.
The wort cooled down to 68°F. The original gravity of the batch was 1.082@68°F →1.083 SG.
Before chilling the beer, I noted the level of the wort inside the kettle to be about 5.5 gallons hot. Water has an expansion factor of 1.043 at 212°F, so chilled I had around 5.3 gallons of wort.
Using a sanitized, broken autosiphon, I transferred the chilled wort to a sanitized 5 gallon glass carboy, splashing the stream of wort as it filled the carboy. Then I plugged it and shook the hell out of it for several seconds.
I sprinkled a pack of US-05 dried ale yeast (expired in 2017) into the carboy, fitted a blowoff tube, and placed the fermentor into the chest freezer set to 60°F.
After a day, there was no visible fermentation activity. In retrospect, this is not surprising. A previous batch of similarly strong beer didn't start chugging until the third day. This time, though, I was concerned about the strength of the wort and the viability of the expired yeast. I've read that dried yeast lose their viability at a rate of a couple percent per year, but still...
So, I took two packs of W34/70 dried lager yeast (also expired in 2017), rehydrated them in some of the set-aside 1.068 wort that I first diluted (arbitrarily), boiled, and chilled, and then dumped the whole thing into the fermentor about a day after brewing.
By the following day, fermentation had taken off like a bat out of hell.
Are both yeast strains working on the beer? Is it just the lager yeast? Or maybe it's just the ale yeast, after a two day lag as before? I don't know.
A few days later I replaced the blowoff tube with an airlock, and as of today the batch is still quietly bubbling away.
I plan to bottle condition this beer, priming with the 1.068 OG wort that I set aside on brew day. Maybe I should just use corn sugar instead, I don't know.
The bottles will get these labels, and caps that look something like this:
The labels I'll print out on glossy paper, cut out with scissors, and then attach to the bottles using a glue stick. The caps I ordered from a printer online; we'll see how they come out.
Yesterday I cleaned out the years of boxes, move-in artifacts, and other detritus from the storage loft in my apartment, so now I have a tidy collection of empty bottles that I can de-label in preparation for filling.
The beer is not yet done fermenting, so I'll update this post come bottling day.
Today was bottling day, and we ended up with 48 12 oz. bottles of primed beer at 9.58% alcohol by volume.
We haven't labeled them yet — still need to update the ABV on the label, print them out, cut them out, and glue them to the bottles.
I primed the beer with the unhopped, unfermented wort that I set aside towards the begininng of the boil on brew day. According to the internet, it's risky to do this, because the fermentability of wort varies from batch to batch (as opposed to glucose solution, which is always the same). I used the Brewer's Friend calculator for "gyle." I hope that the beer doesn't turn out flat.
I boiled and chilled the wort before measuring out the amount specified by the calculator and pouring it into the fermentor using a funnel. Then I gently stirred the wort to mix it in, taking care not to splash.
[It's Spotify's "Yacht Rock," I don't know the song.]
Then my little helper and I siphoned the mixture into 48 sanitized empty beer bottles, capping each one.
I no longer use an auto-siphon. The theory is that it was introducing tiny air bubbles during bottling, causing the beer to go stale almost immediately. I also no longer use a bottling bucket, for the same reason. Now it's just a racking cane into a vinyl tube, all initially filled with sanitizer to start the siphon, and off we go.
Did I mention that I forgot to dry hop this batch? I forgot to dry hop this batch! For such a strong, hop-forward beer as this, it's probably a real loss. I'm curious to find out how to tastes when (if) it carbonates. My guess is that it won't taste right until at least a month in the bottle.
In the mean time, I'll work on the labels.
It's not hard, but the HACKING file doesn't cover it all, so here's the rest.
TL;DR: Do the following.
$ sudo apt install git
$ git clone https://git.sv.gnu.org/git/guile.git
$ cd guile
$ sudo apt install -y \
autoconf automake libtool gettext flex \
autopoint libreadline-dev pkg-config libgmp-dev \
libunistring-dev libgc-dev make gperf texinfo
$ ./autogen.sh
$ ./configure
$ make -j 16
$ sudo make install
$ sudo ldconfig
$ guile
Now in more detail.
Here are the dependencies mentioned in the HACKING file:
$ sudo apt install autoconf automake libtool gettext flex
Then ./autogen.sh
wants the following, due to the presence of gettext
:
$ sudo apt install autopoint
I wouldn't want to use a REPL without readline support, so install that, too:
$ sudo apt install libreadline-dev
./configure
will complain unless all of the following are installed:
$ sudo apt install pkg-config libgmp-dev libunistring-dev libgc-dev
Then of course you need make
, but also gperf
(for some reason) and
texinfo
for documentation:
$ sudo apt install make gperf texinfo
After sudo make install
, guile will be installed under /usr/local/
, but
will fail to run until you first refresh the loader's knowledge of where
shared objects are:
$ sudo ldconfig
Finally, guile is ready:
$ guile
GNU Guile 3.0.6.7-3bce5
Copyright (C) 1995-2021 Free Software Foundation, Inc.
Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.
Enter `,help' for help.
scheme@(guile-user)>
If you zoom in (click), you can see a horse above the third post from the left, in the center of the image. My first few days in Aguascalientes, this horse shamelessly presented itself near the fence. I'll take a picture another day, I said. I'm in a hurry, I said. Then the horse disappeared for weeks. Imagine my concern. When it finally returned, it stayed far away from the road. This is the best I could manage without venturing into the pasture. I can't help but wonder what might have been.
I'm sitting in another Aeromexico commuter jet, just having walked onboard from the tarmac. When I first arrived, there was a beautiful sunset behind the mountains on the horizon past the runway, but in my fluster I didn't photograph it. This time around all we get is a view of the dusty distance.
As compensation, here is another sunset.
I invited Grace's parents over to the Airbnb for dinner. Here we are having a dessert of pineapple and mango with chamoy and tajín. The main course was again Grace's sun-dried tomato chicken and roasted potatoes, and my chicken stew. This time I put a little wine in the soup; I think it worked well.
Grace saved the street food for the end of the trip. Last night we went driving in search of "street corn," or "elote," or "chascas." Because it was a notable historical figure's birthday, and because it was Sunday, most of the street corn vendors were closed. But not this one.
I ordered... something. It looks kind of like what I imagine the inside of an animal looks like.
It's primarily corn, but there's also cheese and bacon and mushrooms and stuff there's so much stuff.
As if that weren't enough (Grace got a smaller dish), we ordered tacos when we got back home.
Tacos!
That's about it. I checked my bag this time, because it now contains a cast iron skillet (never leave a soldier behind) and a bottle of tequila. Since you were wondering, here's the accumulation of a month's worth of grocery shopping.
I kept the skillet and the tequila glasses, but the rest is for the next guy. Except the pepper. Grace's mom got the pepper.
We in no way finished the puzzle.
Ahora, a Nueva York.
Cancún.
Can you find the bottled water?
How about now?
How about now?
I didn't know that was there. I thought it strange that the maid ignored my note requesting more water (nor did she take the pesos). The perpetually hot wood should have tipped me off.
It's nice by the beach. The only trouble was that the water was mostly too rough for swimming. The lifeguard wouldn't allow it until later in the afternoon, and even then you were likely to end up with a face full of sand after the surf had its way with you. I think the chop was on account of the wind.
We'd broil on the beach for a while late in the morning, and then head to one of the pools to cool off. Unlimited drinks everywhere. They come around and ask you what you want. Is it noon yet? I'll have a margarita, please. The guy with the box of cigars sells cocaine. You can drink the tap water. I finally finished Mingus's autobiography.
It's "all inclusive," so the only things you have to pay extra for are bottles of wine (the house wine is unlimited), tips (optional), and I suppose the cigars. One glass of wine will knock Grace right on her ass, so we just had what was included. In four nights we never left the resort. There are famous clubs, pyramids, and outdoor activities to be had beyond, but did I mention the pool? Next time.
The portions are small, so order double. You already paid.
We're currently aboard a Boeing 787-8 Dreamliner en route to Mexico City. Then back to Aguascalientes.
You know those mornings when you wake up and ask yourself, "did somebody hire a mariachi band last night?" Such is life. The idea was that Grace's mom would prepare cocktails for the four of us. They were delicious. We should have started much, much later in the day. Next thing I know, Manuel is a couple tequila shots in, talking excitedly on the phone. Next thing I know, I'm playing an ill fated game of chess with Grace's brother (another Manuel, I blundered my bishop). Next thing I know, a family friend is making requests to the group of eight musicians who had been assembled in the backyard. They were quite good. I do not have any pictures. Here is my last picture.
These people are not drinkers, but perhaps I have a way of bringing out the Irish within. More likely, this they deemed a special occasion. My personal impromptu mariachi party. The singers had trained voices, and the trumpets were in tune. Yes, they played "Cielito Lindo." Of course they did.
Ayyyy yaaaay yah yahhhhhhh...
It's laundry day again. This time I included two bath towels, so now all of my clothes are covered in white lint.
Aguascalientes doesn't fuck around with its sunsets.
Yesterday, Grace, her parents, and I visited the town of Calvillo. Manuel drove us down from the stratosphere and across swaths of characteristically Mexican landscape.
Calvillo has narrow streets lined with brightly colored buildings. Some of the streets (not pictured) were steeply inclined and reminded me of San Francisco.
We stopped for lunch at a nice outdoor restaurant attached to a hillside hotel.
For a desert-dwelling Mexican, Grace does not much tolerate hot weather. She prefers a vampiric lifestyle.
I've been eating food, can you believe?
That morning I fried some leftover bacon for breakfast.
Previously, madre Graciela had prepared Manuel's favorite dish for us: chile relleno. They are distantly related to the stuffed bell peppers that my mom used to make, except fried with egg whites and filled with minced meat.
Grace and I cooked a pasta/chicken/eggplant recipe of hers. There is still some left over...
I improvised some chicken-with-leeks-over-pasta dish. I got the wrong cut of chicken from the supermarket, but it was edible nonetheless. Grace had never had leeks before.
I also cooked a big batch of my chicken stew last night, but I don't have any pictures.
There's a tiny sorbet store of great renown near Grace's parents' place. She got strawberry and I got tamarind.
The real culinary innovation of the trip so far is how to eat fruit. In Mexico there are two magical condiments that form a dyad of fruictific perfection: chamoy and tajín. They balance the sweetness of fruit with tart, salty, and savory goodness. Grace's favorite is on mango.
Here she is looking smug about her preparation of pineapple.
Speaking of food, it's one o'clock here and I haven't eaten today. Let's put that microwave to use. ¡Adios!
I haven't done my own laundry in a while. In this arid climate, you hardly even need to hang the clothes to dry.
The salmon came out well. Luxurious though this stove may be, its lowest setting is more flame than I'd like, so I'm learning how not to burn things.
Grace and I ubered downtown in the afternoon to check things out. My favorite moment was when we sat at a small cafe table outside in a "codo" of shops under the shade of some large trees. I don't have a picture of it. I had a small vanilla ice cream and Grace had churros. Grace managed to get pooped on by one of the birds above. She was momentarily inconsolable.
After downtown, we had a 5:00 dinner date with Grace's parents. The menu was carne asada and "tacos gorda," an invention of Grace's. They call her "Gorda" because she was a fat baby, and because Mexicans have a habit of choosing contrary nicknames.
Tacos Gorda contain slices carne asada with grilled buttered potatoes and carmelized onions, together in a tortilla with salsa verde. They're so good.
After some wine and some beer, we switched to tequila. Things got festive. This is how Grace's mother, Graciela, reacts when you point a camera at her and say "¡Oye, bonita!"
Here she is with her husband, Manuel.
Here's Grace and yours truly.
After dinner, we put away the picnic table and moved into the kitchen to play dominoes. Can you believe that I'd never played before? There was more tequila, or as Grace called it, "truth serum." Grace's mom is quick with arithmetic.
The following morning I was "crudo," but the tequila must have been of very high quality, because I felt well by 11 AM, a personal record.
The following afternoon we hung out at Grace's parents' again, this time listening to Grace's newly bought The Police vinyls, and then a round of Pictionary. I like them.
Try playing Pictionary in Spanish.
I will not run out of water.
¡Hasta la proxima vez!
An electric fence tops the walls surrounding my neighborhood. Presumably this is to keep bad people out, but I can't leave unless the man at the gate allows it. Then again, he is unarmed, and short, and friendly.
This is my fourth full day here, and I'm happy to report that I've so far done nothing much. Yesterday we went over to Grace's parents' place for a dinner of sopa de tortilla, and listened to some records in her father's studio man cave. We sipped fancy tequila. Otherwise I've been hanging out at my sunny three bedroom house inside the castle walls, occasionally venturing out to a restaurant or cafe. There's a big screen TV and a 1000-piece puzzle. I'm becoming fond of my morning ritual of coffee and computer in the backyard.
Bottled water is the way to go, but for making coffee you can get away with boiling the tap for a while. Once the water cools, there's a slick of precipitated minerals on top, and it smells slightly of sulfur. No wonder the coffee tasted bland; this morning I used bottled water and it was much improved.
I didn't realize until yesterday that Aguascalientes sits at an elevation of over 6000 feet (1888 meters). I've been to Aspen before, where altitude sickness is unavoidable, whereas here I just felt a little winded and thirsty.
It's a veritable desert, but February is "Winter," so it's not too hot during the day nor cold at night. Don't forget to drink lots of water (but don't drink the water).
My growing list of Mexican foodstuffs:
I also had a chili hamburger that was the fucking bomb. Oh, and an "Italian" restaurant. And Grace's sun-dried tomato chicken with potatoes. I've been eating food, basically. Welcome to Mexico.
This afternoon we'll head to the supermarket again so I can attempt my salmon recipe. Then later we'll take a walk downtown. Originally I planned to stay for a week or two, but now I'm staying for a month. For $40 per day, where else would I be? Hasta luego...
(define sum
(λ (args ...)
(+ args ...)))
(define (sum args ...)
(+ args ...))
(define-as (list 1 2 third _ ...) (something "foo"))
{key value key2 value2}
[value value2 value3]
(let (foo bar
baz buzz)
whatever)
(match value
pattern template
pattern2 template2)
I'm one of those nerds who likes to opine about programming languages.
From an industry perspective, it's an all-too-common Big Waste of Time, but boy is it fun. It's like having strong opinions about fonts, except that the fonts can move mountains. Digital mountains, of course.
The reason for this exercise is to end up at:
$ mkdir solveproblem
$ cd solveproblem
$ vim somefile.language
[... write a small amount of non-arcane code ...]
$ sometool
$ ./solveproblem
$ xdg-open beer
Maybe you're thinking "Perl." I'm thinking more of something:
tool init
, no special .module
, etc.(for/list ...)
is pretty good. Maybe even more concise.#lang
const thrush = (value, func, ...funcs) =>
func ? thrush(func(value), ...funcs) : value;
I was looking up how I've previously used requirejs and noticed the function definition above.
Some call this the thrush combinator, though I don't know if it's really a thing. It has nothing to do with HIV.
The idea is that you have an initial value on which you want to perform a sequence of transformations. For example, in a UNIX shell, you might write
$ echo foobar | tr i x | sed 's/./\0\n/g' | sort | uniq -c | sort -rn | head -1
2 o
In programming languages with function invocation syntax, you might write
const result = finally_this(and_then_this(then_this(do_this(initial_data))))
The order in which those functions are applied reads right-to-left:
initial_data
,do_this
to initial_data
,then_this
to the result of (2),and_then_this
to the result of (3),finally_this
to the result of (4),thrush
allows you to rewrite that expression in the following way:
const result =
thrush(initial_data, do_this, then_this, and_then_this, finally_this);
That looks more like the shell pipeline. It also looks like a sequence of statements in an imperative programming language, where there is an implicit sequence point after each function invocation:
declare initial_data;
do_this();
then_this();
and_then_this();
finally_this();
In programming languages with objects and methods, this is sometimes expressed as a chain of method invocations and called a fluent interface:
const result =
initial_data.do_this().then_this().and_then_this().finally_this();
That's often formatted vertically to give an imperative feel:
const result =
initialdata
.do_this()
.then_this()
.and_then_this()
.finally_this();
Clojure has a macro version of the thrush combinator, spelled ->, and confusingly called the "threading macro":
(def result
(-> initial-data do-this then-this and-then-this finally-this))
Getting back to the Javascript implementation from before:
const thrush = (value, func, ...funcs) =>
func ? thrush(func(value), ...funcs) : value;
Isn't it cool how that works? Maybe it's clearer with less syntax, in Racket:
(define (thrush value . funcs)
(if (empty? funcs)
value
(apply thrush (cons ((first funcs) value) (rest funcs)))))
Yuck, that didn't help at all. Maybe pattern matching will help. How about this?
(define thrush
(match-lambda*
[(list value) value]
[(list value func funcs ...) (thrush (func value) funcs ...)]))
Getting better? Maybe it's clearer as a pattern matching macro instead of as a function:
(define-syntax thrush
(syntax-rules ()
[(thrush value) value]
[(thrush value func funcs ...) (thrush (func value) funcs ...)]))
They're really very similar, aren't they?
This makes me wonder what a horrid affair this would be in C++. Let's try it:
#include <utility>
template <typename Value>
Value&& thrush(Value&& value) {
return std::forward<Value>(value);
}
template <typename Value, typename Func, typename ... Funcs>
auto thrush(Value&& value, Func&& func, Funcs&&... funcs) {
return thrush(func(std::forward<Value>(value)),
std::forward<Funcs>(funcs)...);
}
The auto
function with a deduced return type (i.e. without a trailing return
type) is a C++14 feature. You know, if it weren't for the std::forward
noise, this would be nearly as clean as the Racket code. Maybe we can omit the
std::forward
calls and it will only matter sometimes. What do I know? Let's
see what that looks like:
#include <utility>
template <typename Value>
Value thrush(Value&& value) {
return value;
}
template <typename Value, typename Func, typename ... Funcs>
auto thrush(Value&& value, Func&& func, Funcs&&... funcs) {
return thrush(func(value), funcs...);
}
Hot damn! What about using class
instead of typename
?
#include <utility>
template <class Value>
Value thrush(Value&& value) {
return value;
}
template <class Value, class Func, class ... Funcs>
auto thrush(Value&& value, Func&& func, Funcs&&... funcs) {
return thrush(func(value), funcs...);
}
That's pretty good. The last thing I'd do is make the first overload auto
,
even though it doesn't save us much. It makes the two overloads more
consistent:
#include <utility>
template <class Value>
auto thrush(Value&& value) {
return value;
}
template <class Value, class Func, class ... Funcs>
auto thrush(Value&& value, Func&& func, Funcs&&... funcs) {
return thrush(func(value), funcs...);
}
Not too shabby. Still can't beat Javascript, though.
const thrush = (value, func, ...funcs) =>
func ? thrush(func(value), ...funcs) : value;
What about in Go? Forget it.
They have Jesus in Kerala.
Christianity, Islam, and Hinduism simmer together in the region like mismatched spices in some yogurt dish. There's even a synagogue. And a bronze chicken.
For comparison, here are real chickens.
We began our long descent from the mountains at eight o'clock. Arun insisted that we be ready to go by seven, which we were. With eight milligrams of loperamide in my stomach, I was a rock, a clogged sink.
Arun didn't read into Vineet's regular requests to turn down the music, but I didn't mind. It was like a cooking show travel montage.
We stopped for breakfast in a homey outdoor cafe. I ordered the smallest thing on the menu.
I liked the place. Also, I found myself liking the people in Kerala generally. It was probably because they were not yelling at me to give them money. Or maybe they're better looking. The cashier girl at the airport KFC just would not stop smiling.
Have you ever wondered how to tell if a restaurant is classy? I've developed a diagnostic that you can use. If there is a sign in the bathroom forbidding vomiting in the sink, then the restaurant has at least two Michelin stars.
My pancake volcano caused no discomfort.
Car hopping from place to place to see the sights worked for Delhi, Agra, and Jaipur. In Kerala, however, it seemed excessive. Aside from sleeping at night, we spent by far the most of our time in the car going somewhere or the other. Six of us and the driver, hobbled into a four wheel drive barreling around mountains on partially paved, nearly lawless roads. You'd be hard-pressed to read a book or take a picture of something out the window. But with Bollywood blaring, it had its own charm.
The better way to do Kerala is slowly. Our hotel offered complimentary bicycles. There are interesting flowers and critters everywhere. It's quiet.
Things finally slowed down when we reached the backwaters of Alleppey.
There we boarded a big ol' houseboat just for us six and the three crew.
There was a helmsman,
a cook,
and one other crew member who I didn't see much of.
Here is a small diaspora of Australians. From left to right it's Shaun, Janice, and Kho Yin, who they call Kho.
When not wrestling crocodiles or boxing kangaroos, some Australians do yoga.
The houseboat cruises down the river, and everybody just hangs out. Later that evening we played charades on the upper deck.
Before dinner, we stopped at the shore of the river to take a walk and see the sunset across a rice field.
The cook prepared excellent food for us. It was "home style" Kerala cooking, rather than the restaurant buffet northern Indian food we had been inundated with until that point. He would occasionally bring out coconuts for us to drink from, or pineapple slices, and plenty of masala chai.
I think Janice took that photo.
While we were charading upstairs, a crew member discreetly turned on the air conditioners in each of our rooms, so that when we retired at ten o'clock, our beds were comfortably cool.
I woke up early, but not as early as the crows. Morning on the river is for the crows. I observed them from the upper deck until the Australians arose.
Without a beast in sight for wrestling, more yoga was sought as a substitute.
The rest of the morning was another leisurely cruise down the river, until we once again reached the shore and were picked up by our driver.
It's a couple hours' drive back to Kochi, where the airport is, but we had more time to kill, so on our last day in Kerala we did some more sightseeing. This included a very full museum of stuff including prehistoric cutting tools, nude portraits, bronze chickens, life-sized crucified Jesuses, and a pantheon of Hindu gods carved into an ornate wooden ceiling.
Then we went to a fishery where nets are lowered into the water on giant levers. After a couple of minutes, the lever is pulled upright again, and the resulting catch hangs in the net just above the water. Or, as was the case demonstrated to us, nothing ends up in the net just above the water. Fishing season is not until later in the year.
These are called Chinese fishing nets.
We then stopped by God's house to say hello.
No photography inside God's house. I recognized all but one of the depictions inside. There was of course our boy J-Unit, his mom, his baptism by John, etc., etc., but also there was this Hindu-looking guy on a horse. I didn't learn about him in school. Dude on a horse, in a church. He was depicted in both the smaller old chapel and in the cavernous new one.
Our last stop was a very fancy shopping mall. I had chicken fried rice and Vineet got some new kicks.
After the mall, Arun dropped us off at the airport so we could catch a direct flight back to Delhi. I cabbed with Asha and Vineet, since I would stay the night at a hotel near where they're staying, or so we thought. We accidentally booked the wrong hotel, and so made a couple of last minute re-bookings from the cab after midnight, which added to the flavor. The keeper at our first stop recommended we go across the street, where the air conditioning is working. After tipping our patient cab driver, I gratefully accepted a room with a working air conditioner, hot shower, and flushing toilet. Not sure exactly where I was.
The next morning, my pocket elephant alerted me to a possible source of water damage. Condensation from the vents of the air conditioner dripped onto the television and into the cable box. Using his trunk as a tiny siphon, the elephant carefully moved the puddle into a drinking glass, and then, bending forward like a beast of burden, pushed the glass with his head until it was positioned below the leak.
I showered and then went down to have the complimentary breakfast, which was fine. There was a man with his two kids, and then later two Indian business travelers came in speaking English.
The rest of the day I spent hanging out at Vineet's parents' place. I sat on their couch working on this blog, learned how to make tea the Ahuja way, and later met Asha's friend and former coworker Melissa, who would continue to travel around India after we left that night.
We got lunch with the Australians one last time, walked through a neighborhood market near Vineet's place, and later that night departed for the airport.
I woke up at around eight o'clock AM Delhi time, Friday, October 4th, and it is now six o'clock PM New York time, Saturday, October 5th. Don't be tempted by night flights, people.
That's it for my first India trip. I think I'll return someday, next time adjusting a few knobs: fewer tours, more people; less driving, more wandering; less airport, more nature.
One of the unsung advantages of having acute diarrhea is the ability to use the little flush button instead of the big one. As you can imagine, I've been spending plenty of time reading about the properties of loperamide. For example, did you know that in addition to slowing the activity of the bowel, loperamide also "increases the tone of the anal sphincter"? It's not as exciting as it sounds.
I'm grateful for the chemically enhanced tone, though, because I think that I have never had an experience more primal, more urgent, more deeply spiritual and foundation-shaking than the five hour ride from the Kochi airport to our hotel in the mountains. I discovered new things about nature, pain, control, perseverance, the human condition, and my anus.
I regretted not having packed a spade (or a gun), it was that bad. I'm currently rather medicated, but I think the worst is behind me anyway. Enough about that.
I'm on a boat! But first, the mountains.
That's the view from my room. My room. Here's the view from the shower.
Yes, the shower. Maybe it's so that you can get natural light into the bathroom during the day, or maybe people in Kerala like to watch each other wash.
We got a tour of a tea factory, but photography was not allowed inside. It's a shame, too, because the facility was way cool.
Next to the factory was a "museum". The items exhibited included:
What, you think I made one up?
There were some nice views from the factory grounds.
With Asha for scale.
On the way to the hotel, we stopped for lunch at an outdoor restaurant.
Was I inoculated here? Or was it the dumplings I had some hours earlier at the airport (they at least were freshly boiled)? I don't know, but when the owner saw my confusion as I looked at my plate, he asked "serve?" I said, "no, I need a fork." This seemed new to him. He hurried to the back and I saw him digging through something and then what looked like vigorous rubbing. Should have eaten with my hands.
Munnar is a heavily wooded jungle-like mountainous area. The roads are treacherous and the beauty is extreme. Clouds float at arms length in the valleys below, and occasionally rise to engulf everything in a thick fog, which depending on whether the sun is shining, is either a cool mist or an intense sauna.
Here is our driver, Arun, and one of our traveling companions, Shaun (and some other guy):
Arun stopped at a couple of waterfalls by the roadside.
Leave no stone untrashed.
The area surrounding the hotel is all tea plantations.
That's what tea looks like. Now you know.
One of our activities was to walk up a hill in a national park. Something about an endangered antelope deer thing. Didn't see any. It was raining, though, which allowed more interesting pictures.
Bear in mind that if I were to relax even for a moment, dark toxic sludge would explode into my pants.
Here's a cat.
Our last stop was fittingly a dam about to burst:
We got back to the hotel before 7 PM, and I immediately slept. I woke up at midnight, drank some water, and slept again until 6 AM.
The view in the morning is even better.
Then it was another five hour drive, this time into the backwaters.
He didn't get left behind, after all. It should have occurred to me earlier to search for the marble elephant in a marble room.
Asha and Vineet are now married. They were already married, but now they still are.
Here was my view of the beginning of the ceremony:
After a while, the staff began bringing in chairs, so by remaining standing I got a better view of what was going on. Asha and Vineet were sitting on a bench opposite a small fire. On the other side was a priest with a microphone.
I should mention that the wedding couple are not visible in the video (they're off to the left), and that the shaking image is due to coffee.
This ritual likely had a deep significance for a few attending, but not for the bride or groom.
At Indian weddings, women have henna drawn on their bodies in a style known as mehndi. Here is Asha the bride:
Here is Asha the cat:
The mehndi artist applies the henna as a paste, carefully decorating the skin as a pastry chef would.
Immediately after, it looks like this:
The women then are unable to use their hands for well over an hour. It was hilarious.
After the henna has dried and been scratched off onto a torn piece of newspaper, the stained skin starts to gradually darken from a iodine orange to a deep red brown. Here's Andrea a day after:
Speaking of Andrea, check her out. She borrowed this sari (saree) from the groom's mother.
I was wearing my blue suit. You've already seen it. If you really want to see more, then I refer you to any of the ten million photographers hired for the wedding.
Oh, I just remembered the band that played during the first event. They were told to turn down on at least two separate occasions, but the Force was strong with them.
Later that evening, a different band played Hotel California. The guitar solo was on point. The vocals were better than I could do.
Here's what you can see from my room:
and here's what you can see from the pool:
and that's about all I shot.
Reading the complimentary Hindustan Times this morning over breakfast, I was surprised to see no fewer than three rape-related articles. Now, I know that newspapers tend to focus on the morbid and sensational, and that 1.3 billion people live in India, but still:
In the novel I'm reading, a shelter for battered women and themes involving abuse, rape, and incest feature prominently. I'm not sure what all this is trying to tell me.
Here is a marble elephant.
At eight o'clock I'll be meeting a new friend for dinner and then getting up
rather early tomorrow for a flight to Kerala, where the second half of
this trip begins.
I promised you a cow. You know, I won't be eating them for a while. It's just not right. Well, I might have eaten some of them today. More on that later.
It wasn't just one, either. There are at least three cows in India. I saw one chewing on a newspaper from a garbage pile. Some are employed as roadside landscapers.
In the morning, we got into Mr. Singh's car for the considerable drive back to Delhi. The road is farms and truck stops across the semi-arid landscape.
Unlike in Agra, these cows have impressive horns.
At the halfway point, we stopped at a roadside hotel with an attached café. The owner (the man was, as I later found out) has a family member in tech at Morgan Stanley, and his son is studying "big data" in Kerala. Andrea had chai and I had black coffee. She got some chocolate covered vanilla wafers, and after my thinly guised entreat, she gave me half of them.
I'm listening to Blind Lemon Jefferson's recording of "Black Snake Moan." It nearly fits the mood of the sleepy highway outpost, with its neglected playground and bovine passers-by. It seemed that Mr. Singh has a longstanding working relationship with the owner. On the one hand, the professionals we met on our tour have done this a million times and know what gets a good response from us the customers. On the other hand, I sometimes thought we were a resource being passed through a quiet network of receivers.
We got back to Delhi just in time, as my bladder was about to explode. They gave us a room with two beds. They had guilty looks on their faces and upgraded us to a larger room, with a refrigerator. Serves them right. Finally somewhere to put the cheap champagne that Mr. Singh gifted us.
It's not really champagne in any sense of the word. It's twelve percent alcohol by volume, is probably derived from grapes, and contains carbon dioxide, but aside from these things it more resembles fortified Kool-Aid. I will drink it.
At 4:30 we were back in the car on our way to a walking tour of Old Delhi's street foods. Our guide was an adorable college student from Bangladesh or a nearby Indian territory. She's studying creative writing. She said that among the three languages in which she's fluent, English is "the best." She prefers to travel alone around northern India, because she tours so fast and rough that she has yet to find somebody who can hang with her.
We ate a lot of food. Andrea and I skipped lunch, but it didn't seem to help.
I'm not moved by food. That's not to say I don't enjoy good food, which it all was. It's just that I have trouble focusing on food.
That being said, we sampled quite the spread as we squeezed through the crowded streets of Old Delhi, sometimes hopping on a rickshaw to reach another neighborhood. We had:
We began with another Sikh temple, and along the way there was a quiet street of Jain households. After kulfi we rickshawed back to the car, where our guide seemed oddly interested in keeping far away from our driver.
So ended the street food walking tour.
On the way back, we stopped to get some cash for tipping Mr. Singh, as it would be our last drive with him.
Thanks for the jungle juice.
Copper (II) sulfate is the product of the reaction between copper metal and sulfuric acid. It's water soluble and moderately toxic.
Copper (II) oxide is the result of the breakdown of Copper (I) oxide in humid air. It's not particularly toxic, but regular exposure can cause copper to accumulate in the body, eventually leading to neurological, liver, and kidney problems.
According to the 2018 New York City Drinking Water Supply Quality Report, the average concentration of copper is 0.008 milligrams per liter of water, taken across 464 samples. However, the majority of copper contamination in New York City comes from copper pipes in buildings, while the water report samples at the mains. Separate, smaller samples (481 of them) are taken "at the tap." The finding is that concentrations of copper in drinking water can be as high as 0.483 milligrams per liter of water, though ninety percent of samples are below 0.185 milligrams per liter.
Anyway, here are some pictures of the Taj Mahal:
You get the idea. White, pretty, enormous. Totally my type. We even saw some monkeys:
I said, "oh, more monkeys!" Our guide for that day replied, slightly amused, "Yes, more monkeys. Don't go near them."
The Taj Mahal is in the city of Agra, which is not Delhi. So, Mr. Singh drove us for a few hours through the Indian countryside. You can probably guess from his name that Mr. Singh has a beard and wears a turban, which makes him easy to identify though a windshield. He could be a tour guide himself (as Andrea noted), not just a driver, for all he told us along the way.
Indians are really into cows. It makes me feel bad about eating them (cows, not Indians). As we entered the open country, I began to notice tiny thatched huts spotting the farmlands. They seemed much too small to be homes, but maybe they were sheds. Mr. Singh explained that this is where cow manure is stored until it dries into a hard cake, which can then be used as fuel. Gas fires are unnatural, while this fuel is manifestly natural, and so it is preferable. I don't know whether it smells like manure when it burns, but it probably doesn't smell like hickory.
Once this was pointed out to me, I was floored by number of these huts. Later, when we were driving through a town, we could see large cylinders of the stuff rolled up on the side of the road, in its marketable form.
It's not just the poop, either. Cows are everywhere in Agra. If a cow wants to lie down in the road, it lies down in the road. If a cow wants to pick through garbage, it picks through garbage. If a cow is crossing the road and decides that actually it wants to go back the other way, it does so. They don't moove in response to car horns as reliably as dogs do, so our driver mostly just steered around them.
Small herds of cows are led each day on the side of the road from the farms into town, where they're milked early in the morning, then loiter around blocking traffic and sniffing the ground, and then return to the farms to graze until they are milked again in the evening.
Women squat in the fields collecting the dung, and a man is often nearby "helping" (Mr. Singh says that the piles can get heavy, and so maybe the man is there just in case he is needed, should the woman call in an upper body strength strike).
You want to see some pictures of cows? Well, I'm as surprised as you are that I don't have any. In India so far I have taken more pictures of pigeons, the undisputed rulers of New York City, than I have of Hindu's sacred animal. I'll try to correct this before returning to Delhi.
We saw more than the Taj Mahal in Agra. The first tour was of the so-called "mini Taj," properly the tomb of I'timād-ud-Daulah. You can read about the history on wikipedia. The thing to note in the pictures is how the white marble is integrated with the red sandstone. I can no longer think of the terms "white marble" and "red sandstone" without an Indian accent.
The Mughals were trying out the whole "white marble" thing in the tomb of I'timād-ud-Daulah. It ended up looking so badass that, when the Taj Mahal was built, they went balls deep with the marble. The marble was barged over from Jaipur on a river.
Here's the "mini Taj."
There are two styles of ornamentation at play. One is inlaid stonework (pietra dura), and the other is mural painting. In a similar way to how the Taj Mahal shed the red stone preferring marble, the Taj Mahal also is relatively devoid of murals, instead using extensive pietra dura.
But that's all pretty boring, isn't it? Check out these camels.
As we approached this caravan from afar, Mr. Singh said "look, elephants!" After a few seconds I contributed, "they're very small elephants," and I don't remember if it was Andrea or Mr. Singh who first admitted that they're camels. The lesson here is that you should never mistake a camel for an elephant. Thank you for listening.
My favorite picture so far is this one:
Rashād watches over all of us from his kingly roost. HDR is really growing on me.
Speaking of elephants, here is what they look like:
Yes, we exploited the elephants. I'm sorry, ok? To be honest, had we known that the item on the tour mentioning elephants was a touristastic ride up a hill, we would have left it out; but, the tour is a package, and who knew what the elephant part was about?
Also, what have you done to protect the well-being of elephants? You're a hypocrite, and you know it, and that's why you're going to hell when you die.
If you're not sure what I'm talking about, then read this kind of stuff until you're crying.
For what it's worth, Mr. Singh and our guide claimed that the use of elephants for tourism in India is regulated by the government, that each elephant is limited in the amount of work it is allowed to be made to do in a day, and that everything is really just fantastic. On the other hand, Mr. Singh also said that car crashes are relatively rare in India due to the low speed of travel, but in actuality vehicular deaths are four times more likely in India than they are in the Americas.
I'm just saying that I wish well for the elephants. Moving on.
Ok, one more thing about the elephants. Two things. The first is that Andrea had the sensibility to greet the elephant once we dismounted it, and earlier asked the mahout what the elephant's name is, how old she is, and how old the mahout is. The pair of them were twenty and twenty-five years old (I don't remember who was older), and the elephant was named something that sounded like "Sonya." I looked at the elephant's face when Andrea bent down to meet her, and I couldn't read her expression. Andrea said something similar afterward. Do I sound guilty? I feel guilty. In fairness, elephants did not evolve facial cues to be understood by untrained humans.
The second thing is the picture above. When you're swaying back and forth on the saddle strapped to a moving elephant, photographers try their best to get a decent shot of you, and then presumably later pressure you to pay them for the photographs. We suffered no such trouble, but once we got to the top, I was amused to get the "behind the scenes" shot.
Here is a picture of doorframes framing door frames.
Here is a picture of someone taking a picture of doorframes framing doorframes.
We need to go deeper.
Here is a picture of a blog post being prepared about a picture of someone
taking a picture of doorframes framing doorframes.
Here is a picture of Asha the cat.
We saw a bunch of palaces. The Mughals moved here, and then they moved there, and then they moved back again, and then a prince killed his brothers and imprisoned his father and somebody built the Taj Mahal. I'm probably getting some of this wrong. People died, it was all deadly serious. Also, harems.
The first palace was inside of a fort. If you look closely, there are cows.
Inside of the fort, there are multiple layers of defensive obstacles. There
are attack monkeys,
boulder corridors of death,
and arresting views of the vast, lush landscape that would pull invaders into
a state of placid reflection, whence they can be slain by arrows or boiling
oil.
The guy in the picture was our tour guide in Agra. He was alright. I found him off-putting in a subtle way. There was a rote thinness to his presentation, and he more often deflected questions than answered them. Andrea suggested that he was acting like a salesman who isn't selling anything. I thought that he was crushed by the monotony of saying the same things in the same way in the same places nearly every day for years and years. Sometimes, after an energetic monologue, he would turn away and sigh.
The fort palace has three notable sections. The first is the courtyard where the king would address a private audience. It's real fancy.
The second section is the king's private quarters, which is even fancier.
The last section is the courtyard where the king would address a public audience. I'm not sure what is meant by "public."
One of the kings married three wives: a Muslim, a Christian, and a Hindu. The Muslim wife lived in a lavishly decorated shoebox. The Christian wife lived in a small flat (I found dubious the tour guide's claim that the hallway's entrance into three rooms signified the crucifix).
The Hindu wife lived in a palace of her own within the palace. She is also the wife that bore the king fourteen children, so they were definitely doing it. The other token wives did not bear the king any children.
Here are some pictures of the palace in which the king, his three wives, and a ton of other people temporarily lived while away from the fort.
These tours do this thing that's new to me. Mr. Singh motivated it for us on the way from Delhi to Agra. You go to a showroom adjacent to a factory where some good is made, such as marble pietra dura, textiles, or jewelry. In the showroom there is a small sample of workers making the good. A salesman shows you how the good is made, offers a sometimes-interactive demonstration of the process, and then takes you to a proper showroom where you receive a full length infomercial about everything you can buy there, and then if you like you can buy things, all the while attended to by a very dedicated salesman.
I was reminded of the scene in Indiana Jones and the Last Crusade where the Nazis try to bribe the local ruler with a chest of gold and silver treasures, coins, jewelry, and gems (perhaps all stolen from wealthy Jews), but the ruler is much more interested in the car that the Nazis drove there.
I couldn't get myself to buy anything on any of those occasions, but Andrea did pick up some marble, and as a result, the salesmen gifted me a rough-hewn marble elephant (methinks from the reject pile). So, I have a souvenir for myself, anyway.
Ah, I think I left the elephant at the hotel room in Agra. So it was with me but for a short time.
Mr. Singh recommended a buffet style restaurant for dinner, and drove us there. Andrea and I assumed that he would be joining us for dinner, but he did not. Maybe we should have invited him to stay. The "appetizers" presented to us constituted at least a two course meal, after which we were supposed to go up to the buffet and get more food, and that's not to mention dessert. I lightened up after a beer and a half, fortunately.
The next day began early with a cloudy sunrise tour of the Taj Mahal, which I've already shown you. Later we bid our mediocre tour guide farewell and hit the road again with Mr. Singh, this time for Jaipur.
In Jaipur I was reminded that I have a condition known as resting bitch face. On a long high school marching band trip bus ride, one of my closest friends at the time scrutinized me in silence during a pause in the conversation and felt the need to ask, "are you ok, dude?" I turned to him and said, "yeah, I'm fine." He said, "because you look pissed." I thought about it and told him, "I'm just tired." He looked at me slightly incredulously and said, mostly to himself, "so you look angry when you're tired."
In fact, it's not only when I'm tired. It's any time I make no effort to convey emotion using my facial muscles. Also I hate everybody.
The friendly and slightly avian jewelry salesman told me point blank, "smile, won't you?" I smiled and told him that we were tired.
For example, behold this impromptu pout:
The elephants were leading us up to a palace on a hill, my favorite palace of the trip:
Our tour guide in Jaipur was much better than the previous. It was a state secret how that silver shone for so many years without tarnishing. Such secrets were kept by a family of artisans. A metalworker would not even tell his own daughters the secrets, since then when she was married she could spill the beans to another family. He would tell his daughters in law, though, since they were more part of his family, at least politically.
The secret is that it's not silver, it's a tin amalgam. Surprise! Andrea wondered whether working with the mercury caused health problems, and so I told her about the mad hatters.
There were many things to look at.
Does the following building look to you like it's partially submerged?
There's an 18th Century observatory in Jaipur, where giant instruments are made of stone. For example, this building is a sundial. It is a sundial.
The triangular steppy thing casts a shadow on the curved marble thing, and on the latter are marked two-second increments. On a morning with good direct sunlight, you could watch the shadow crawl across the ticks in real time.
Here is an image of the dome of the night sky, carved in stone:
There are two versions, each omitting half of the sky to make room for astonomers and/or astrologers to stand and take measurements. Simple solution, no?
A similar thing happens here. A tall poll to the right casts the shadow of its tip on one of the little rectangles. As the sun moves across the sky, you can plot the curve of its transit, and from this deduce the current inclination of the Earth in the plane of the solar system, and probably a bunch of other things.
Next we went to a museum on the grounds of the current royal family of Jaipur, but we wouldn't take pictures inside. Nearby, there was this:
I forget what it is, but it looks cool.
We were thoroughly pooped when we returned to the hotel late that afternoon. They also moved us into a room with two proper beds, so Andrea could end her stay on the cot from the previous night.
Today is a down day. We might do something this afternoon, and we might not.
There is a nice pool. Tonight is a cooking class.
This is Aniruddha Ram Kumar Mukherjee. He is a dog. Like many natives of Delhi, Aniruddha must find ways to cope with the oppressive afternoon summer heat. A quick nap on a bench can help pass by the hottest part of the day.
Fortunately, since it is late September, Winter is arriving and so temperatures are beginning to fall. Still, Aniruddha and his large extended family of conspicuously similar looking and ubiquitous canines seem hardly to ever move, preferring instead to lie out and sleep in the center of lawns, hills, sidewalks, roads, and crowded temple entrances.
I found myself thinking that perhaps it is the same dog, silently following us when we're not looking and then plopping down onto the ground apparently fast asleep as soon as we make a stop. Or maybe this dog possesses special powers that allow it to be in multiple places at once or maybe it's no dog at all, but a corporeal projection of some supernatural essence just beyond our comprehension, reaching out to us from the great unknown in the only way that it can: this looks like a nice place to nap... my back is itchy... do you have food?
Aniruddha Ram Kumar Mukherjee in particular (as many share parts of his name) spends his afternoons near the bathrooms outside of Humayun's tomb, an impressively symmetrical example of Mughal architecture (the tomb, not the bathrooms — though the bathrooms were nice, too). It looks the same from all four sides, such that when you walk out of it you don't know which side you're on. You must travel with somebody who knows which side you're on.
Inside of the tomb are nooks and crannies, domes and doves and a human thumb that kept photobombing me. There are no dead bodies, though. Those are allegedly kept underneath the tomb, off limits, to be sanctified by elected officials at regular intervals.
The grounds surrounding the tomb are a grid of sixteen patches of lawn landscaped with lime trees, jasmine, shrubs, one of Aniruddha's cousins, and my thumb.
The trip started out on one of Emirates' Airbus A380 super mega mondo grande venti mocha soymilk airplanes, or as it's referred to in the industry: "that big fucking plane." The plane is so big that if you're sitting over the wing, as I was, it's hard to see anything but the wing. Here is the glow of the sun as it rises over the Atlantic well into the flight, but before we got to Europe. It's strange how the day fast forwards as the plane races east at a significant percentage of the Earth's velocity at the surface.
Supposing that the Earth is a uniform sphere (it's not), rotating evenly about a single axis (it doesn't) at 1000 miles per hour (not quite) at the equator, then the surface of the Earth at forty-one degrees latitude, where New York City is, is tearing through space at 755 MPH. The Airbus A380 has a design cruising airspeed of mach 0.85, which is 652 MPH, so assuming that we were flying through a body of air stationary with respect to the ground (unlikely), along New York's line of latitude (we weren't), then we would have been experiencing the passage of the sun at 186% its normal rate, which I think is a pretty good deal. Also I had the whole row to myself, so I could lie down and torture everyone with my snoring.
That flight was twelve hours long. We were late getting out of JFK, and so we had to hustle our way across the Dubai airport to make our three hour connecting flight to Delhi.
Now in a state of near delirium (our mental state, not the state of India), we were picked up by a friendly and attentive hotel representative and driven to the hotel by a quiet and apparently reliable driver. Once at the hotel, the representative gave us a complimentary welcome cocktail and a play by play of everything that we'd be doing on the tour, and when. He also mentioned that Andrea and I would be sleeping in the same bed. I told him that "we don't typically sleep together," and so he spent the next few minutes going back and forth with the management seeing whether any two-twin rooms were available. There weren't. However, if we wanted they could wheel in an extra twin bed for us, which I volunteered to sleep on. So, Andrea gets the big bed.
I'd like to mention that this is the second time in less than a week that I have been sleeping on an auxiliary bed. Look, I get it, people. I get it.
That evening Vineet (the groom) reached out to us and invited us to come over to his place for dinner, to meet his father and hang out with his mother and Asha (the bride, not the cat). Here is a picture of Asha the cat:
Andrea and I nearly fell asleep at the dinner table, and so we politely ate and ran to an Uber who "had a GPS malfunction" and so took twice the time to get us back to the hotel, where we passed out immediately.
The following day (which is now ending, from my reference frame) we met a tour guide and the driver from the previous day, and they took us all around Delhi to see the sights. One of those sights was Humayun's tomb, where I met Aniruddha Ram Kumar Mukherjee.
It's very convenient to have a private tour guide and driver. They do all of the things and you just have to walk around, take pictures, and ask questions.
I took few pictures of the sights, because I was mostly seeing them, and because Andrea was carrying around a nice SLR, the contents of which I can later mooch off of.
I'm not going to narrate the whole tour (if you want that, I encourage you to visit Delhi for a day), but here are some of the pictures. Also I'm tired and want to go to sleep.
We started by visiting a large Sikh temple, which in actuality is just a giant family style restaurant, pool, and concert venue. No shoes required. That is, you are required not to wear shoes. The cooks don't wear shoes either.
Also, you must cover your hair, because that is how the Sikhs do. There's this guy:
and then there's Andrea:
Young Indian men kept coming up to Andrea and asking whether they could take a picture with her, but I have yet to figure out why.
After that we walked around a big old fort/palace, "the city within a city"
Here is a picture of where Gandhi was cremated:
Here is another tomb:
Here are two of my fingers, with a tower in the background:
Here is the tower closer:
Here are two of my fingers, a woman, and one of the archways of what remains of a mosque that was built out of the demolished remains of multiple Hindu temples. They removed all of the faces of the various depictions of Hindu gods, because Allah is the one true god, right? Right?
Oh yeah, earlier in the day we also walked around the parliamentary buildings and saw distant views of an arch and the Prime Minister's mansion. Here you are:
At some point we ate dinner, and the food was delicious. Now it is nearly one o'clock in the morning, and I am going to bed.
I implemented Go-style channels and select in C++. The need doesn't arise often, but when it does, it's frustrating not to be able to multiplex operations easily.
But Boost already implements channels! And why not just use asio? And if you care so much about multiplexing operations on logical threads of execution, why not just use Go instead of C++? What, you're going to use use kernel threads for this? Won't that use too many resources? Aren't you concerned about the cost of context switching? What about the C10k problem? Besides, you probably don't even need channels. You should just do things another way. Why are you reinventing the wheel? Don't you know anything?
Okay, okay. Your concerns are valid, but things are going to be fine. Computers are fun.
Back when I was writing a C++ wrapper library for POSIX message queues, I was frustrated by how difficult it was to portably consume a message queue while also being able to stop on demand. The simplest consumer I could imagine, "consume messages from this queue until I tell you to stop," in general requires the use of UNIX signals, since in general POSIX message queues are not files, and so cannot be used in IO multiplexing facilities like poll. Sure, you could send a special message to the queue, "okay, stop now," but that works only if you are the only consumer. You wouldn't want your "stop" message to go to some other consumer.
Fortunately, on Linux it is the case that POSIX message queues are files,
and so I can use poll
to block on the condition that either a message
arrives on the queue or somebody pokes me to tell me to stop. I could make
a pipe on which the consumer would "poll" for reads, so that when I
wanted to tell the consumer "stop," I'd just write to the pipe. The
consumer would then handle that event by ceasing its queue consuming
activities.
What do I write to the pipe? Anything really. What if I wanted to communicate more than just "stop," though? Maybe there are other commands I'd like to send to the queue-consuming thread. I could invent a protocol of messages to encode onto the pipe, and then the queue-consuming thread would parse them on the other end. That would be silly, though, since the consumer is in the same address space as the "stopper." Instead, it would be better to coordinate the copying/moving of a "command" object from one location to another, using the pipe only to wake a possibly sleeping thread.
Now what if I had more than one thread that wanted to send a command to the consumer? Well, they would contend for some mutex and thus each would have to wait its turn.
I could even add a more contrived requirement that a thread be able to send
such a command to one of multiple consumers, whichever is available first.
Regardless, the abstraction that is coming into focus from this combination of
poll
ing files and copying objects is a channel. Let the mutex,
the pipe, and poll
all be implementation details of a mechanism for
exchanging a value with another thread. Further, I want to be able to
perform one out of possibly many exchanges, with only one occurring between any
two threads at a given time.
select(...)
In Go, the facility for doing exactly one send/receive operation from a set of possible channels is called select. I like that name, so let's use it.
The thing is, we're not concerned solely with sending and receiving on
channels. In the motivating example, above, one of the operations is to
receive from a POSIX message queue. Or, possibly I want to read/write on a
FIFO, or wait for some timeout to expire, or accept on a socket. We need a
more general notion of select
than Go provides.
Also, as a library writer in C++, I can't change the language itself. What should the C++ analog of Go's select statement look like? My favorite idea, from this project, is to use a switch statement:
switch (select(file.read(&buffer), chan1.send(thing), chan2.recv(&message))) {
case 0: // successfully read data from `file`
case 1: // successfully sent `thing` to `chan1`
case 2: // successfully received `message` from `chan2`
default: // an error occurred
}
For the naughty-minded among you: no, you can't use preprocessor macros to make something more like Go's select statement. Not without lambda expressions and additional overhead, anyway.
Go is not the only language with channels. It is likely the most popular, and the reason why so many other languages are now adding similar facilities of their own (e.g. Clojure).
I enjoy Scheme. One of its variants with which I have the most
experience, Racket, has a select
-like
facility, called sync, that works with all kinds of things,
not just channels. The "things" it works with are deemed "events," and
evidently there's a whole calculus, called "Concurrent ML," for
composing events and synchronizing threads of execution with them (see
this, this, and this).
I did not implement Concurrent ML in C++. It's a little beyond my grasp.
What I did take from Concurrent ML, though, is the idea that my
synchronization primitive, select
, will operate on events, not on
channels.
What is an event? To me, it's a state machine. Under the hood, a thread will
be blocking in a call to poll
, but the events determine which files will be
monitored by poll
.
Let an IoEvent
be a notion of the sort of thing poll
can monitor (like the
pollfd structure, but including timeouts and not depending on any
system headers), together with a special value indicating "I'm done." Then I
call an event any object that supports the following three operations:
IoEvent file()
: Give me something to wait on in poll
.IoEvent fulfill(IoEvent)
: The indicated IoEvent
is available now. Either
give me another (or the same) IoEvent
to wait on, or otherwise indicate
that you are done (have been fulfilled).void cancel(IoEvent)
: Somebody else was fulfilled before you, so clean up
whatever you might have been doing. Here's the most recent IoEvent
you
gave me to wait on.That's it! Then a sketch of how select
works is straightforward:
def select(events):
ioEvents = [event.file() for event in events]
while True:
poll(ioEvents)
for i, (ioEvent, event) in enumerate(zip(ioEvents, events)):
if ioEvent.ready:
ioEvent = ioEvents[i] = event.fulfill(ioEvent)
if ioEvent.fulfilled:
for ioEvent, loser in zip(ioEvents, events):
if loser is not event:
loser.cancel(ioEvent)
return event
The trick, then, is to express sending to or receiving from a channel as one such event.
I don't know how to implement channel send events and receive events using the
framework described above. I thought that I did, but there's an essential
piece missing that, I think, makes select
ing on channel events impossible.
Here was my original design. A channel contains a mutex, a queue of senders, and a queue of receivers. Each sender or receiver has two pipes: one for communicating with that sender/receiver, and another for responding to whoever was writing to the sender/receiver.
The event member functions for a sender or receiver could then look like this:
IoEvent file()
: Lock the channel mutex and add myself to the relevant queue
(sender queue if I'm a sender, etc.). If I'm not the first in line, or if
there is nobody in the other queue, then return an IoEvent
that's the read
end of my pipe. I'm waiting for somebody to visit me.
If I am first in line and there is somebody at the front of the other queue,
then write a HI
message to their pipe, and then return an IoEvent
that's
the read end of their reply pipe. I'm waiting for them to respond.
IoEvent fulfill(IoEvent)
: Read a message (byte) from the pipe in the
indicated IoEvent
and proceed based on the message:
HI
: Somebody wants to exchange a value with me. Write READY
to my
reply pipe and then do a blocking read on my pipe. A blocking read
seems counter-productive, but it is necessary. Were I instead to return
to poll
, it could be that another event is fulfilled on my thread
before, during, or after the other thread performs the exchange, and so
at best there's a possibility that two events are fulfilled (a violation
of select
's semantics), and at worst the other read reads from or writes
to invalid memory, as I have already moved past the select
call.
The result of this blocking read will be one of the following messages:
DONE
, CANCEL
, or ERROR
. DONE
means done. I can return an
IoEvent
indicating that I am fulfilled and select
will return to
the caller. First, though, I must look again at the channel to see
whether there's anybody I need to POKE
— more on that later.
CANCEL
means that the other thread fulfilled a different event, and
so I must revisit the channel to see whether I can contact another
thread or if I must wait to be visited by another thread. ERROR
means
that an exception was thrown on the other thread while it attempted to
exchange the value, and so I too should report an error on my thread
(perhaps by throwing an exception).
READY
: I had contacted another thread about exchanging a value, and now
that thread is ready for the exchange. Copy/move the object to/from
their storage and then send them either a DONE
or ERROR
message,
depending on how it goes.
CANCEL
: I had contacted another thread about exchanging a value, but
now that thread has fulfilled another event. I must revisit the channel
to see whether I can contact another thread or if I must wait to be
visited by another thread.
POKE
: I was not first in line, but then those in front of me finished
and so now I am in front. I should visit the channel to see whether
there is anybody I can exchange a value with.
void cancel(IoEvent)
: Another event was fulfilled on my thread. Write a
CANCEL
message to whoever I was interacting with, visit the channel and
remove myself from the queue, possibly POKE
the guy behind me, and return.
I thought that this was a good protocol, and it mostly works. The fatal flaw takes the form of a deadlock.
Suppose you have two threads, thread1 and thread2, selecting on two channels, chan1 and chan2. The following situation can produce a deadlock some minority of the time.
On thread1:
switch (select(chan1.send(value), chan2.recv(&destination))) {
// ...
}
On thread2:
switch (select(chan2.send(value), chan1.recv(&destination))) {
// ...
}
That is, thread1 is sending to chan1 and receiving from chan2, while thread2 is doing the opposite — sending to chan2 and receiving from chan1.
What causes the deadlock is that blocking read in IoEvent fulfill(IoEvent)
.
Here's one possible interleaving that causes a deadlock.
thread1 | thread2 |
---|---|
sit in chan1 | sit in chan2 |
say HI on chan2 |
say HI on chan1 |
got HI on chan1 |
got HI on chan2 |
block for reply on chan1 | block for reply on chan2 |
For comparison, what happens more often is the following:
thread1 | thread2 |
---|---|
sit in chan1 | sit in chan2 |
say HI on chan2 |
say HI on chan1 |
got HI on chan1 |
|
block for reply on chan1 | got READY on chan1 |
transfer value over chan1 | |
say DONE on chan1 |
|
got DONE on chan1 |
Here there's no deadlock; instead, chan1 "won the race." How can I avoid the deadlocking case?
No amount of protocol tweaking is enough to fix this problem. In order to have
the "exactly one event is fulfilled" guarantee, a send/receive event must
perform a blocking read at some point, and doing so could cause a deadlock when
select
involves more than one channel.
Deadlocked and forlorn, I looked to Go's implementation of
select
for inspiration. This description of Go channels, by
Dmitry Vyukov, was especially helpful. In particular, he notes the following
(emphasis mine):
There is another tricky aspect. We add select as waiter to several channels, but we do not want several sync channel operations to complete communication with the select (for sync channels unblocking completes successful communication). In order to prevent this, select-related entries in waiter queues contain a pointer to a select-global state word. Before unblocking such waiters other goroutines try to CAS(statep, nil, sg), which gives them the right to unblock/communicate with the waiter. If the CAS fails, goroutines ignore the waiter (it’s being signaled by somebody else).
That's what I was missing! In my original design, a thread interacting with
another thread over a channel had no notion of the other events happening in
either thread's select
call. A thread must bring along with it a piece of
(as Dmitry put it) "select-global state," effectively allowing different events
in the same call to select
to interact with each other.
While it's encouraging that there is a way to overcome the deadlock described
above, doing so spoils the simplicity of the original select
implementation.
On the other hand, it simplifies the protocol described in the previous section
(HI
, READY
, DONE
, etc.) since now a mutex will be used for coordinating
one side of the communication between two threads, rather than an additional
pipe.
EventContext
Associated with each call to select
will be an instance of the following
struct
:
// `SelectorFulfillment` is a means by which an event in one `select`
// invocation can check or set the fulfillment of an event in a different
// `select` invocation.
struct SelectorFulfillment {
// Note that, by convention, `&mutex` (the address of the `mutex`) will be
// used to determine the locking order among two or more
// `SelectorFulfillment::mutex`.
Mutex mutex;
enum State {
FULFILLABLE, // not fulfilled, and fulfillment is allowed
FULFILLED, // has already been fulfilled
UNFULFILLABLE // not fulfilled, but fulfillment is not allowed
};
State state;
// key of the fulfilled event; valid only if `state == FULFILLED`
EventKey fulfilledEventKey;
};
Channel send/receive events are then each given an EventContext
by select
,
where the EventContext
contains the EventKey
of that event, and a smart
pointer to the select
's SelectorFulfillment
. EventContext
looks like
this:
struct EventContext {
SharedPtr<SelectorFulfillment> fulfillment;
// key of the event to which this `EventContext` was originally given
EventKey eventKey;
};
An event can be given its EventContext
as an argument to the one call to
IoEvent file()
, so now the event concept looks like this:
IoEvent file(const EventContext&)
IoEvent fulfill(IoEvent)
void cancel(IoEvent)
Non-channel events, such as file reads/writes, can simply ignore the additional
const EventContext&
argument.
Now, to make this new scheme work, there are three things that need to happen.
select
keeps its SelectorFulfillment::mutex
locked at all times except
when it's blocked by ::poll
. Effectively, we're implementing a condition
variable — but one that plays nice with file IO multiplexing.fulfillment.mutex.unlock();
const int rc = ::poll(/*...*/);
fulfillment.mutex.lock();
When a channel send/receive event wants to "visit" another thread, it does
so by locking the other thread's SelectorFulfillment
. Naively, this can
cause another deadlock, where now instead of blocking each other on
reading a pipe, threads could block each other acquiring a lock on each
others' mutexes. The trick to avoiding this is always lock the mutexes in
the same order. In particular, this means that if a thread's mutex comes
after the mutex of the thread it is trying to visit, it must first
unlock its mutex, then acquire a lock on the other mutex, and then re-lock
its mutex. The initial unlocking of the thread's mutex prevents the
deadlock.
Once a visiting thread has acquired the two locks, it examines the
state
field of the other thread's SelectorFulfillment
. If the state
is FULFILLABLE
, then the thread performs the transfer, marks the
state
FULFILLED
, notes the EventKey
of the other thread (so that
select
knows who was fulfilled when that thread next awakens), and
writes DONE
to the other thread's pipe. If the state
is not
FULFILLABLE
, then unlock that thread's mutex and try somebody else.
select
checks its SelectorFulfillment::state
after each call to poll
,
or any event's file
or to fulfill
member functions. It could be that
during one of those calls, the event fulfilled an event on another
thread, or it could be that the event momentarily relinquished the lock
on its mutex and was fulfilled by another thread. Either way, select
's
work is done. It can see which event was fulfilled by reading the
SelectorFulfillment::fulfilledEventKey
field, and proceed with cleanup.Once I implemented these changes, the deadlock described in the previous section went away.
selectOnDestroy
For any of you still reading this (good job!), there were other morsels of C++ design that I encountered while working on this project.
For example, I want a channel's send
and recv
member functions to return
an event object suitable for use as an argument to select
:
switch (select(chan1.send(something), chan2.recv(&somethingElse))) {
case 0: // ...
case 1: // ...
default: // ...
}
That's fine, but what if I want to perform a channel operation on its own, e.g.
chan1.send(something);
or
std::string message;
chan2.recv(&message);
How do I make sure that such calls actually do something? One option is to have separate member functions instead:
chan1.doSend(something)
std::string message;
chan2.doRecv(&message);
That looks terrible.
At least with recv
we could overload the member function to have a
no-argument version that just returns the received value:
std::string message = chan2.recv();
This wouldn't work for send
, though.
The equivalent code using the existing send
and recv
would be:
select(chan1.send(something));
std::string message;
select(chan2.recv(&message));
That also looks terrible.
If only send/receive events could somehow know whether they were part of a
select
invocation. If they could, then they could have the policy "if my
destructor is being called and I was never copied into a select
call, then
call select
with myself as the argument.
This way, code like this would still work:
select(chan1.send(something)); // Used in `select`; don't block in destructor
but so would this:
chan1.send(something); // Not used in `select`; call `select` in destructor
For those of you currently thinking "that is a terrible idea," I agree with you. Returning an object whose destructor then performs an operation is not the same thing as performing an operation before returning.
Also, aren't we supposed to avoid blocking in destructors? I mean, look at what std::thread does. What about stack unwinding? Fortunately, a destructor can detect whether there are currently any exceptions in flight. It wouldn't surprise me if use of that function is frowned upon, though.
Terrible idea or not, at least for the intended use case, the "history-aware
destructor" gets the job done. My main concern would be that returned
temporaries are not destroyed until the "end of the full statement" in which
they were created, which would mean that if you create multiple send/receive
events as part of one complicated expression, the actual sends and receives
will all happen "at the semicolon," rather than at their call sites. I just
don't see this being a problem, though, because there are only two reasons why
a send
or recv
would be part of a larger statement:
select
. Fine, that's their intended use.select
. Like what? The overloads in question don't return
meaningful values, so in what situation would you compose them into a
non-select
expression?So, the "history-aware destructor" solution is viable. How do we implement it?
Let's ignore C++11's move semantics for now, and restrict ourselves to copies. The signature of the copy constructor looks like this:
Object(const Object& other);
const
Object
, so we can't modify the other object. Then how are we
supposed to mark it as "don't block in your destructor"? We'll have to use
mutable
:
class Object {
mutable bool selectOnDestroy = true;
public:
Object(const Object& other)
: selectOnDestroy(other.selectOnDestroy) {
other.selectOnDestroy = false;
}
~Object() {
if (selectOnDestroy && !std::uncaught_exceptions()) {
select(*this);
}
}
// ...
};
This breaks the idea of what it means to copy something. Better would be to
make Object
a move-only type, and modify other.selectOnDestroy
in the move
constructor. However, I want my library to support C++98, and so I'd need this
hack anyway.
Now, how does an Object
detect that it is being used in a call to
select
? We could set selectOnDestroy = false
in the file
member
function, but it's possible that file
will never get called if another
event's file
causes the select
to be fulfilled. What's needed is an
additional member function in the event concept:
void touch() noexcept;
touch
is guaranteed to be called exactly once on each event before file
is called on anybody. This way, each event gets an opportunity to mark
itself selectOnDestroy = false
:
void Object::touch() noexcept {
selectOnDestroy = false;
}
With these changes, we support both usage styles for send
and recv
:
// block until we can send
chan1.send(something);
std::string message;
// block until we can receive
chan2.recv(&message);
// block until we can either send or receive, but not both
switch (select(chan1.send(somethingElse), chan2.recv(&message))) {
case 0: // ...
case 1: // ...
default: // ...
}
I haven't mentioned how error handling works in this channels library. Does
select
throw exceptions? Does it return special values indicating errors?
How does a client of select
know when an error occurs, and which kind?
My first idea was just to have select
throw an exception when an error
occurs. The trouble with this is that then if a client wants to handle the
error immediately, they have to indent the entire select/switch construct in a
try
block:
try {
switch (select(...)) {
case 0: // ...
case 1: // ...
}
}
catch (...) {
// ...
}
This wouldn't bother me if it weren't for that fact that one of the strengths of the select/switch combination is that the "handler" for each case is right there in the switch statement. Indenting the switch in order to catch exceptions means indenting all of the "handler" code as well.
This problem goes away if the client allows the exception to propagate out of
the scope in which select
was called, which is probably the common case, and
the benefit of exceptions generally. However, I still consider the try
block
too high a price to pay.
As an alternative, select
can return negative values for errors, and
associated with each error code there can be a descriptive (though
non-specific) error message. For example:
switch (const int rc = select(...)) {
case 0: // ...
case 1: // ...
case 2: // ...
default:
std::cerr << "error in select(): " << errorMessage(rc) << "\n";
}
That looks okay. But what if the client wants an exception to be thrown?
For that, we can replace the errorMessage(int)
function with a
SelectError(int)
constructor:
switch (const int rc = select(...)) {
case 0: // ...
case 1: // ...
case 2: // ...
default:
throw SelectError(rc);
}
This way, the extra code needed to use exceptions is just one statement.
So far so good, but there is still something missing. My original idea of
using exceptions throughout had the added benefit that the throw
er of the
exception can include runtime-specific information in the exception. For
example, if copying/moving a value across a channel throws an exception, that
exact exception could be propagated to the caller of select
. Or, if the
error that occurred was at the system level, such as in the pthreads library,
then the relevant errno
value could be included in the thrown exception.
This is not possible if all you have to work with is the category of error
(one of the negative return values of select
).
Is there a way to combine the "throw an exception only if you want" behavior above with the "preserve information known only at the site of the error" property of using exceptions throughout?
The only way I thought to reconcile them is by using a thread-local exception
object. When an error occurs within a call to select
, an exception is
thrown, but then rather than letting the exception escape, select
instead
catches it and copies it to thread-local storage. This way, clients of
select
can do the following:
switch (select(...)) {
case 0: // ...
case 1: // ...
case 2: // ...
default:
throw lastError();
}
Maybe you don't like the idea of using thread-local storage. It feels like a global variable. It feels like a hack. It feels dirty.
Hey, it works.
There's one more alternative that I considered. Instead of returning
an integer, what if select
returned some object implicitly convertible to
an integer, but that also contained error information?
switch (Selection rc = select(...)) {
case 0: // ...
case 1: // ...
case 2: // ...
default:
throw rc.exception();
}
Now there's no need for thread-local storage, because the exception object that
clients might want to throw can be stored in the Selection
object returned
by select
. To be honest, I still prefer the thread-local version, but I
might implement this variant as well, for naysayers.
I set out with the requirement that this channels library work with C++98 in addition to more recent versions of the language. One reason is simply the joy of what I'll call "constraint driven design." Another reason is that there are droves of programmers out there still chained to dead-end platforms and profitable balls of mud. I highly doubt that any of those programmers are about to start using my channels library in their legacy code, but they could if they wanted to.
One easy way to support C++98 without losing your mind is use boost, the grandfather of all C++ libraries. Boost is both at the cutting edge of what can be done with the language, and provides portable C++98 versions of various now-standard facilities.
Boost is also big. That's not a viable excuse for my not using it, but requiring clients of my library to have boost installed does contradict the goal of providing a minimal, portable (except for Windows), self-contained library.
An alternative to boost that I considered is BDE, Bloomberg's C++ library. It's about half the size of boost, and certainly implements all of the facilities I'd need for the channels library, but BDE is not nearly as widely used as boost, uses its own version of parts of the standard library, and does not seem to be maintained.
Without boost or something like it, I'm on my own to use POSIX for whatever I need. At first I thought that this wouldn't be a big deal, but it ended up consuming most of my development time.
Since you asked, here is the list of could-have-just-used-C++11 features that I ended up implementing:
chan::Mutex: Uses pthread_mutex_t
under the hood.
chan::LockGuard: Works with a chan::Mutex
.
chan::SharedPtr: If std::shared_ptr
is available, then it's
just a type alias for that. Otherwise, it's a minimal implementation that
uses a chan::Mutex
to protect its reference count.
chan::TimePoint: In order to specify timeouts, I needed kosher
representations of points of time and intervals of time. I could have just
used int milliseconds
, but this is C++ and we can do better.
chan::TimePoint
fills the same niche as
std::chrono::time_point.
chan::Duration: Fills the same niche as std::chrono::duration.
chan::now: Fills the same niche as
std::chrono::steady_clock. I implemented it using
POSIX's CLOCK_MONOTONIC. The C++ standardization
committee was right to call it steady_clock
instead of
monotonic_clock
. If a "monotonic" clock is used for measuring intervals,
then what would be the point of having an unsteady monotonic clock? I
suppose you could use it to order events relative to each other, but I'd
say "clock" is too strong a word for a counter. As far as I can tell
from reading on the internet, CLOCK_MONOTONIC
always happens to be a steady
clock.
chan::shuffle: Fills the same niche as
std::shuffle. In order to enforce fairness in breaking ties
among multiple events that might be fulfilled at the same time, select
randomly permutes the order in which it visits events. I couldn't just use
C++98's std::random_shuffle
, because it is not guaranteed to be thread
safe. Instead, I wrote my own shuffle
that takes a pseudo-random number
generator as an argument. I had to implement the generator as well.
chan::Random15: Fills the same niche as
std::linear_congruential_engine. I couldn't just use C++98's
std::rand()
, because it is not guaranteed to be thread safe. I also
couldn't use any of POSIX's pseudo-random number generators, because
even those APIs that could get around the thread safety problem are sometimes
not implemented so.
chan::randomInt: Fills the same niche as
std::uniform_int_distribution. If you need to restrict the range
of values produced by a pseudo-random number generator, you must be careful
not to introduce a bias in the output (such as is often the case if you use
operator%
to do the restricting). The implementation uses
rejection sampling.
chan::systemRandom: Fills the same niche as
std::random_device. Pseudo-random numbers don't look
very random if they are seeded with a constant. Instead, I need a random
starting value with which to seed the generator. The implementation uses
/dev/urandom
.
chan::lastError: In order to implement the thread-local
exception feature, described above, I had to simulate C++11's thread_local
keyword. Fortunately, every compiler under the sun supports the non-standard
__thread keyword, so I just used that. In addition to thread
local storage, I also needed to make sure that the object I put there was
properly aligned. Without C++11's std::aligned_storage
or std::max_align_t, I had to use a union
of all of the built-in numeric types supported by C++98.
CHAN_MAP: Since C++98 does not have
variadic templates, if I want to support up to, say,
nine arguments in select
, then I have three options:
select
, one for each arity.I opted for option 3, and so there's a small library of preprocessor macros in chan/macros/macros.h, and their use in chan/select/select.h is a real eyesore, but at least I didn't repeat myself.
chan::currentThread: Fills the same niche as std::this_thread::get_id(). I use it for debugging only. The implementation uses pthread_self.
I could have avoided implementing those twelve components if only I had required C++11 or boost. All together my implementations amount to an additional 1173 lines of source. That sounds like a lot, but considering that it allows the library to support C++98 without depending on a large external library, I think that it's justified.
That's enough of that. If your curiosity is piqued, then you can get started playing with C++ channels and see how you like it.
I turned my old Android phone into a web server. Here's how.
I got tired of paying nearly $80 per month for my one phone line and the less than one gigabyte of data I was using. Logging into my Verizon Wireless account to look for a better deal, I found that the "recommended plan" for me was one that costs even more money and allows five times more data.
After a little bit of research, I decided to try Google Fi. The only problem was that my aged Verizon-provided Droid Turbo does not support Google's networks. However, seeing that I would stand to be paying just under $30 a month with Google, it didn't bother me to buy Google's cheapest offering, a new Moto G6 for $100.
It was super easy. I didn't even have to call Verizon (later on I did anyway, just to make sure). Google has this sorcery where you give them your Verizon Wireless credentials and they transfer and cancel things on your behalf.
Anyway, new phone works, all is well. But what about this old phone? It's practically unsellable, though it works just fine.
To be more specific, the Droid Turbo has the following specifications (according to wikipedia):
That's a pretty powerful computer. Also, if this study on the power consumption of a comparably more powerful phone is applicable, then with the phone's mobile network radio and screen turned off, it will draw less than one watt of power. For comparison, a desktop computer might draw between 60 and 300 watts. This is peanuts compared to the Flux Capacitor's minimum requirement of 1.21 gigawatts, but that's another matter.
I have an idea! Let's gain root access to the operating system underlying the phone, wipe over that system with some Linux distribution having drivers appropriate for the phone, install a web server on it, and expose it to the internet! Wouldn't that be fun?
No, it would not be fun. It's not even possible, because Verizon is evil.
I'm well out of my depth on this subject, but as far as I understand from reading on the internet, when a phone powers up, there's a little system that loads a program that starts up another little system that loads a program that etc., etc. until the "real" operating system is loaded and starts running your beloved apps.
At each stage in this process, or at at least one stage, the code whose job it is to further the loading of the system can check whether what it is loading is "legit," for some definition of legit. Qualcomm even makes a special circuit that's effectively "write-once" memory, so that you can potentially modify such logic (e.g. to say "don't check whether it's legit") but can never change it back.
Many phones that reach the consumer as part of a wireless service contract (such as my old one) have their "bootloader locked" so that the "ROM" loaded to start the system must have a certain digital signature. This means that you can't do anything with the phone except view advertisements and buy merchandise. Were you to replace the contents of the ROM with anything else, the phone would refuse to start, or perhaps permanently disable itself so that you can never do anything with it again (called "bricking," apparently because at that point the phone is little more than a silicon brick).
Fortunately, phone manufacturers are not, on the whole, in the business of limiting the capabilities of their products, and so Motorola Android phones have a process you can use to "unlock the bootloader," typically voiding the phone's warranty in the process.
This involves turning off the phone, turning it on in a special way (holding the power button and the volume-down button), copying down some magical numbers, and typing them in to one of Motorola's websites. Then, supposedly, Motorola looks up some secret key based on what you typed, and then you can enter that key into your phone and, voila! From then on your phone will load whatever happens to be in memory, digital signature be damned.
Except that it doesn't work if you got your phone from Verizon. The website will tell you that "your phone cannot be unlocked." Why? Because Verizon insisted on this. Because Verizon is evil.
This phone does not belong to you, it belongs to us. We have no more use for it, and so neither shall you.
Ok, so it's disappointing that I can't completely control the system on the old phone, but not all is lost. After all, Android is designed to make writing apps easy. I could just code up my hobby as an application, rather than having to replace the entire system.
In fact, I don't necessarily need to write any code. Surely somebody has already released an Android app that makes the phone serve web pages. So, I can just download such an app from the Play Store, configure it to serve my static website, and job done.
Where's the fun in that? I want the full Linux experience. Fortunately for me, there's an option. It's called UserLAnd. These wizards have leveraged a program called proot, which allows you to "chroot, mount --bind, and binfmt_misc" without root privileges. proot uses a Linux feature called ptrace to trick a running program into thinking that it's running as root in a chroot jail, when in fact it's just being controlled by proot.
The folks at UserLAnd wrapped up proot in Android app clothes, including a terminal emulator and busybox, a compact suite of Unix tools, including, conveniently enough, a simple HTTP server.
Once you have UserLAnd installed on your phone, you can instruct it to install a Linux distribution in the proot jail, then start a "session" on that system as a specified user with a specified password, and you're ready to go.
Working on the Linux command line using a phone is awful. The first thing I did is find out which ssh server was running on my new system and connected to it from my laptop. This involved me learning properly for the first time how to use ssh-keygen, ssh-copy-id, and ssh-agent. Also, apparently dropbear exists and is an alternative to sshd.
The result is that I can now poke around inside my old phone from my laptop. For example:
david@LAPTOP-DTKG66RE:~
$ ssh -Y david@192.168.1.4 -p 2022
Warning: No xauth data; using fake authentication data for X11 forwarding.
david@localhost:~$ ps -ef | head
UID PID PPID C STIME TTY TIME CMD
david 6995 10263 0 Jun12 ? 00:00:00 dropbear -E -p 2022
david 6996 6995 0 Jun12 pts/0 00:00:00 -bash
10133 7136 424 0 Jun12 ? 00:00:19 com.google.android.inputmethod.latin
10082 9923 424 0 Jun11 ? 00:00:00 com.vzw.apnservice
10004 9970 424 0 Jun11 ? 00:00:00 com.android.providers.calendar
10006 9985 424 0 Jun11 ? 00:00:03 android.process.acore
10055 10005 424 0 Jun11 ? 00:02:11 com.google.android.gms
10055 10018 424 0 Jun11 ? 00:00:17 com.google.process.gapps
10085 10068 424 0 Jun11 ? 00:00:05 com.telecomsys.directedsms.android.SCG
david@localhost:~$
Alright! Now all I needed to do is:
sudo apt install git
,git clone http://github.com/dgoffredo/dgoffredo.github.io
cd dgoffredo.github.io
make
busybox httpd -p 0.0.0.0:8080
Now my phone is serving my blog at port 8080, but how do we access it from the internet?
When you type an IP address into your web browser, the browser attempts to establish one or more TCP connections to that address on port 80. But I'm not listening on port 80, I'm listening on port 8080. This is because you can't bind to ports below 1024 unless you are root. Besides, even if I were listening on port 80, it's my home wireless router that's getting the incoming connection. How is it to know that it is meant to forward that connection to my old phone?
Fortunately, the Netgear WNDR3400v3 wireless router sitting by my window supports something called "port forwarding," which allows you to say "when you receive a connection on port X, instead forward that connection to local address Y on port Z."
So, I navigated to my router's admin web interface at http://192.168.1.1
, and
configured the following settings:
192.168.1.4
is the local address of my old phone, which is connected to the
router over Wi-Fi, port 80
is the port on which I can expect HTTP requests to
enter my network, and port 8080
is the port on which my phone is listening.
You'll notice I also configured forwarding for ssh, so that I (or, now that I've written this, almost anybody) can remotely log in to my old phone. Remember, though, that the system to which I'm logging in is just a proot jail running inside of an Android app having no special permissions. I suppose you could erase my SD card, which would be a dick move, but since I just reset the phone there isn't anything on it to lose. Go ahead and add my phone to your botnet, see what I care!
In addition to telling my router to forward HTTP requests to my phone, I also
need to tell it always to assign the local IP address 192.168.1.4
to the
phone. Otherwise, the next time I restart the phone or the router, the phone
might be assigned a different local address (e.g. 192.168.1.5
). The router
has another option for this, called "address reservation."
This says that any device having my laptop's MAC address
(which is just my laptop) will be assigned local address 192.168.1.2
, and any
device having my old phone's MAC address will be assigned local address
192.168.1.4
. So, that settles that.
At this point, the old Android phone sitting in my apartment and connected to my Wi-Fi router is now accessible from the entire internet at 74.68.159.94, my router's public IP address.
"Seventy-four, sixty-eight, one hundred fifty-nine, ninety-four" does not have much of a ring to it, I admit. Sure, it's kind of like a weird phone number that you could memorize, but who wants to do that?
It would be better if that address could be looked up somewhere using an alternative name -- a more human-friendly name.
Fortunately, such a system exists. It's called the Domain Name System (DNS), and, luckily enough, there are people who own domain names and are willing to let you use a subdomain of theirs for free!
The service I used is called freedns.afraid.org.
Using this, I was able to associate the subdomains
dgoffredo.crabdance.com and
dgoffredo.jerkface.net with my public IP,
74.68.159.94
. So, when you type either of those names into your web
browser, the browser will ask DNS "what IP address(es) is (are) associated with
this domain?" DNS will reply "74.68.159.94," and then the browser will send
an HTTP request to the old Android phone sitting in my apartment.
FreeDNS's web interface looks like this:
That service even has a programmatic API, so in principle I could carry around my web server it my pocket, and every time it entered a new network and was assigned a new IP address, it could notify DNS of the new address, so that my website remained available anywhere my old phone happened to be connected to the internet. Besides being a useless idea, it wouldn't even really work, though, because I wouldn't be able to set up port forwarding on every network that I enter.
I have no answer.
After all, I don't need to host my blog from an old phone sitting in my apartment. It's already hosted "in the cloud" at dgoffredo.github.io. Instead, this was just for fun.
It got me thinking, though, about how we all have these powerful computers in our pockets, connected to the internet, all day, every day. Further, whenever you're asleep at night, the phone is plugged into a power source, screen off, still connected to the internet.
I looked into the idea of a distributed only-when-you're-sleeping volunteer computing network, so that scientists could crunch the hard numbers on protein folding and quasar searching and whatever else, at no cost to you except the additional pennies in electricity that your phone would consume while you sleep.
This already exists, of course. It's called BOINC and there's an Android app for it that does exactly what I described. It's now installed on my old phone server, chewing up CPU cycles looking for possible signals in LIGO's gravitational wave detection data.
So, hack around with your phone! It's fun, and you might learn something.
I ran into a graph problem a while back, and finally decided to write about it.
One day I'll write a bdlat-compatible C++ class
generator. It will allow you to write things like basic.bdlat
:
(package basic
(import core)
(type Value
"a generic value in a restricted type system"
(choice
("integer" core/integer)
("decimal" core/decimal)
("string" core/string)
("optional" (optional Value))
("array" (array Value))))
and then do something like
$ bat-codegen --language c++ --toplevel Lakos --component types basic.bdlat
to produce the files basic_types.h
and basic_types.cpp
containing the C++
class Lakos::basic::Value
.
Here's a first shot at what the definition of Lakos::basic::Value
might look
like:
class Value {
bdlb::Variant<int64_t,
bdldfp::Decimal64,
bsl::string,
bdlb::NullableValue<Value>,
bsl::vector<Value> >
d_data;
public:
// ...
};
A real implementation would need to account for choice elements having the same
type, and so the types in the Variant
would each have to be tagged with a
distinct integer, but let's ignore that for now.
We've already run into worse trouble, because this code won't even compile.
Value
's size depends on its own size. To work around this, a level of
indirection is needed. Suppose the class template Allocated<T>
were a
wrapper around a heap-allocated T
referred to by a T*
, but with value
semantics rather than pointer semantics (i.e. copying the Allocated<T>
copies the referred to T
instance). Then Value
can be defined as:
class Value {
bdlb::Variant<int64_t,
bdldfp::Decimal64,
bsl::string,
Allocated<bdlb::NullableValue<Value> >,
Allocated<bsl::vector<Value> > >
d_data;
public:
// ...
};
That would work, because Allocated<T>
's size is independent of T
.
When a type depends upon itself, it doesn't necessarily do so by having itself
as a member (as was nearly the case with Value
above). It could be that the
type contains a type that contains the original type. Or it could be that the
type contains a type that contains a type that contains the original type, etc.
Generally, a type A
contains a type B
if any of the following is true:
A
is a sequence or a choice having an element of type B
A
is a sequence or a choice having an element of type (optional B)
A
is a sequence or a choice having an element of type (array B)
We can think of a set of types as a directed graph where an edge points from
A
to B
if A
contains B
. Let's call such a directed graph a dependency
graph.
Then A
depends upon B
if A
contains B
or if A
contains a C
where C
depends upon B
. That is, A
depends upon B
if there exists a
directed path from A
to B
in the dependency graph.
The graph for the types in (package basic ...)
, above, looks like this:
It has one vertex and one edge, from Value
to itself: because (before our
modification) Value
contains Value
. Our modification, though, creates
a level of indirection: replacing Value
with Allocated<Value>
within the
class definition removes the one edge in the graph, since Value
no longer
contains Value
:
A cycle involving only one vertex is not very interesting. But suppose the dependency graph looked like this instead:
How many cycles does that directed graph contain? I see two:
As before, we wish to make this directed graph acyclic by removing edges, where
we remove an edge by replacing the appearance of a type T
within a class
definition with Allocated<T>
.
Which edges should be removed?
Perhaps only because of the way the graph is drawn, I'm tempted to think of the
vertex D
as the "main" type in the schema, and that makes me want to remove
the two edges inbound into it:
The graph now has no cycles. In fact, it would be a tree were it not for B
having two parents (A
and F
). Also, note that since this is a directed
graph, the edge sequence FA
, AB
, BF
is not a path, and thus not a
cycle, because the edge between B
and F
goes from F
to B
, not the
other way around.
What we did from the C++ point of view is go into the definitions of the A
and G
classes and replace all appearances of D
with Allocated<D>
.
You might have noticed that there is another way we could have broken the cycles. Here's the original cyclic graph again:
Both cycles share an edge, DF
, and removing just that one breaks both
cycles:
Now graphviz wants to put F
on top, interesting.
What's the difference between the two de-cyclings demonstrated above? My first
thought was that less is more, and so removing the cycles by adding only one
Allocated<T>
is better than adding two. That's not a fully reasoned argument,
though.
It's true that there is a cost associated with the level of indirection
introduced by Allocated<T>
. Whenever you want to read or write to
something in that member, you have to chase a pointer, rather than just work
at an offset from the parent object. Also, when the Allocated<T>
is
created, it must allocate space for the T
. That cost might or might not
be significant depending on how memory is allocated.
How much is the total cost, though? Suppose that we replaced every member
of every class with Allocated<T>
, as is done in dynamic and "object
oriented" languages. Would a running program notice the difference? Would
all the concern about "cache friendliness" matter?
Here's another thought. If the object of type T
that you are replacing with an
Allocated<T>
is much larger than Allocated<T>
on the stack, and if access to
that T
would have been infrequent anyway, it might actually be advantageous
to "tuck away" the rarely accessed member to allow adjacent objects to remain
in cache more often.
Nothing but a good suite of benchmarks could answer these questions.
Still, we have to at least get the code to compile. Making everything an
Allocated<T>
seems at least unnecessary, if not detrimental. How, then, do
we decide which edges to remove from our cyclic graphs?
Simple but suboptimal would be to say: "Any type that appears in a cycle must
be referred to only by Allocated<T>
." This makes certain that all cycles are
removed, but potentially introduces many more Allocated<T>
than necessary.
In the previous example, it would mean removing five edges instead of one:
Also, if there were edges coming in from other vertices into any of the vertices
A
, D
, F
, or G
, those edges too would be removed, even though they aren't
part of either cycle.
It seems a waste, but on the other hand, maybe it's simple to implement: just walk through the graph with a clipboard, noting all of the nodes appearing in cycles, and you're done. Surprisingly, enumerating all of the cycles in a directed graph efficiently is nontrivial.
The problem is that there can be an awful lot of cycles in a directed graph. A complete directed graph having n vertices and ~n2 edges can have up to this many (simple) cycles:
That's way more than 2n, so at worst we're totally screwed.
Fortunately, no schema describing C++ classes is going to have a complete dependency graph. Even if it were complete, then n would be small.
So, the shotgun method of "Allocated<T>
for every T
appearing in a cycle" is
still on the table, but can we do better?
What about the smallest set of edges we'd need to remove to make the
dependency graph acyclic? How do we calculate that set? In the example, above,
the answer is just the one edge DF
— we can't remove zero edges, and
removing the one edge DF
does the trick, and so that must be the smallest
set (assuming there is no other single edge that could be removed instead, and
there isn't).
The problem of finding the size (cardinality) of a minimal set of edges to remove from a cyclic directed graph to render it acyclic is called the minimum feedback arc set problem, and it's NP-complete, which sucks.
Apparently the solution can be approximated to an arbitrary level of accuracy, but unless I find a tested implementation of one of the approximate or complete solutions in an off-the-shelf graph library, I'm not about to implement it myself for a code generator.
I've come up with an algorithm that spits out a set of edges to remove from a
directed graph to render it acyclic, and as far as I can reason its worse case
time complexity is a polynomial in n
, the number of vertices, and e
, the
number of edges.
Now, maybe I'm wrong about that complexity, but regardless, either the algorithm is more complex than I think, or it's doesn't always find the minimum feedback arc set (or both). Because P ≠ NP. Probably.
Still seems pretty good to me, though. Here's how it works.
The idea is to do a depth-first search for "cyclic edges," trying multiple times, once for each vertex. The result is then the smallest of the sets of "cyclic edges" found. If there multiple disconnected subgraphs, then the result is the smallest union of the results from all subgraphs.
Choose a starting vertex, say, A
. Walk the directed graph depth-first,
keeping track of ancestor nodes using a hash set s
(i.e. add nodes "on the
way down" and remove them "on the way back up" — or mark the vertices in
place, if possible).
If you re-encounter an ancestor in your set s
while traversing the graph,
note which edge brought you to the ancestor, take a step back (across that
edge) and then proceed as if that edge did not exist. That edge is what I called
a "cyclic edge," above.
This procedure will give you a set of edges removed from the graph in order
to make it acyclic from A
's point of view.
The hunch of this algorithm is that if you repeat the procedure starting at each other vertex as well, then maybe you'll stumble upon the minimum feedback arc set.
Here is an animated example using the same graph we've been using.
Start at vertex A
and do a depth-first traversal until you run into trouble:
In these animations, the purple (pink) node is the one from which we started,
blue indicates the edge or vertex we're currently examining, and gray indicates
an edge or vertex that we've already visited. Red marks edges that lead to a
vertex in the current set s
of ancestors.
We detected that the edge FA
creates a cycle, so we remember it, remove it,
and continue:
We detected that the edge GD
creates a cycle, and so we did as before. Now
there's little left to do, from A
's point of view:
Thus the following two edges were removed to make the graph acyclic, starting
from A
:
Next we do the whole thing over, but this time starting from B
. Starting from
B
or C
is not very interesting, since they're both leaves. The algorithm,
having run out of tree at B
and noticing that there were still unvisited
edges, would proceed starting from another vertex, say, A
. But we already did
A
. So this adds no new information.
The next interesting vertex to start from, alphabetically, is D
. Here is the
complete animation starting from D
, and then the resulting edges removed:
As you can see, one of the edges is different than before.
The punchline is the result starting from F
. Here it is:
So we discover that the minimum feedback arc set of this directed graph is
{DF}
.
As I said before, this algorithm can't be right. What might help is a test suite that randomly generates directed graphs, and then compares the result of this algorithm with the result of brute-force checking the power set of the graph's edges. I could hope to find a counter-example that way, though none very large, since the brute force solution is exponential in the number of edges.
Playing with a pet project, I noticed a relationship between s-expressions and JSON.
The following JSON is also valid Llama:
{
"hello": null,
"isn't": ["it", 2, "conveninent?"]
}
Since
{
, [
, and (
are intechangable in Llama (though they must match up),,
) is considered whitespace, and:
) and null
are valid symbol names,the above is the same as:
("hello" ': 'null "isn't" ': ("it" 2 "convenient?"))
Does that mean we can represent JSON text within Llama without having to escape our quotes?
(Document ((version 1.1) (xmlns "http://mycompany.com"))
(Widget ((name Fred))
(_.content
(json {
"columns": ["Foo", "Bar", "Baz"],
"rows": [
[1, 2, 3],
[null, null, null]
]
}))))
Ideally that would be the same as:
(Document ((version 1.1) (xmlns "http://mycompany.com"))
(Widget ((name Fred))
(_.content
"{
\"columns\": [\"Foo\", \"Bar\", \"Baz\"],
\"rows\": [
[1, 2, 3],
[null, null, null]
]
}")))
Values could even be substituted in:
(let ([names ["John" "Paul" "George" "Ringo"]])
(json {
"Beatles": names,
"musicians": names,
"Brits": names
}))
yielding:
"{
\"Beatles\": [\"John\", \"Paul\", \"George\", \"Ringo\"],
\"musicians\": [\"John\", \"Paul\", \"George\", \"Ringo\"],
\"Brits\": [\"John\", \"Paul\", \"George\", \"Ringo\"]
}"
Perhaps even computed property names:
(let ([key "Willy Wonka"])
(json {[key]: "value"}))
It's a happy coincidence that the syntax for computed property names looks like a Llama list. The above expands to:
(json (("Willy Wonka") ': "value"))
It's not hard to imagine that the json
form might have a special case for
lists in property name position -- just pretend the (singular) contents of
the list were there instead.
The one thing that bothers me is Javascript-style unquoted property names:
{
"quoted": "value",
unquoted: "value"
}
In Javascript (but notably not in JSON), that's the same as if the second property name were in quotes:
{
"quoted": "value",
"unquoted": "value"
}
In particular, in Javascript, even if there's a variable named unquoted
, that
property name is literally "unquoted"
, not the value of unquoted
; hence
the special syntax for computed property names.
The unquoted:
case is a challenge for our json
feature, because
unquoted:
is a single Llama token, and it's a valid symbol. It just
happens to end with the colon character. I could forbid this case to make
things easier, but why not support it? Llama is all about finding the sweet
spot between brevity and readability.
And what will json
be, exactly, in Llama? Is it a procedure? A macro? A
special intrinsic?
I work through these and other questions in the following sections, and then
propose a definition for the json
form.
Well, there you have it. It has to be a macro, not a regular procedure. Here's why:
(let ([: "colon lol"])
(json {"foo": "bar"}))
As perverse as that might seem, it's perfectly valid to bind the symbol named
:
to some value. If json
were a normal procedure, its arguments would be
evaluated first, and so the list of arguments going into json
would end up
being ("foo" "colon lol" "bar")
, and we just can't have that. With a macro,
though, whatever literally appears as the argument is what is passed in, e.g.
the symbol :
.
We're not out of the woods yet, though.
null
null
is a special symbol that has to be dealt with. The trouble is that
the symbol null
might be bound to some value during evaluation. I think,
therefore, it's best to force null
always to mean null
within a json
form. However, it's reasonable to accept it as a valid value after
evaluation as well, so that this:
(let ([nada null])
(json {value: nada}))
yields
"{\"value\": null}"
As an aside, note that the appearance of null
in the let
binding, above,
would need to be 'null
(quoted) if null
were bound to a value above. That
is, while the expression above is fine, the following:
(let ([null "oops!"]
[nada null])
(json {value: nada}))
would yield a different answer; namely,
"{\"value\": \"oops!\"}"
In order to refer to the literal null
, it has to be prefixed by the quoting
character:
(let ([null "oops!"]
[nada 'null])
(json {value: nada}))
so that once again the result is "{\"value\": null}"
.
So, null
will be treated literally without evaluation when appearing in a
json
form, but the value after evaluation will also be accepted.
Trickier even than encountering the colon (:
) symbol are the situations we
get into supporting unquoted:
property names. Is the following a JSON
object, or JSON array having two elements?
(foo: "bar")
Well, we have to decide. Fortunately, JSON does not have a concept of
"symbols" like most s-expressions do, and so we can
forbid them outright in the final output of the json
form (except for
null
).
This means that the example above, (foo: "bar")
is a JSON object with one
property named "foo"
having the value "bar"
.
Or is it? What if foo:
were a name bound to some other value?
(let ([foo: "gotcha"])
(json (foo: "bar")))
Now we might want this to expand to (json ("gotcha" "bar"))
, and that looks
at lot more like a JSON array having two elements, i.e. ["gotcha", "bar"]
.
What are we going to do? If the symbol foo:
is bound to some value, and we
encounter foo:
within a json
form in a context where it could decide the
object-ness of a form, did the programmer intend for it to be the unquoted
property named "foo"
, or did they intend for an array element having the value
bound to the name foo:
?
It's once again tempting to disallow unqouted property names, as in JSON; but
then it seems awkward having the {[computed]: "property names"}
borrowed from
Javascript without also having the unquoted property names.
One idea that helps is to parse symbols-ending-in-colon as unquoted property names, during macro expansion, before any potential value substitution. This means that
(let ([tricky: "look out!"])
(json {tricky: "tricks"}))
yields "{\"tricky\": \"tricks\"}"
instead of "[\"look out!\" \"tricks\"]"
.
That settles that ambiguity, but still we have a problem if the object or array
is empty.
What does (json ())
yield? Is it "[]"
or is it "{}"
? Remember that the
different types of grouping characters are indistinguishable in Llama.
This presents a serious problem -- it reveals that in order to truly represent
JSON unambiguously in Llama, we'd need the help of the reader (the parser). The
reader knows, after all, which of (
, {
, or [
it encountered, because it
must match it up with the corresponding )
, }
, or ]
.
I'm tempted to add this information to the output of the parser. Right now, a datum is represented in the implementation as a Javascript object whose sole property name tells you the type and the value at that propery is the value, e.g.
const listOfNumbers = {list: [{number: "1"}, {number: "2"}, {number: "3"}]},
aNumber = {number: "13"},
aString = {string: "hello"};
The datum listOfNumbers
could have been parsed from any of (1 2 3)
,
[1 2 3]
, or {1 2 3}
, but the parser has jettisoned the distinction.
What if a list datum had an additional property, "suffix"?
const listOfNumbers = {list: [{number: "1"}, {number: "2"}, {number: "3"}],
suffix: "]"};
This way, we would know that it was [1 2 3]
to begin with.
Doing this would solve the "empty object or array?" problem, at the cost of
requiring that the json
form be implemented as an intrinsic macro --
macros and procedures written in Llama would not have access to this extra
information found in the implementation. Instead, the macro would have to be
written in Javascript.
I see no way around it. The parser has to be modified to preserve the
distinction among the various flavors of lists. Doing this will require some
subtle changes "downstream," as well, since we have to make sure that we
don't accidentally consider a list's "suffix" as part of its value. That is,
I still want (1 2 3)
, [1 2 3]
, and {1 2 3}
to be considered equal, except
in contexts where the distinction is explicitly relevant, like in the json
macro.
That work was done in this commit. In doing so, I accidentally introduced a bug, which I fixed in the following commit.
JSON.stringify
There's one more sticky point, before we get into the implementation. When
first thinking about the implementation, I thought it would be convenient to
have the json
form produce a javascript value suitable for JSON
serialization by JSON.stringify
, so that all I have to do is "unpack" the
AST nodes into a form that JSON.stringify
understands, and then it would
do the serialization for me.
This would work fine, except that the only way to get JSON.stringify
to
print a number is to give it a Javascript Number
. Javascript numbers,
though, are always stored in IEEE double precision floating
point format.
So what? Double precision floating point is good enough for everybody, right?
No! We must support arbitrary numbers, as defined in the Llama grammar! (Or, for that matter, the JSON grammar)
In order to do this, the textual content of Llama numbers has to be bypassed
through the JSON serializer, and since JSON.stringify
does not support this
(even with its replacer
argument!), we have to do our own JSON serialization.
Fortunately, JSON is simple, and also we can still use JSON.stringify
for
String
s, null
, Date
s, and any other non-numeric scalars.
Before we get into writing the json
macro itself, recognize that the job
of converting an evaluated list of data (datums) into a string of JSON can
be done by a procedure, once the colon business has been taken care of, and
so the job of the json
macro will be to take care of the colon business
and then produce an invocation of this procedure.
The input to the helper procedure will be a Llama datum that has received the following pre-processing by the macro:
:
) symbols have been removed from lists that denote objects (lists
ending with "}"
).foo:
) will have been replaced with strings
lacking the trailing colon (e.g. "foo"
) in lists that denote objects.null
will have been quoted (in the lisp sense), so
that null
means null
regardless of whether the symbol is bound to a value.So, the job of the helper procedure is to convert, for example, the following Llama (note the lack of colons):
[1, {"foo" "hi" "bar" null}]
whose AST is the following Javascript object:
{
suffix: ']',
list: [
{number: '1'},
{
suffix: '}',
list: [
{string: 'foo'},
{string: 'hi'},
{string: 'bar'},
{symbol: 'null'}
]
}
]
}
into the following Javascript object:
[
{[numberProperty]: '1'},
{
foo: 'hi',
'bar': null
}
]
where numberProperty
is a special string recognized by the JSON serializer to
mean the contained string is to be serialized as a number rather than as a
(quoted) string. You can see what I mean in the code.
The helper procedure is called jsonify in the
implementation. The only hairy part was walking through a list two elements
at a time (Javascript's "splat" (...
) operator and
recursion helped here).
json
MacroAll that remains to write is the json
macro itself, which will prepare its
argument for the helper procedure and then expand into an invocation of the
helper procedure with the modified argument, i.e.
(json argument)
becomes
((lambda ...) modified-argument)
so that then the evaluator will evaluate modified-argument
before applying it
to the helper procedure.
The macro-time massaging of the argument happens in the
removeColonsFromObjects function, which also does the null
quoting
(I need to change the name to indicate that...).
After the massaging, the macro expands to an invocation expression, and finally the helper procedure does its work before calling the custom JSON serializer.
With that, we're done! JSON embedded within a lisp, using macros.
Input (Llama):
(pml ((xmlns http://www.proprietary.com/ui)
(xmlns:pml http://www.proprietary.com/markup))
(Table ((pml:name tickets))
(_.dataSource
(pml:json (json
(let ([(row ticket status owner desc)
{ticket: ticket, status: status, owner: owner, desc: desc}])
{
columnTitles: ["Ticket", "Status", "Owner", "Description"],
rows: [
(row 11333 "open" "Bob" "The darn thing doesn't work")
(row 11334 "closed" "Bob" "Could you do this for me?")
(row 11332 "open" "Alice" "URGENT: label is wrong color")
]
}))))))
Output (XML, after additional formatting):
<pml xmlns="http://www.proprietary.com/ui"
xmlns:pml="http://www.proprietary.com/markup">
<Table pml:name="tickets">
<_.dataSource>
<pml:json>{
"columnTitles": ["Ticket", "Status", "Owner", "Description"],
"rows": [
{
"ticket": 11333,
"status": "open",
"owner": "Bob",
"desc": "The darn thing doesn't work"
},
{
"ticket": 11334,
"status": "closed",
"owner":"Bob",
"desc": "Could you do this for me?"
},
{
"ticket": 11332,
"status": "open",
"owner":"Alice",
"desc": "URGENT: label is wrong color"
}
]
}</pml:json>
</_.dataSource>
</Table>
</pml>
You can try it out by cloning Llama onto your computer and opening the playground in a web browser.
Previously I described an API for submitting a query asynchronously in such a way that the caller provides the object into which the query results are written, but without having to specify an object factory. It looked like this:
class DatabaseAccessor {
template <typename Results>
using Callback =
std::function<void(const std::function<void(Results&)>&)>;
template <typename Results>
void asyncQuery(const std::string_view& query,
const Callback<Results>& callback);
};
For brevity, let's define a couple of macros:
#define FN(...) std::function<void(__VA_ARGS__)>
#define CB(...) const FN(__VA_ARGS__)&
So we have:
class DatabaseAccessor {
template <typename Results>
void asyncQuery(const std::string_view& query,
CB(CB(Results&) callback);
};
What if, in addition to producing result sets, this database API also has a concept of output parameters? These are like query parameters, except that their values are overwritten by the query rather than used in it.
We could define an overload of DatabaseAccessor::asyncQuery
that takes a
callback with an amended signature:
class DatabaseAccessor {
template <typename Results>
void asyncQuery(const std::string_view& query,
CB(CB(Results&) callback);
template <typename Results, typename OutputParameters>
void asyncQuery(const std::string_view& query,
CB(CB(Results&, OutputParameters&) callback);
};
If the user wants output parameters, then he provides a callback that fits the second signature, while if he doesn't want output parameters, he specifies a callback that fits the first signature.
Most users don't want output parameters. They're a strange concept in databases, they can always be replaced by multiple result sets instead, and Microsoft SQL Server is the only database I've worked with that has them. It would be ideal to appropriate as little code in our library to support them as possible.
To this end, observe that since this library deduces the structure of its
output from the types of the output parameters (using this), to omit
the OutputParameters
argument is the same as if you specified one, but one
whose structure describes the absence of data: an "empty sequence."
In other words, if you don't specify the OutputParameters
, the library knows
to expect no output parameters from the query. Also, though, if you specify
an OutputParameters
that has no members, the library knows to expect no
output parameters.
So, let's define a helper type, EmptySequence
, that has no members and
introspection into which indicates "nothing to put here." Then the first
overload of DatabaseAccessor::asyncQuery
can be written in terms of the
second, "as if" the user had specified an EmptySequence
for
OutputParameters
rather than not specifying that argument at all.
How do you write this? I find it very tricky to think about.
Let's look at the code again:
class DatabaseAccessor {
template <typename Results>
void asyncQuery(const std::string_view& query,
CB(CB(Results&) callback);
template <typename Results, typename OutputParameters>
void asyncQuery(const std::string_view& query,
CB(CB(Results&, OutputParameters&) callback);
};
We want to implement the first in terms of the second using the EmptySequence
type for the dummy OutputParameters
.
template <typename Results>
void DatabaseAccessor::asyncQuery(const std::string_view& query,
CB(CB(Results&) callback)
{
asyncQuery(query, /* ? */);
}
The difference, of course, is in the second argument of each asyncQuery
overload. We're given a function that takes a function that takes a
Results&
, and we need to call the overload that takes a function that takes
a function that takes a Results&
and an EmptySequence&
, and we must
additionally see to it that the EmptySequence&
refers to some (dummy)
instance.
One way to think about this is that we need a function, injectDummy
, that
maps one type of function into another type of function:
template <typename Results>
void DatabaseAccessor::asyncQuery(const std::string_view& query,
CB(CB(Results&) callback)
{
asyncQuery(query, injectDummy(callback));
}
where injectDummy
has the following signature:
template <typename Results>
FN(CB(Results&, EmptySequence&)) injectDummy(CB(CB(Results&)));
You might want to look again at what the FN
and CB
macros mean.
You know that thing where when you have a cube drawn in two dimensions, you can think of some of its corners as popping out of the page at you or as going into the page, and if you do something weird in your mind you can switch back and forth?
I feel like figuring out an answer to this C++ puzzle gave me a new sort of
facility along those lines. Here's the implementation of injectDummy
that I
came up with, written using lambda expressions:
template <typename Results>
FN(CB(Results&, EmptySequence&)) injectDummy(CB(CB(Results&)) callback)
{
return [=](CB(Results&, EmptySequence&) innerCallback) {
EmptySequence dummy;
callback([&](Results& results) {
innerCallback(results, dummy);
});
};
}
It's not complicated at all once it's written down.
I don't know enough Haskell to say whether there's a fancy name for this operation, but it does feel a bit mathy, doesn't it?
So there we have it -- the no-output-parameters overload written in terms of the output-parameters overload:
template <typename Results>
FN(CB(Results&, EmptySequence&)) injectDummy(CB(CB(Results&)) callback)
{
return [=](CB(Results&, EmptySequence&) innerCallback) {
EmptySequence dummy;
callback([&](Results& results) {
innerCallback(results, dummy);
});
};
}
template <typename Results>
void DatabaseAccessor::asyncQuery(const std::string_view& query,
CB(CB(Results&) callback)
{
asyncQuery(query, injectDummy(callback));
}
A colleague of mine called this "like the opposite of bind," or maybe it's an inside-out bind. Six lines of C++11 is what it is.
I ran into something cool related to callbacks lately, and I have to tell you about it.
Say you're writing an API for some sort of query or RPC system. Maybe you're writing a client library for a request/response message passing framework, or maybe you're writing a database accessor.
Also suppose that there's some system of compile-time introspection that you and your users have agreed upon, so that they can indicate the structure of their data in the types of object that they use with your API.
This means that one way to write your API is using templates. For example:
class DatabaseAccessor {
template <typename Results, typename Parameters>
void syncQuery(const std::string_view& query,
Results& results,
const Parameters& parameters);
};
The user specifies a (SQL?) query, a reference to an object into which the results can be written, and a reference to an object from which the query parameters (if any) can be read.
DatabaseAccessor::syncQuery
will submit the query to the database, and then
when results are received, it will deduce how to unpack them into results
by
using the introspection facilities associated with the type Results
.
Pretty cool.
The above recipe works even if the structure of Results
is not certain at
compile time. It could be that when a Results
object is created, it takes
as one of its constructor arguments a "schema" object that determines how the
object will behave at runtime, e.g. "pretend to be an array of strings," or
"pretend to be a choice between a string named 'foo' and a pair of integers
named 'bar'."
The reason that it works is that in this synchronous API, the user creates the
Results
object, not the library:
DynamicValue results(parseSchema("fpml_exotics.xsd"), "Stellar");
StellarParameters params = /* ... */;
database.syncQuery("execute GetContract(@id, @client);", results, params);
But what about an asynchronous API? In that case, it's the library that will
be creating the Results
object. For example:
class DatabaseAccessor {
template <typename Results>
using Callback = std::function<void(Results&)>;
template <typename Results, typename Parameters>
void asyncQuery(const std::string_view& query,
const Callback<Results>& callback,
const Parameters& parameters);
};
The user provides a function that takes a Results
, but where does that
Results
object come from? It's created by the library:
void onResponse(DynamicValue& results) {
// ...
}
database.asyncQuery("execute GetContract(@id, @client);",
std::function<void(DynamicValue&)>(&onResponse),
params);
What happened to the stuff about the schema? This is trouble, because unlike with a type whose structure is known at compile-time, the library can't just default construct an object and fill it up using introspection, and unlike in the synchronous API, the library can't have the user provide the object.
Or can it?
One way to get around this shortcoming is to have a version of the asynchronous
API that takes an additional "factory" argument from which objects of the
Results
type can be constructed. This way, the user can specify any dynamic
construction logic in the factory. It might look like this:
class DatabaseAccessor {
template <typename Results>
using Callback = std::function<void(Results&)>;
template <typename Results>
using Factory = std::function<Results()>;
template <typename Results, typename Parameters>
void asyncQuery(const std::string_view& query,
const Callback<Results>& callback,
const Parameters& parameters,
const Factory<Results>& resultsFactory);
};
This solves the conundrum. Now the user can write this:
DynamicValue makeStellar() {
return DynamicValue(parseSchema("fpml_exotics.xsd"), "Stellar");
}
void onResponse(DynamicValue& results) {
// ...
}
database.asyncQuery("execute GetContract(@id, @client);",
std::function<void(DynamicValue&)>(&onResponse),
params,
std::function<DynamicValue()>(&makeStellar));
I don't like the factory. I don't know why, it just makes me feel bad. You
can see another way, though, once you observe that resultsFactory
is not just
a function that returns a Results
, it's any code at all that happens to
return a Results
. So, in addition to providing a hook for the user to give
us a particular Results
, we've also provided a hook to do whatever they want
at that point in the API's operation.
The user gives us a callback that takes a Results
, but in general we don't
know how to create a Results
suitable for putting results into, and so the
user additionally gives us a Results
factory. So it's:
Results
object.Results
object with the results.But the following would accomplish the same thing:
Results
.Results
object and call your continuation with it (or not).In code, this alternative contract looks like this:
class DatabaseAccessor {
template <typename Results>
using Callback =
std::function<void(const std::function<void(Results&)>&)>;
template <typename Results, typename Parameters>
void asyncQuery(const std::string_view& query,
const Callback<Results>& callback,
const Parameters& parameters);
};
That hurts my head a little at first. Callback<Results>
is a function that
returns void
and takes as its one argument a function that returns void
and takes as its one argument a Results&
.
Seeing it used helps:
void onResponse(const std::function<void(DynamicValue&)> fillResults) {
DynamicValue results(parseSchema("fpml_exotics.xsd"), "Stellar");
fillResults(results);
// ...
}
database.asyncQuery("execute GetContract(@id, @client);",
DatabaseAccessor::Callback<Results>(&onResponse),
params);
Notice how I use the DatabaseAccessor::Callback
alias to keep things short.
Isn't that cool?
It rolls off the tongue a bit more easily if you're willing to employ a macro:
#define FN(...) std::function<void(__VA_ARGS__)>
#define CB(...) const FN(__VA_ARGS__)&
This way, the signatures are easier to read:
class DatabaseAccessor {
template <typename Results, typename Parameters>
void asyncQuery(const std::string_view& query,
CB(CB(Results&)) callback,
const Parameters& parameters);
};
Though your willingness to use macros, especially with such short names, is a matter of taste.
Why choose one of the two styles above over the other? Is one better than the other?
Yes, the second version of the asynchronous API is better. Here's why:
The callback-in-a-callback solution has the user answer the question: "What happens when the query results are available?" Part of answering that question is having a mechanism to fill an object with the results (that's the argument to the callback).
The factory solution has the user answer two questions:
What happens when the results are available and I've filled a Results
object with them?
How do I create a Results
object?
with the understanding that a certain sequence of operations involving the answers to the two questions will be carried out by the library. Better to give the user what they need and have them do what they want.
Of course, in the case where default constructing a Results
object is the
right thing, it's convenient to have a version of the API that does this for
you, since then the API looks more like a function:
makeQuery :: Query -> Results
But that's not the general case.
This is an aesthetic argument rather than a technical one, but I like that the
synchronous flavors of the API take a single output argument, and the
asynchronous flavors of the API take a single callback argument. Whether the
user or the library needs to provide a Results
object is decided by the
signature of the callback, rather than by the presence or absence of an
additional factory argument.
If this sort of thing interests you, then take a look at part 2.
I've been reading a lot of technical documentation, blog posts, StackOverflow answers, and Reddit threads. Some writing stands out, having an apparent clarity, honesty, and maturity. I notice that my writing is not as good as it could be. Here are some things that I'd like to work on.
English contains something like 250,000 words, but these are not among them.
Pronounced hō-ˈmȯ-je-nəs
, the noun derived from the verb "homogenize." This
is not a word. The word is "homogeneous," pronounced hō-mə-ˈjē-nē-əs
(notice
the e
s).
An adjective derived from the noun "performance." But it's not a word. Maybe it is becoming one, thanks to us C++ programmers, but not yet. Use "fast" or "efficient" instead.
You mean "condense."
A less common mistake, but similar to above, you mean "converse."
You mean "regardless." I don't care what Merriam-Webster says.
You mean to "time travel," or perhaps to bill murray or to carruthanize (an example of carruthenasia).
Sometimes I say something other than what I meant, because what I said doesn't mean what I thought.
"i.e." is short for id est, which is Latin for "that is," which is used before a restatement for clarification or illustration. It does not introduce an example.
"e.g." is short of exempli gratia, which is Latin for "for example," which is used before an example. It does not mean the same thing as "i.e.," and neither means the same thing as the word "ergo."
"Ergo" means "therefore" or "thus." I see no reason to use "ergo."
Sometimes "whether" means "if," but more often it means "if or if not," which would mean "whether or not" can mean "if or if not or not," which is not what you meant. Instead, you can say "regardless of whether." This is not a definite rule, though, since "whether you like it or not" is an acceptable phrase, even though "regardless of whether you like it" means the same thing, as does "regardless of if you like it." What I think is wrong, though, is to say, "I don't know whether or not you like it." In this case, you could say "I don't know whether you like it," and so the "or not" is redundant. You might argue that in this last example, the additional "or not" emphasizes that I am unsure about the outcome, rather than am concerned about the positive outcome, but you also might not. "I don't know whether you like it" is clearer than "I don't know if you like it," though, since it avoids the potential ambiguity with "you like it" being a sufficient condition for "I don't know."
This is a rule that I learned in school, and I still disagree with it, but rules are rules. When punctation would logically follow a closing quotation, "like this," the punctuation must instead go just before the closing quote. The justification I heard is that "it looks better." It's infuriating because it can create ambiguity when you end a sentence with a quote. Peter read the text "Paul asked a question?"
OCD is an initialism for obsessive-compulsive disorder, and is a noun, not an adjective. You cannot "be OCD," and you cannot "get OCD about" something. "Obsessive compulsive," however, is an adjective, but "being OC" is not in use.
An acronym is pronounced as a word, e.g. NASA, OPEC, ASCII, and FUBAR. Many first-letter-of-every-word abbreviations, though, are not acronyms, since they are not pronounced as a word, but instead are only initialisms, such as CPU, LRU, and the NAACP.
These adverbs can be omitted without changing the meaning.
Sometimes I say something because it's close enough to what I mean, and I know it will create the desired association in the mind of the listener, but it's not exactly what I mean. I'd do better to avoid that.
You mean "now." If using the simple word "now" makes what you're saying sound silly or insubstantial, then maybe what you're saying is silly or insubstantial.
This means "eventually," or "in the future." I prefer either of those alternatives.
Besides being a bad image, this means "easy tasks."
You mean "easy task."
You mean "failure." I'm less critical about using "feel" where you mean "feeling," for some reason.
Usually when planning software that must be delivered quickly. This is a euphemism for "worry about it later," or "whatever." Stronger would be "neglect design." Generous would be "it won't matter."
Anyone who does something for money is a professional. Anything used in such a profession is professional. Usually this word is (unbeknownst to the speaker) a euphemism for "pretentious," "obedient," or "orthodox." I prefer "civil."
This is used instead of "not breaking." What stable actually means is "established." I prefer to distinguish between the two. Stable can also mean "not wavering," so I see the intent, but software that has never worked is also stable.
You mean "culpability." The term "accountability" is not mentioned when one is being held accountable for success.
This is a nice bit of jargon to describe something that happens a lot in computer programming, but it's a cliché. Maybe "bickering" is an alternative.
I haven't heard this one much, but I've read it, and used it before. Alternatives are "working" or "preparing" ("work," "preparation").
You mean "managing."
I try to keep in mind the points above when I'm editing my writing, which I don't do nearly enough. Sometimes I end up having to throw away what I've written and replace it with something that is simpler and more honest, but then it's less likely to be wrong.
Here I am in the Prague airport. I gave myself a little too much wiggle room getting here, since I took an Uber but timed it based on the public transportation route. As a result I have a half hour to kill before they'll even check me in.
Prague is a beautiful city. I arrived at night, and was surprisingly exhausted. I checked into the hostel and walked around the city briefly, dodging the men loitering around offering tickets to strip clubs or various scams. I found a little gyro pizza place and got a slice of pizza. Then it was back to the hostel and straight to bed.
I got up early and walked across the touristastic Charles Bridge. It occurred to me that I hadn't charged my phone, so I wouldn't be able to take pictures. I walked up up up to the top of a big hill that has a tiny Eiffel Tower on it. It was something of a climb. My phone did allow me one picture:
I descended the hill and crossed another bridge back onto the other side of the city. It was about noon and I was surprised to find nobody anywhere. Every restaurant I passed was presumably open but completely empty. Since I was in the mood for some authentic Czech cuisine, I finally decided on a little Korean place:
They even allowed me to borrow a phone charger. Just me and the Czech waitress. Wasn't she a doll. Now I was feeling quite better, and proceeded to walk around all day until my feet hurt. There was a bunch of stuff, most of which I didn't photograph (had to conserve phone juice):
There was a horrible noise coming from that garden place above. Turns out there are at least three peacocks:
There are plenty of pretty nooks and crannies:
It's better to be there than to see pictures of it, I think. The weather was nice and the breeze off of the river was chilly, so there was this nice smell/feel vibe about the whole area.
It wouldn't surprise me if there were more Americans in the hostel than at my job. Still, predominantly non-American. At breakfast I spoke to two German guys who were traveling after finishing school. It occurred to me at some point in the conversation that I had ten years on these guys. I would have done well to travel earlier, but better late than never.
Also, the backpacking scene is not like sightseeing. It is not uncommon for a (young) person to leave town with a backpack, a smart phone, and very little money and pop around between European cities for months at a time. Here I am with my two day stay!
In the bunk below me was a dreadlocked black guy from Tennessee. At one point we were at a big music bar and several locals were taking pictures with him as if he were a celebrity. That night we went out also with two French travelers – a guy and a girl – and generally there was much merriment. Tennessee and the two frenchies disappeared after a while, and so remained me and the Indian-looking Londoner increasingly drunk at the bar, sitting in front of the band. As far as I could tell, it was a jam band situation, but the bass player had one of the best ears I've heard in a while. He clearly didn't know the songs, and it clearly didn't matter because he was so good at it.
I hadn't planned on going out with people from the hostel that night; instead I went out by myself with a list of notable beer locations to try out. The first was an absolute bore – I had one beer and left. On the way there, though, I had passed an authentic and gritty looking dive bar, so I sat down there. I think I was a point of mild amusement for some of the regulars, sitting there with my hoodie and my phone, but I wasn't looking to talk to any English speakers and was instead just taking in the room (and the beer). The big fat guy sitting next to me seemed imposing at first, but after a couple of drinks I saw he had moved down the bar a bit and was giving one of the women a back massage. Gentle giant.
So I went back to the hostel ready for bed. After all, I had a whole museum day planned for my last day and didn't want to drink too much! But then Mr. Tennessee and the two frenchies happened, and I ended up in bed after a double dose of beer, which left me entirely miserable the next day. Mr. Tennessee and the frenchies, on the other hand, did not return until the following morning. Tennessee asked where I went, and I responded, "I was near the stage, where did you guys run off to?" He replied, "Oh man, everywhere. I don't even know." I'm glad I didn't follow.
I slept in until eleven or so and then forced my ass out of bed with the music museum as my destination.
Oh wait, I didn't even tell you about Roger! Roger is Polish. He was sitting next to me on the flight out of Warsaw into Prague. I was practicing my "make friends" game, so as soon as we sat down I shook his hand and introduced myself. Turns out he's a forty-something father of two who travels among California, Warsaw, and Prague working some job that has something to do with fiber optic cable. His older daughter is an exchange student in Mexico. He's building a Summer home on the beach. Anyway, we chatted on the plane and he told me some spots I could see in Prague. Later he came up to me at the luggage carousel and offered to share a cab into the city center, which he would pay for a write off as a business expense. Thanks, Roger. The funny thing is that the night I was telling you about before, with all of the beer, I ran into Roger on the street on my way to the boring bar. He recognized me in the dark and remembered my name. There is no shortage of good people in this world.
Okay, back to the hangover day. Food was not an option yesterday. I walked initially along the same route I had taken the previous day: to the river, across the Charles Bridge, past the tourist horde, up the hill, etc. I then began to realize that I was actually dying inside. I had my fully charged phone guide me to the museum of music, which also serves as a recital venue, and tried to keep the heaving and cold sweats to a minimum. Fortunately I was literally the only person at the museum. My visit to the men's bathroom was more like a short nap. Anyway, there was a lot there to interest a music geek like me, but I photographed relatively little. Here are clarinets specially built to support quarter tone scales:
If you have a vague idea of how to play the clarinet, you'll wonder how this arrangement manages to allow twice as many different notes to be played. Too bad quarter tone music sounds like shit.
The building itself was interesting. In one of the exhibition rooms, there was a tiny staircase leading only to a window.
Here is the main room:
In each room in the permanent exhibit, there's a little chair with two sets of headphones. The idea is you look at the period instruments and read about them, and then you can sit down and hear recordings of period music played on those instruments. They had all kinds of instruments. But there were hardly any saxophones! For that you need to travel to Paris.
After the museum, I canceled the rest of my afternoon and headed back to the hostel for a nap. As the sun began to set, the conflict between my inability to eat and my not having eaten became more urgent. I decided to walk around the city in search of a bite, and then would eventually end up at the Charles Bridge again after sunset, since it's supposed to be prettier at night.
At first I was hurrying out of a restaurant soon after walking in, since the smell of food hit my brain like a bucket of water hits a sofa. I settled on a few bites of a enormous hotdogish sausage that I bought from a street vendor. I knew I wouldn't finish it, but couldn't get myself to throw it out, so I spent the next half hour or so walking around Prague with a giant brown penis in my hand, occasionally nibbling on it with a nauseous look on my face.
But still, life was good. The sun was setting:
There is a life-sized crucifix on the Charles Bridge:
And the city does look great at night, though it's difficult to capture without better optical control:
I stopped by a Chinese restaurant on my way back to the hostel, mindful of how famished I'd be the next morning. Turned out to be a good move. So, I didn't have any authentic Czech food during my stay (aside from the hilarious sausage). Next time.
That's about it for my trip. I'm waiting for the plane to arrive here in Prague. Then it's a few hours in Warsaw, and finally the long ass flight back to Newark. Ahoy!
I'm sitting at the gate in the Warsaw airport, again. I can see them fueling the plane, and they just finished loading luggage from the cart. Hopefully my luggage was included.
Friday was an impromptu tour of Russian motor vehicle bureaucracy. Due to policies created since Pasha absconded to America, renewing your Russian driver's license requires proof of residency, a psychological evaluation, drug test, physical exam, eye exam, and I suspect a prostate exam though Pasha wouldn't say. Pasha and Gregory zipped around town visiting clinics and filling out forms like true pros. Along the way we got a small supper at a cafe. Gregory noticed that the cafe sold health-related products and so took the opportunity to schmooze the cashier to set himself up as a supplier.
Here is a picture of a church in the distance:
After Pasha was reverified as Russian, we went to the Victory Park, which commemorates the triumph of communism over fascism in World War II. It's a nice, big, open park near the center of Moscow, and has a bunch of impressive monuments. I was promised a tank:
There used to be more tanks in this park, but now they've presumably been moved to Ukraine. This tank, though, remained, along with this soldier:
I'm talking about the guy on the left.
There's all kinds of stuff in the park. Here are a couple of things:
And another thing:
There was a certain manly gravitas about these depictions that surpass any war memorial I've seen. The tall thing above is interesting and recent: surrounding it (not shown) are the coats of arms of each of the major Russian cities, all cast in bronze. Circa 2016.
See? It's a generally nice park, and the weather before dusk was ideal. All sorts of kids and couples frolicking on roller skis and hover boards.
I'm on an airplane now. Remember World War II? Well apparently a lot of shitty stuff happened. Here's a memorial that freaked me right the fuck out. Take a look:
It's called "The Tragedy of Nations." To the left of those figures are discarded shoes and bones. In the middle are tombstones with inscriptions in several languages, none of which I can read:
Wakka wakka!
The most prominent object is this tall thing:
At its base is a man on the horse BEHEADING THE NAZI DRAGON. Fuck you, Nazi dragon!
Eventually the sun began to set, as it does.
So ended Friday.
I just got to a hostel in Prague. I'm surprised to see two people sleeping. It's an eight person dorm. Jet lag? Alcohol? I'll inquire tomorrow. For now there's more Russia to tell.
On Saturday we got up early to drive to St. Petersburg. Originally Pasha and I had planned to go there just the two of us on a high speed train for our long awaited romantic getaway, but instead Gregory insisted on driving us and showing me around personally. No complaints there. Pasha warned me, "It will be less chill, but I think he really wants to go." Vladimir came too. His daughter, alas, did not.
It's a long drive; something like ten or twelve hours depending on stops and traffic. Gregory drove the whole way, despite offers from me and Vladimir to take the wheel.
As you leave Moscow, the relative poverty of Russia becomes more apparent. I didn't take any pictures along the way, though, except for the folksy roadside tea and pirogi stand where we took our last break before reaching St. Petersburg.
There were several presumably stray dogs scrounging around for scraps. One, though, was simply curled up in a sunny spot on the ground. I don't have a picture, but Pasha and I agreed that this was a most cat-like dog and most likely the local hegemon.
I liked the view behind that stand, too.
If you like the lighting in that picture, it means that you have good taste.
Finally we arrived in St. Petersburg. It was a quick homemade dinner and then straight to bed. The next day we traveled into the city center:
Pasha agreed that St. Petersburg looks "more European." This sort of thing reminds me of Stockholm (I think it's the way the lamps hang between the buildings).
St. Petersburg has canals
and fighter jets
and, most of all, churches:
But these aren't your frumpy American protestant churches. No, before the revolution, the Russian orthodoxy was nearly synonymous with the ruling czar, and thus with the disproportionately focused wealth of the entire state. Check this out:
Church stop number one included a mass complete with holy water in the face. Church stop number two included an English audio tour.
Then we went to a palace (right).
Next to the palace is a phallus:
At least I was taller than the soldier.
Saturday also happened to be Gregory's fifty-seventh birthday, which I think in part explains why Vladimir came along. We celebrated over dinner back at our rented apartment that night, in the traditional Russian style: Russian food (lots of that) and vodka (lots of that). After each couple of shots, one of the Russian speakers would toast to the birthday boy. In Russia, birthdays are rather more important than they are in America. Vladimir's speech to Gregory on his birthday was heartfelt – something about planting a tree in life and giving to others and befriending people from other cultures and stuff like that, and how now his tree bears fruit later in life from the good things he has done.
Breakfast was tricky the next day. Not because I was hung over (I wasn't), but because these guys eat SO MUCH DAMNED FOOD first thing in the morning. It's incredible. Also, you should eat more tomatoes, because lycopene is good for your prostate. I don't know if it's true, but eat your damned tomatoes. If you don't have a prostate, then you're missing out, but also off the hook.
Come to think of it, I might be getting my days mixed up. Whatever, consider this an editing room resequencing.
We drove to a town near St. Petersburg called Petergof. There we visited the former Summer palace of the aristocratic overlords that sits in a large park that stretches to a bay, across which you can see St. Petersburg. If you can see really well, then there's also Finland.
The girl on the right is photographing herself. I asked whether she'd like me to take a picture for her instead, but she was totally not into it. Fine, take your own pictures. This is not my caring face.
If you're wondering about the hideous jacket, it's one that Gregory graciously offered me to supplement my Moscow clothes. It was much colder in St. Petersburg, and windy and wet.
Running from the palatial fountain all the way to the bay is this canal (viewed from the bay side):
Here is the water:
Let's be honest, though. You didn't come here for palaces. You came here for churches. So, we visited another, larger, church. This one was built just at the beginning of the twentieth century, and has inscribed within it in stone the names of various fallen soldiers on ships dating back to the early 1700s. The older the names got, the more trouble Pasha had reading them (Russian has changed quite a bit since back then).
There was a mass going on as we checked out the place. There were four women in the chorus, facing away from the center of church so that you heard primarily the echo of their voices (I think it's called "reverb"). I was impressed by the music, at least as impressed as one can be about such music. The church is gorgeous:
Hey look, it's Jesus. Here's a low resolution panoramic shot:
Done? No, we're not done. We managed to squeeze in a bonus tour this morning. The tour was in Russian, so I have only a vague idea what was going on, but I took some pictures. There were rabbits:
and a river:
and a heavily fortified... fort:
a couple of pretty buildings:
and... can you guess it? A church! This one has famous dead people in it.
That's enough for now. It's almost 11 PM here, and I think I ought to venture out in search of food before bed. Solo in Prague!
Hi Again,
Yesterday was more relaxed than the previous days. After lunch here at the flat, Pasha and I took the metro further into the city to meet his dad, Gregory, with his car. We drove to the computer market in town so that Pasha could get a new power adapter for his laptop, since it broke. After that we drove around until we ended up at the Chruch of Christ the Savior. The plan was to check out the church and then to take a short boat trip along the Moscow river, but I'm guessing that the schedules did not align, so instead we did just the boat ride, which was nice.
After that, the sun was beginning to set, and we drove to a theater across the river from "Moscow City," which is the skyscraping financial district of Moscow. None of the buildings there are much more than ten years old. Since all business was controlled by the state until the fall of the Soviet Union, a skyscraper would have been built only if a bureau decided to build one, which I'm guessing would have been an excessive expenditure. I always had assumed that any sufficiently large hub of industry and urbanity would have a district of tall buildings, but not in the Soviet Union.
A relative of Gregory's (and Pasha's) works as chief engineer at the theater. We met him at the theater to mooch off of the employees' cafeteria for dinner as it was closing – chicken soup and "Navy macaroni," and tea, of course. Russians love their tea. And you always eat the soup first.
After dinner we walked across the darkened stage to the front of the building where we met some more relatives. As much as I could ascertain, the engineer is Gregory's second cousin. We met his wife, his mother, some younger cousin about my age, and Ella (Pasha's mom). We walked together to a large coffee shop and talked for a couple of hours. Of course I had no idea what anybody was saying. At one point Gregory asked me to tell everyone, in English, what we had done, but I drew a blank (I also didn't quite understand the context of the question, and suspected my audience did not understand English). Anyway, everyone politely ignored me as I stumbled into nonsense, and then it was back to Russian.
Today we'll probably go to a park where there are a lot of tanks. Gotta love tanks. Cheerio!
Greetings from Motherland,
Come sit, for I have much to tell you. I arrive three days before, and see many things. On first day, I take plane. It was big plane, but slow plane. I did not sleep on plane, so when I arrive in Moscow the next day, I want to sleep. My body think it late at night, when it Moscow it middle of afternoon!
Gregory – papa Pasha – and Pasha pick me up in machina at airport, and we drive to city center. Gregory speak some English, but I speak no Russian so I like some English. Moscow big city. There are many building, mostly apartment building. We go to flat of Ella – mama Pasha – and Ella very happy to see Pasha. She make high pitch noise. Ella no see Pasha since seven year ago, but they talk every week. We all eat big dinner and get very sleepy. There is much garlic. Gregory ask if I want to visit some place, but Pasha say no, we are tired. So we sleep very early.
Next morning I wake up at midnight, and at six, and then at eight. Ella make breakfast of porridge and yogurt and I have apple. Ella very nice woman, also speak some English. Then Ella go to work and Pasha and I take metro to meet Gregory in city center. Moscow metro is large and clean. It is deep underground for nuclear war. I no see rat and no homeless person. The ceilings are high like cathedral.
Gregory know powerful people in Russia. One powerful person know even more powerful person. This person sign document allow American into state Duma, center of Russian government. Document so effective that Russians must wait and use other entrance. American with document can use any entrance.
In Duma we meet Vladimir – friend of Gregory. No, not that Vladimir. Vladimir work in Duma for twenty-three year. He is retired but still know people in Duma. Vladimir gave tour to American and show offices of four parties of Russian politics. Russian politicians on holiday, so no meet Russian politicians. Pasha translate for American. Vladimir has much hair in nose. He look like tooth brush! Tooth brush know much of Russian political history. Pasha translate whole history for American.
In Duma we eat lunch in cafeteria, and there is much to eat. Then we have tea in cafe. American have trouble staying awake in Russian conversation, but only Pasha notice.
After Duma, we go to Kremlin. Gregory and Vladimir leave, but Pasha and American go to Kremlin. Not whole Kremlin, that for another day. We walk through Kremlin, and go to armory museum. Armory museum have most luscious collection of silver relics in all Europe. Audio tour say luscious many times. It amuse American, but Pasha no understand why. Pasha is disappointed at the size of collection. There are many rooms of items, but not as many as Pasha expect. American tell Pasha he in New York too long.
After armory museum, we walk around Kremlin in park. It is very nice weather and many pretty girl. American ask Pasha where ugly women in Moscow. Pasha say we will see them. We sit on bench and get hungry and sleepy. We walk to Red Square, where American take pictures. The sun is setting and it is very pretty. There are many tourists. Then we go to fast food Russian pancakes and then to outside bar cafe. Beer cost New York prices near tourist area. Pasha says before exchange collapse, Moscow very expensive city.
Then we walk to famous street in Moscow, like La Rambla in Barcelona. It long walk to get there, but we pee before so no problem. There we find fancy beer bar, and have fancy beer. Young group sit at table next to American and Pasha. Pasha and American speak English, and group assume we no understand Russian. This amuse American, and Pasha tell American what they say about us.
We go back to flat of Ella, and she is worried. Midnight is late for Ella. Pasha call Ella, but still Ella worry. Ella express worry by making soup. Very good soup, with much garlic. We go to bed. The next day I am thirsty.
We get up early. The beer help wake up, and help in bathroom. American very impressed. Gregory have whole day planned. Gregory very energetic man, generous host. Gregory take us in machina to laboratory of friend. Friend head of Russian pharmaceutical quality verification agency. He want to retire, but there is nobody to replace him. Man wear white lab coat and shake hand of American and speak Russian. Pasha translate for American. He give tour of facility. There is much expensive equipment. Women working on equipment avert gaze when man in lab coat enter room. One room has full chemistry lab. American tell Pasha, "I love chemistry, so this is like a playground for me." Pasha does not understand and translates roughly, "This looks like child's play." People in lab are offended, and American does not understand. Gregory tell American later, "this is state facility, not private." Then American understand, and tell Pasha to clarify. Still American think it is not clear. Later in machina American tell Pasha again, and only then Pasha understand. American learn it dangerous to translate subtle ideas. Still, man in lab coat did not seem offended. He show us more expensive machines, and guinea pigs and lab mice. He surprised earlier when American ask him how they measure toxicity using spectrographic methods only. So man in lab coat reveal animals. Man in lab coat ask if American parents doctors. Pasha say no, dentist. Then we have tea and cake in break room. There we talk to Olga, H.R. of facility. American find it hard to look directly at Olga. Olga want to be actress, but was rejected from acting school. American tell Olga to be actress in New York. Olga laugh. American ask Pasha where are ugly women in Moscow. Now Pasha says he does not know. Gregory and man in lab coat left us three for conspicuously long time.
After facility, we stop at cemetery. Relative of Gregory famous actor, and murdered horribly a few years ago. We get flowers and put them on grave. Always even number for dead, odd for living. Do not give Russian girl even number flowers. Cemetery have graves of many famous Russian people. There is section for artists, and section for soldiers.
After cemetery, we stop at church to light candles for murdered actor. Pasha say religion business, and Gregory shrug.
Next we go in machina to Moscow State University. We pick up daughter of Vladimir as tour guide – Natalie. Natalie does not have visible nose hair like father. Natalie's eyes like Neptune. American was hungry, but after meeting Natalie American no longer hungry. Natalie teach dance to children near university. She speak a little English, but travel much. We go to university building, big building. We can no enter building without pass, and cannot get pass, so we walk around campus. Moscow State University have very nice campus. Pasha tell American next time Pasha in Moscow, he stay near university.
Gregory buy everyone business lunch at restaurant near campus. American and Pasha have rubles, but Gregory insist. Gregory order for American all classic Russian foods. Then we walk back to car and drop off Natalie. American very sad. Gregory insist that Natalie give email address to American and Pasha, so we can send pictures we took. American start to think Gregory very smart man.
After university we go in machina back to flat of Ella, but there is much traffic. Victory Day on May 9 commemorate destruction of Nazi in Second Great War. Russians rehearse parade, and so there is much traffic. Gregory hate Moscow traffic.
Before flat of Ella, Gregory need to do something for work. Gregory drop off American and Pasha near giant television broadcast tower. Near top of tower there is observation deck, cafe, and restaurant. Cafe and restaurant rotate around tower once every forty minute. There is no room in restaurant, but we go to observation and take pictures, and have beer in cafe. American like view from tower. Many good picture of sun setting from tower. After tower, Gregory return with machina and take us back to flat of Ella. American very tired and we sleep.
Pasha and American sleep almost twelve hours! American wake up and start writing log of travels. Now it 2 PM and Ella come home from work for lunch. До связи!
I'm aboard the Arlanda Express, on my way towards a nine hour flight. Fortunately, in addition to being an airplane, the vessel will be also a time machine, so I'll arrive in New York only two hours after departing.
We stayed just outside of central Stockholm at a friend's apartment, which has a balcony whose view is this:
Our host looks like this:
The first full day he took us around in his car to see some scenic areas of Stockholm:
and we went to a museum about this old boat they found buried in the ocean and managed to restore with 98% of the original material. There were floors in the museum on things like the curing process used in the waterlogged wood, the skeletons of the crew, and a bunch of other interesting stuff that you wouldn't expect to find explained in such detail. I rather liked it. The boat itself is gigantic:
The following day we drove out to a boat to take a ferry to one of Stockholm's many islands. It was a nice ride:
Once on the island, we took a cable barge to a smaller island where there's an old fort:
Hobbits live there:
Then we took the ferry back around sundown:
and headed into a pub in the city center to watch the "Sweden versus Europe" world hockey semi-final. Sweden lost. C'est la vie. The city looks nice at dusk:
and they have balloons:
I have so far resisted the cesspool of disease otherwise known as Vineet, but we'll see how I fare waking up for work tomorrow. Ciao.
Greetings from 35,000 feet.
So much for Barcelona; boy am I tired. Our last day in Madrid we went to a cafe; on the fourteenth floor of a hotel "Dear Hotel," (with the comma) to enjoy the view and have a rather early glass of wine:
Then it was off to the train station to board my first high speed train, which travels at high speed. The speed looks like this:
and the views are generally gorgeous:
When we arrived in Barcelona, we took the metro to our Airbnb host, who did not speak English (nor much Spanish!), so that was interesting. Then it was out to the main popular section of town for dinner and drinks, where we met an Indian bartender who we chatted with until he closed the place.
Day two it was off to the sights. We went to this Gaudi park museum house thing. There was some cool stuff:
A little later we headed towards Las Ramblas to walk around and see stuff. Stuff:
Here's another homage to Columbus. He's standing, pointing towards (presumably) the New World. I feel like he is pointing East, though, since we're on the Mediterranean side and there's the beach.
There is ocean:
Then with our aching feet we climbed to get to the entrance of a cable car so we could see everything from above. The hill we had to get up offered some nice views, too:
And then from the cable car:
A lot of young couples in Barcelona. Bring your own babe.
Then we walked over to the beach. I took off my shoes and walked around. Here's the sea:
Then it was back to the Airbnb for a siesta in anticipation of all of the fish we were going to have for dinner:
No fish was safe that night.
There's other pretty stuff around the city. We rented bikes at some point and rode around the parks until we hit the beach.
Are those bats wearing armor?
Anyway,
I made a dinosaur friend:
The big sightseeing attraction in Barcelona is the Basilica. It's a big deal. I didn't know this, but they've been building it for well over 100 years. It's scheduled for completion in 2026, I imagine just before the Second Avenue Subway opens. But look at the thing:.
Ready to head inside? Are you wearing your diaper?
Here's the back. The idea is that the front entrance is all about the Nativity, and the rear is all about the crucifixion. We did the audio tour.
Here's more detail on the front:
Goudi was totally stoked on Jesus. Couldn't get enough of him.
The rest of the trip was wandering around the Gothic Quarter at night, eating and seeing what's around:
There was this festival going on in the main park, so we went there and saw all of this live music and interesting art installations. You want to see more of the Basilica, don't you?
Now it's a just-under three-hour flight to Stockholm, where presumably we will eat meatballs and shop for furniture. The snack guy is coming around I NEED COFFEE.
[2020 note: the images get more corrupted as the post goes on.]
Slept in a bit today. Vineet’s vibrant nasopharyngeal discharge has accordingly ceased.
The “morning” looked like coffee and tapas for brunch at an outdoor café, followed by a stroll in a random direction out of the touristy section where we are staying (Sol), into a place where there are people who actually live in Madrid. Turns out that walk was longer than it felt, because by the time we stopped to rest, it was time to head to another part of town for our 3:00 bicycle tour.
We saw the spokes and the pedals and the HANDLEBARS and oh! You should have seen the seat.
After this Vineet and I each got on a bike, along with the only other – our very own personal Swiss tour guide. He was older, and he seemed to know everything there is to know about the history of Europe, so that was good. The ride was nearly three hours, so you really get to see a lot of the sights of the city this way, as he later mapped out for us in green and blue highlighter:
This would have been a good thing to do the first day, but we didn’t know to do it the first day. Anyway, there were many pretty pictures to take.
There was guy on horse:
A palace:
A cathedral:
Our guide (now I remember his name – Casper. Casper the friendly bicycle tour guide) was not impressed by the stained glass on the cathedral. Well, you try staining glass and we’ll see how it comes out.
I was surprised to learn just how much royal mingling there was in the last six centuries. For example, that palace two above was designed by the French (baroque style, if you’re into that), built by an Italian king, in Spain.
Then there’s this big plaza:
With nice arched entrances:
Oh, and Spain has no government. They can’t decide who’s in charge so they’re just like fuck it we’ll do it later. There is still a congressional building, though, just for us:
There were plenty of other pretty buildings, but also a vertical garden:
A horizontal garden:
And trees beginning to change color:
Part of the trouble of learning the history of buildings in Europe is that the stories mostly fit the same template. At one point, Vineet and I were walking into some historical square, and there was a brass plaque on the wall explaining, in Spanish, what the building was. Vineet asked me what is says, and I replied that the building had been commissioned hundreds of years ago by a powerful rich person, and was built over multiple decades, at great expense (some people died building it!), and served an official purpose until about a hundred years later it was severely damaged in a war, and was sold and long abandoned until within the last 100 years it was bought by a preservation society that left it in the custody of the city and recently restored it, so that it now serves as a gift shop and has an impressive plaque on it. Many of the prettiest buildings seem to share this story. I have no idea what the plaque says. [2020 note: I'm not confident that conversation actually occurred]
Check out this one:
Some guy lived or died there. Or is buried there. Or some combination.
Here’s the town hall:
Refugees are welcome into this building. I did not see such a banner at immigration, though. Check out the statues. They are actually statesmen frozen in carbonite – according to the prophecy, they will come back to life when either a government is formed in Spain or unemployment drops below 20%, whichever happens first. Some were first transformed into giant cats by the Witch King of Leon.
That’s all solid marble. Don’t make them like they used to.
There is a colossal tribute to Columbus discovering the New World:
And finally, since you asked, another guy on a horse:
Had your fill? Granted, Madrid has nothing on Iceland in the horse department, but they make a good effort:
Tomorrow it’s off to Barcelona. Buenas noches.
Greetings from Gatwick,
The flight into Madrid is a bit late. I'll take this time to catch you up on what I did in London. For example, at some point we encountered a monument:
I daresay it was monumental. Here's what it looks like with the sun behind it:
That day we also walked along the Thames and saw Tower Bridge from London Bridge:
and Tower Bridge from Tower Bridge:
There were interesting alleys:
and vintage dental equipment (at the science museum):
Later that day we met up with some Bloomberg people (we're everywhere) for a few pints and a burger. Even one of my New York based managers showed up. Small world.
The next day we took a train out to Greenwich, where the time is mean. The ceiling paintings and chapels are less mean, however:
and the parks are nice:
I named the fields around Greenwich "Jurassic Park" on account of the large number and density of giant ravens and geese that saunter around.
Coming down from the hill we ended up in a town called Blackheath, where we stopped into a pub for some food and saw a little local philosophy:
Apparently the pigeons have a lot to be concerned about in London, since even pelicans will eat them. What? Yes, in London, pelicans (there are pelicans) eat pigeons (which are not fish). If you don't believe me, google it. I must warn you, though, it's graphic.
Today while Vineet got brunch with an old family friend, I opted to head back into central London and pay a quick visit to the British Museum, where I observed the Roman god of wine dancing with his panther, as only the Roman god of wine can:
Vamos a Madrid!
Today's cool thing was a glacier hike. You know, hiking on a glacier. A fucking glacier. Strapped crampons onto our feet, helmets on our heads, and walked up onto hundreds of meters of solid ice as it was melting. We walked down into a cave eroded into existence by melt water. Everything was made of ice. The walls like wet blue glass sparkling with starry air pockets. Ice for miles.
I don't have any pictures of that yet, because they were taken on Vineet's phone.
Nanny nanny boo boo, stick your head in doo doo.
We saw also a beach with black sand and strange rock formations. The sun was setting behind this sort of wall of stone, and the picture really doesn't do it justice. The setting sunlight painted the structures gold, LIKE THE HALLS OF VALHALLA!!!
You really would need multiple exposures combined to see what it looked like.
Anyway, strange rocks:
This is what Vineet looks like when he's not falling to his death:
Mountains in the distance to your right (where the glacier is) and the ocean to your left:
https://drive.google.com/open?id=0B6rp2WsQNNAlVzVVOTBTWjVXTW8
And did I mention yesterday? No? Well that involved energy drinks,
geothermal ejaculation,
[2020 note: this picture was cropped badly due to file corruption.]
waterfalls,
[2020 note: this picture was cropped badly due to file corruption.]
a lake formed in a volcanic crater,
[2020 note: this picture was cropped badly due to file corruption.]
and sometimes climbing to the top of a hill to sit and listen to the creek and look at the horses.
[2020 note: this picture was cropped badly due to file corruption.]
We're at a hostel-like hotel in "Hella" now after having gone to "Vik" for dinner. Cod stew. Tomorrow we drive over towards the airport to stay at the Blue Lagoon, which is a spa. Maybe I'll drop 100 euros on a massage. Then another Airbnb near there and off to the airport Tuesday to return the car, and then we're LONDON BOUND.
You want to see more cats? Me too:
[2020 note: these pictures are cropped badly due to file corruption.]
[2020 note: my God, look at the cock on that one.]
These are the true Icelanders. The humans are kept alive only to act as caretakers in their intergalactic communist utopia.
Vineet copes with the low coefficient of friction in Iceland.
Morning from the North,
Vineet and I got in just before midnight Iceland time last night. Immigration was about five seconds in and out, and there was no customs. I bought some of their funny money, which looks like other funny money I’ve seen. It’s not green and it comes in large denominations.
Then the rental car guy put us and a few other Americans into a van and drove five minutes to their lot, where we coughed over some more money and got into a nice Korean SUV thing (an unexpected upgrade from what I had booked), and I drove it on the wide, empty, Icelandic highway for less than an hour until we reached the capital. The Airbnb place where we’re staying this and last night is a nice, large, two bedroom apartment with a space-age shower and bizarre power outlets. Vineet and I are in room 2. Just met the couple from room 1 this morning. They’re nice, but they talk funny. I think they’re English.
Body clock has no idea what’s going on. I guess the time difference is not so bad. Going to drive around The Golden Circle today. I think the rental guys might not have given us the promised full talk of “petrol,” which I presume is a more expensive word for gasoline.
For those of you stalking along at home, we are currently here:
DNS insists on bringing me to Icelandic websites. Don’t they know who I am?