Why I Like Neo4j

Earlier this week, I gave a presentation comparing various SQL/NoSQL databases. I provided an overview of five different databases described their particular use cases, but I noticed myself talking about one much more than the rest: Neo4j.

Part of this is because Neo4j is one of the least-known database from a list that includes PostgreSQL, Redis, MongoDB, and CouchDB. But part of it is because it takes a really fascinating approach to data. While I’ve used MongoDB in the past for a number of applications, I’ve really enjoyed using Neo4j for a couple of recent projects – and I think I finally understand why: MongoDB prefers to internalize data attributes, whereas Neo4j seeks to externalize them. (Incidentally, Neo4j’s approach is loosely analogous to the way objects are handled in Lisp).

Alternatively, MongoDB is about data properties, whereas Neo4j is about data relationships.

In MongoDB, the idiomatic way to represent data is as an object, which contains properties. I might have the following:

  { "name" : "John",
     "state" : "NY"

  { "name" : "Frank",
    "state" : "NY"

If I want to find all New York residents. MongoDB selectors lets me do this with a simple query: db.users.find({"state" : "NY"}). However, let’s be clear about what is actually happening: there is no inherent connection between the “state” field in John’s object and the “state” field in Frank’s object. It is almost as if the fact that both fields have the value “NY” is coincidental. Both happen to “own” the same value and reference this property by the same name. The primary way of interacting with data in MongoDB is by aggregating relationships into objects and then querying against those aggregate relationships.

In Neo4j, I might instead do the following:

This represents the exact same information, but the interface is very different, and one difference is particularly notable: John’s “NY” value and Frank’s “NY” value are both the same – not just by coincidence, but by the very structure of our database relationship. Furthermore, neither the John node nor the Frank node “owns” the NY node.

Finding all New York residents is therefore as easy as finding all directed edges of type “lives in” that terminate on the (single!) “NY” node.

Thus, the primary way of interacting with data in Neo4j is by disaggregating data objects into datapoints and then defining relationships between datapoints. It’s a slight stretch, but we can almost say that the relationships are more important than the data itself! Since Neo4j prefers not to internalize information within nodes, a single node is nowhere near as informative as the entire collection of related nodes.

As a fairly blunt analogy, it’s the difference between the following:

{"_id" :  ObjectId("50e21b4623e50ac61370"),
  "field-1" : "value1",
   "field-2" : "value2"}

This is not to say that Neo4j doesn’t support properties – in Neo4j, nodes can have properties (and edges can too). However, aggregating over nodes by property value is likely suboptimal, and it robs you of the very power that a graph-based database is designed to provide.

This parallels the Lisp object system (CLOS) in a way that’s a bit subtle and probably worthy of its own post. For those of us who naturally prefer functional programming styles, however, graph-based databases like Neo4j may provide an interesting way of looking at our data.

Sputnik: Rising Above the Air

Apple has achieved success in large part because it created expectations rather than satisfying them. This marketing strategy paid off for Apple; for example the iPhone wasn’t compared to existing smartphones like the Blackberry, which would have highlighted its relative shortcomings (no copy-and-paste, inferior battery life, AT&T-lock-in, etc.). It was viewed as its own entity that targeted a different market altogether.

And, to an extent, that market segmentation was initially true – the iPhone caught on quickly among demographics that weren’t using Blackberries for enterprise and personal use. According to this narrative, RIM and Palm sold “personal organizers” with phone functionality, whereas Apple sold “touchscreen smartphones”. If you’d tried to predict iPhone sales by demographic based on Blackberry and Palm devices, you’d have been way off, because the devices targeted very different markets.

Criticisms of the iPhone based on comparisons to Blackberries and Palm devices didn’t hold up as well, because the target iPhone users didn’t have the same expectations as existing PDA + phone users – many of the first iPhone most passionate users had never owned a smartphone or PDA before. When the iPhone finally got some of these missing features, they were a nice surprise, but they weren’t deciding factors for the first generation of users. iPhone users didn’t complain about the lack of voice control and drop-down notifications (available in Android since very early versions), but they still celebrated the release of Siri and iOS5 notifications without comparing them to the devices that had offered this support for ages.

I was excited to hear yesterday that Project Sputnik was coming out of beta. As a beta tester, I can testify that the computer, a Dell XPS13 supporting Ubuntu out-of-the-box is excellent – in fact, it may be the single best laptop I’ve purchased.

I fielded a number of questions all day yesterday about the laptop on Hacker News. Most people were asking questions about how it compared to the Macbook Air in the predictable ways: display resolution, keyboard feel, touchpad, suspend support, etc. While I’ve been happy on all of these counts – the Sputnik keyboard, for example, is clearly superior – they’re far from the most important things I look for in a laptop.

Many of the components of the “Apple touch” sound fine in theory. It’s just that, when you get down to brass tacks, I’d gladly trade a metal vs. plastic chassis for tiling window management, focus-follows-mouse, and a Caps Lock key that isn’t hilariously incompatible with Vim. (Not to mention the underlying operating system itself: unified package management, tmpfs support, etc.) And have – I’ve run Linux on other hardware (Samsung, HP, etc.) for some years now as my main computers/laptops.

People who criticise the XPS13 in comparison to the Macbook Air for feature $X are missing the point, just like the Android users who laughed at Siri for implementing a fraction of the voice commands that Android had provided for years. Siri wasn’t made to compete with an Android feature; it was made to compete with previous versions of iOS and targeted at those who would never have considered buying a non-iOS phone.

In the same vein,, I don’t like the Sputnik laptop because the Macbook Air is “good enough” and the Sputnik laptop is better. I like it because I dislike the Macbook Air, and Sputnik just so happens to borrow the few appealing aspects of an otherwise unappealing laptop and adds that to the existing models I know and love.

Android has four times the market share as iOS, but for many iPhone users, switching isn’t even a possibility. The iPhone has transcended the realm of competition for a small minority of the market by competing with nobody but itself. It’s not even close to the dominant mobile platform, but nobody’s complaining about its profits.

Similarly, while Linux may never gain the majority share of the Ultrabook market – even among developers – it doesn’t need to in order to be indispensable for a smaller, but highly profitable, minority of the market.

Not surprisingly, Sputnik’s price bothered some Apple users. While I was fortunate as a beta tester to receive a 20% discount on the Sputnik model I currently have, I would easily have paid the sticker price ($1,549) if I were getting it today. The price compares favorably to the Macbook Air, spec-for-spec2, but again, that misses the point. I would easily pay more for a machine simply because it provides out-of-the-box support for a better OS. Linux may be free-as-in-freedom, but I’d still prefer it even if it weren’t free-as-in-beer as well!

Objects & Procedures: A Python and Java Comparison

Earlier today, I had a discussion with another hackNY fellow about whether Java was more strictly object-oriented than Python. While neither language is truly object-oriented in the pure sense, it’s clear to me that Python is hands-down more consistently object-oriented, whereas Java is a mixture of object-oriented and procedural programming.

Put another way:

Java forces you to write class-based code using both objects and non-objects, requiring you to juggle both objects and non-objects simultaneously.

Python forces you to write object-based code, so you only need to handle objects, ever.

Like a good citizen of the OO world, Python makes objects so transparent and seamless that you don’t even realize that basic calls like addition involve objects.

a = 5
a += 2

As we shall see, Python lets you break out of OO strictness, but only if you really, really want to (which means you also really know what you’re doing.). Python also lets you take advantage of this object-oriented abstraction, by redefining integer addition on odd numbers to perform division instead (for example) – but again, only if you know what you’re doing!

(Python’s type system is a bit complex, so I will have to gloss over some of the details here. If you’re interested in more, I would recommend reading this StackOverflow question about metaclasses in Python and the book Python Types and Objects (available for free online). If anybody know similarly good resources for Java, I will be happy to add them here.)

Object-Oriented programming is just functional programming in disguise

Yes, you heard me! And just so you don’t take my word for it, here’s a 2003 email from Alan Kay (the ‘inventor’ of object-orientation). He closes with the essence:

OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. It can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I’m not aware of them.

(Note that he does not mention inheritance, classes, or polymorphism – having these does not imply object-orientation!

Also note that this email is from 2003, around the height of Java’s popularity.)

Understanding Messages

This is the most important one. You can spend a lot of time studying messaging, but I’m going to cut to the chase and tell you that (spoiler alert!) messages is basically just currying.

In the Smalltalk sense, you have an object, and you can pass messages to it. The result will be another value, which may be another object that you want to pass messages to.

This is no different from functional programming; a method is a higher-order message which takes other parameters (messages) and returns values. Calling a method and providing a parameter to a method are both just messages.

Understanding local retention, protection, and hiding

This could be a blog post in itself, but it’s essentially a comment on implementing encapsulation using scope and closure. An oversimplification: closures are a way to ‘export’ access to local variables while still maintaining control over them (ie, how they can be access/modified, and by whom).

Understanding late binding

This is the sticking point. Python is dynamically typed and uses duck typing. In other words, the following call:

$ foo.bar

does not need to know the type of ‘foo’ – all it does is look up the list of attributes that ‘foo’ has and looks for something called ‘bar’. You can do this manually with the getattr() function or the __dict__ attribute. If that attribute is a callable (a function), you can also do

$ foo.bar()


$ foo.bar(5)

This is more or less the same as passing foo the message ‘bar’, then passing the message ’(’ telling it to access the hidden ’__call__’ attribute, then passing it the message ‘5’ and the message ’)’ telling it to end the call.

What happens if ‘bar’ is not found? This is where things get interesting:

If an attribute is not found in the object, it then looks up the attribute in the class. But classes are objects in Python, so it’s looking up the attribute in another object. This is because Python classes are really just syntactic sugar for prototypical inheritance.
If getattr() doesn’t see the attribute listed in the dictionary of attributes, it then looks up the .__class__ attribute and then looks in the __dict__ associated with that. (The docs provide more precise information).

Don’t be fooled by the name .__class__ – this doesn’t actually have to be a class (at least, not the way you’re probably thinking of a ‘class’!).

This means you can do wacky things like this:

(I’d encourage you to experiment with this yourself! Try modifying the attributes).

In other words, “class Foo(object):” is syntactic sugar that creates an object of class ‘type’ with a ’__name__’ attribute of  ‘Foo’, then binds it to the external variable name Foo. We could do this without a ‘class’ keyword altogether, by writing:

Foo = type(‘Foo’, (object,), {‘apple’ : ‘red’})

Note that the external name and the ’__name__’ attribute are completely independent. 

(In fact, not only can Python objects be created without the ‘class’ keyword, they don’t even need to have classes, as long as they are capable of handling all of the messages that an object with a class can.)

Note: the reason this is possible is not that Python is a dynamic language; the reason this is possible is that Python objects don’t ‘inherit’ from classes; they simply contain a ’__class__’ attribute that is a reference to another object, which can be accessed, mutated, and referenced just like any object.

Objects all the way down…. almost

What happens when you get to the prototype of a prototype of an object (or equivalently, the class of a class of an object)?

To avoid getting into higher-order type theory, I’m going to wave my hands a bit and point you towards the concept of a kind; that is, the type of a type constructor.

It gets down to this: all of Python can essentially be derived from one kind – if you follow the turtles (prototypes) all the way down, you’ll eventually reach some common ground. And you don’t even have to go that far to see the benefits – you can access attributes (functions and values) of objects, classes, and values interchangeably, because they’re all objects. Classes included.

Which brings us to Java….

Everything is an object (except if it’s not)

Java fails to be an object-oriented language for one reason: In Java, all objects have a class, but not all typed values have classes, so some values can have a type without being an object.

This is another way of saying that Java supports (and requires) multiple kinds. This is illustrated very easily by the need for the object autoboxing for primitive types. The following code will fail with an interesting error code:

because an int is not an object. You need to provide an Integer, which behaves the same most of the time…. except when it doesn’t. Many people incorrectly refer to Integer as a ‘wrapper object’, but while it’s not a primitive, it’s not an object either; it can be used interchangeably with objects 98% of the time, but not the remaining 2%.

That 2%, ‘except when it doesn’t’ seems small, but it’s the key to understanding why Java is not actually object-oriented. It’s not quite class-oriented either, but it’s more class-oriented than object-oriented, because you rarely (never?) create objects directly; instead, you create classes (named or anonymous) which you later instantiate as objects (sometimes only once).

The fact that Java is (almost) class-oriented doesn’t make it any more object-oriented; in fact, it makes Java less object-oriented, because classes prevent many of the abstraction properties that make OO programming powerful.

If there’s interest, I could illustrate some of those limitations in Java specifically, though for now, I think I’ll save that for a separate post!

Appropriating Your Cake and Eating It Too

I was very disappointed this evening to find that, with their new API rules, Twitter is trying to eat their cake and still have it too. To put it simply, ask yourself this: would Twitter be what it is today if these policies had existed from the start?

Meetup had its own sink-or-swim moment when it began charging for Meetup groups. Changes like these affect the way the product is received by a network; they don’t change they way the product can be used, but they change the way the product is used.

This happens because network-driven products are eventually appropriated by the network. If you run a network-driven product, you want this. It means your users are teaching you how to use your product, not the other way around. You may even change your company’s vision in response. This is a Good Thing.

The flip side of appropriation is that you no longer own your product. You may think you do. Your server costs may disagree. But you’re mistaken if you think that public control of your product starts with your IPO.

Taking bold moves and yanking your product out from under your users’ feet works for a while. Sometimes it may be the right decision. But Twitter should have realized that the last time they were in this position was in 2008, by my estimate.

From the start, Twitter has grown because it is quasi-universal. The 140 character limit was chosen for compatibility with text-messaging. To say that the third-party clients have been a boost to Twitter’s traffic would be a disservice; Twitter’s own official clients on OS X and iOS were originally third-party creations.

Very early on, Twitter had two choices:

1. Double down on their API, brand themselves as a  platform, and monetize accordingly (data, advertising, or usage-based pricing [which is different from applying for permission!])

2. Become a closed network, with centralized tools for interacting with the network, and monetize off of a walled garden.

Now, the choice is still there – but it’s no longer Twitter-the-company who will be deciding. And it seems Twitter-the-network has a very different vision in mind.

The Irrational Choice

Spectator has defined my college experience in a way no other organization could. I first signed up for the mailing list during a prospective students’ visit over four years ago – before I even was a student at Columbia – and have been there ever since. After all that time, having held multiple titles (including president), I finally got to see my own words printed today in the paper for the first, and only, time as a student here.

Speech is free, but words aren’t cheap. My college career, in 785 words: The Irrational Choice.

Online Piracy: The New ‘War on Drugs’

Recently, I was asked to lead a discussion for a Columbia student group about internet ‘piracy’. The topic isn’t synonymous with SOPA/PIPA/ACTA, but those bills obviously came up. Glenn Greenwald does an excellent job explaining why, even if SOPA is dead, we may still be losing the war.

This is what I have to add about all of those bills.

No law will ever stop online filesharing (in the long run).

Copyright, copyleft, public domain, whatever – it doesn’t matter. Technology moves too fast, and government moves too slowly. We’ve seen it time and again – they shut down Napster, then Kazaa popped up. Kazaa came under fire, then torrenting became popular. Now the trend is towards magnet links, which are even easier to share, and even harder to censor. Temporary setbacks aside, if you really want to keep getting digital media for free, I’d bet almost anything you’ll still be able to in ten, fifteen, twenty years. (If we haven’t yet moved beyond digital media to telepathy by then, that is).

It’s worse than the drug war, because it’s not even a fair battle. You need a lot of cooperation in Congress to get a bill passed, but you only need one Shawn Fanning or one gojomo to come up with a brilliant innovation that turns everything on its head again. At best, this is an arms race – an arms race in which the technological innovators have the upper hand every time. 

Even with every law in the world on yours side, if you can’t even stop people from buying and selling drugs, which are rival, excludable, and expensive[1], don’t kid yourself thinking you can stop people from consuming a good that costs nothing to reproduce, and at zero marginal cost to both supplier and consumer.

These laws should be applied universally, or not at all.

The natural counterargument to my first point is that ACTA/SOPA/PIPA would/should only go after ‘the worst offenders’. That’s even worse. If you’re going to censor information, it has to be censored equally and uniformly. That’s the difference between applying a law out of principle and applying a law because it suits your own interests.

That statement applies to many laws, and it’s why I feel so strongly about it. With the war on drugs, we also go after ‘only the worst offenders’, and look where it’s gotten us. Any law applied selectively is applied with a bias, whether intentionally or not – it’s just that the bias may not be obvious.

With the war on drugs, the real victims are primarily racial minorities (especially black/Latino), the poor, and youth. (This is such common knowledge that I won’t even bother linking to sources, but if you really don’t believe me and can’t find a credible source, let me know.)

Sometimes, that’s the entire point. There are plenty of examples of laws which have been used primarily – even exclusively – for purposes completely unrelated to the original ones. The PATRIOT act is only the most recent controversy that comes to mind; U.S. (and world) history is littered with other examples. The recent ‘child porn’ law, if it it passes, would eventually be used for far more than child pornography – mark my words. Child porn is just a convenient red herring.

The war on drugs marginalizes certain groups so dramatically that I’ve heard it referred to as ‘the new Jim Crow’. With ACTA/SOPA/PIPA, I don’t know who the real victims would be. Certainly not ‘the worst offenders’. Maybe youth? Political activists? Independent musicians? I don’t know. All I know is that I don’t want to find out.

But ‘pirates’? Nah. Don’t worry about them. The government will ‘stop’ filesharing, just like it ‘stopped’ drug use.

[1] Even if you don’t trust the DEA’s published list of street prices – and I wouldn’t blame you for not – I think it’s clear that the street price for any drug is greater than $0.

 One of the nice things about living in New York is that I have…

 One of the nice things about living in New York is that I have the opportunity to experience the Occupy Wall St. protests directly, without the filter of any secondary source. Last weekend, I was in the financial district, so I took a short walk to Zuccotti Park, the campground that protesters now dub ‘Liberty Park’. At the bottom are two of my favorite pictures from the trip.

After seeing the protests firsthand, all I can say is this: these people aren’t kidding around.

I can’t say that I agree with all of their concrete goals. But whether or not you do agree with them, it’s clear that this isn’t some temporary fad. Before, I was shocked when Mayor Bloomberg backed down on his plans to clear the park, but after visiting the park, I can see why. On this Saturday afternoon, the park was filled with an incredibly diverse range of people – not at all some easily dismissed ‘fringe group’. But here’s the kicker – the part that should really terrify anybody who wishes that the Occupy Wall Street protests would just go away:

These people look happy.

I’ve seen my fair share of protests before, and people tend to look fired up and ready for action. Sure, the OWS protesters are angry and ready for action too. But they’re also clearly comfortable where they are – camping in sleeping bags on a semi-private park in downtown Manhattan. With a band playing in one corner and street cart food vendors all around the edge of the park, it wasn’t too hard to close my eyes and imagine myself in a modern-day Woodstock. I talked briefly to a few people, and they seemed like a political protest was exactly what they wanted to be doing on a Saturday afternoon, thank you very much. 

And that was only a few hours before thousands of them stormed Times Square.

When the weather gets cooler around mid-November, I expect the crowd in Zuccotti Park to shrink, but that doesn’t mean I think Occupy Wall Street will die down. Already there are signs that it is spreading to universities like Columbia (which, in case you’ve forgotten, has quite a history of protest).

You may not agree with the objectives of Occupy Wall St. – I find some of their goals problematic myself – one thing is clear. When you have a bunch of people protesting out of anger and frustration, there’s trouble ahead. But when you have a bunch of people protesting from anger and frustration and genuinely enjoying the fight, you’d better believe that they won’t just fade away.

(I’ve spoken to some friends in other cities who are under the impression that this is a ‘dreads vs. suits’ battle. These two photos that I took last week tell a different story – one of the reasons I believe that this isn’t going away anytime soon.)

Marrow Matters

This weekend, a friend of mine sent me a link to the Be The Match registry, which allows people to register to donate bone marrow. Because of the very small chance of finding a match, the registry is in need of more donors, particularly for certain ethnic groups. 

The process is simple. You register to receive a kit. When it arrives in the mail, you take a cheek swab and send them the sample. If a patient matches your sample, you receive a call and have the option of being a donor.

But when I tried to register, I saw this.

(In case you can’t read it, it says: “7. (Men only) In the past 5 years have you had sex, even once with another male?”)

I tested it out. Sure enough, if you select ‘yes’, the site will not let you take the test. It doesn’t stop you from donating marrow – it stops you from even taking the test to find out if you are a match.

This seems silly to me for a few reasons. And if anybody can explain this to me, I’d honestly appreciate it, as it makes no sense to me.

First, while I understand the fear of HIV, this is a bit misguided. In the early 1980s, the cause of AIDS was uncertain, so this screening question would have been very useful. Nowadays, gay men are not necessarily the demographic most prone to HIV, yet MSM (men who have sex with men) are one of the few demographics explicitly barred from registering. The question makes no attempt to distinguish between high-risk, unprotected sex and low risk, protected sex. Think about the ridiculousness of this for a moment – a hypothetical straight woman who never uses condoms with her many sexual partners is eligible, while a gay man in a monogamous relationship who always uses protection is not. Ceteris paribus, who would you rather receive marrow/blood from?

Second, a test is not equivalent to a donation! This is very different from a blood donation, which is a one-time, anonymous commitment. A blood donor donates in advance of an unidentified patient’s need. A marrow donor only donates once the recipient patient has been identified. Once a match is found, the donor has to submit to a battery of medical tests. A marrow donor must also follow up several times, which makes it easy to test for HIV or other blood-transmitted diseases. While the seroconversion process varies in length, HIV is detectable in most people within 90 days of exposure. Yes, there are false negatives, but this is always a known risk for blood donations and is not exclusive to MSM.

Furthermore, once registered, a person remains in the registry until the age of 60. A five-year ban makes little sense in this context, especially given that a person’s sexual behavior may change greatly between 18 (the minimum age) and 60 (the maximum age).

Finally, this policy is hardly more secure than no policy at all. Why? Imagine that you are a gay man, and your father/mother/friend is in need of a transplant. Are you going to let this checkbox prevent you from even finding out if you can save their life? Probably not. If you really care about the person, you’ll lie. Or at least be tempted to.

And that’s the real problem. This policy only works if people are honest. But not everyone is – particularly for such an emotional issue. People can be dishonest for a variety of reasons, with good or bad intentions. People can also be unaware of their HIV status – particularly straight men and women, who are not subjected to the same level of HIV education/PSAs and may not get tested as frequently. Because of this, blood tests need to be run on donors for the sake of safety, which makes a question like this one rather uninformative.

Personally, I can’t find a problem with allowing people to register and then testing them if they are matched. People who know they have HIV or hepatitis are already prevented from registering. I don’t understand the problem with at least testing for matches for the rest.

If someone can justify this policy to me, I would really appreciate it. But the way I see it, HIV kills enough people. Let’s not let the fear of HIV put even more lives at risk.

(If you are a woman or a heterosexual man and you do not use intravenous drugs, you are eligible to register to donate bone marrow, and I encourage you to do so. People like Amit Gupta and others are depending on your generosity!) 

How many bloggers does it take to change a lightbulb?

How many computer scientists does it take to change a lightbulb?

None – that’s a hardware problem.

How many industrial designers does it take to change a lightbulb?

None – they just convince you that darkness is a feature.

How many hipsters does it take to change a lightbulb?

It’s an obscure number… you’ve probably never heard of it.

How many Ubuntu users does it take to change a lightbulb?

One. Just run apt-get install -f.

How many Archers does it take to change a lightbulb?

Ten. One two change it and nine to marvel at the Arch Bulb System.

How many sysadmins does it take to change a lightbulb?

None. The first time it broke, the admin scheduled a CRON job to handle all future lightbulb maintenance.

How many Java programmers does it take to change a lightbulb?

Three. One to create a Lightbulb, one to create a LighbulbFactory, and one to create an AbstractLightbulbFactory.

How many Perl progammers does it take to change a lightbulb?

Four, but they can do it in three characters of code.

How many Python programmers does it take to change a lightbulb?

One. Just import anti-darkness.

How many Ruby programmers does it take to change a lightbulb?

Five, but they each figure out eight ways to do it.

How many Lisp programmers does it take to change a lightbulb?

Seven. (Only (one) does (the work, (but he has to bo(rrow) parentheses from the rest.))

How many Windows users does it take to change a lightbulb?

One, but he’ll insist on using 240 volts until it becomes the new standard.

How many Macintosh users does it take to change a lightbulb?

None – they have to take it to their local Apple store to be serviced.

How many Emacs users does it take to change a lightbulb?

Three. One two change the lightbulb, and two to adjust his carpal tunnel brace.

How many Vim users does it take to change a lightbulb?

ne, as long as he doesn’t forget to enter ‘insert’ mode first. [sic]

(I was only planning on posting three, but I was having too much fun with this.)