Clojure, SLIME, ODBC, SQL Server

I had a lot of trouble connecting to an MS SQL Server at work via Clojure. Java 6 comes with a JDBC-ODBC bridge which worked fine from a Clojure REPL at a command prompt, or from inferior-lisp in Emacs, but in SLIME it would hang every time I tried to connect and I'd have to kill Java. Couldn't for the life of me figure out why.

I got it to work eventually by using Microsoft's own JDBC driver, which you can download here.

Once you put the downloaded .jar file on your CLASSPATH (in my case, sqljdbc4.jar) you can connect like this:

user> (def db {:classname "com.microsoft.sqlserver.jdbc.SQLServerDriver"
               :subprotocol "sqlserver"
               :subname "//server_hostname;database=SomeDatabase;user=SomeUser;password=SomePassword"})
#'user/db
user> (use 'clojure.contrib.sql)
nil
user> (with-connection db 
        (with-query-results rs ["SELECT * FROM whatever"] (prn rs)))
... results ...

Posted for the sake of Googlebot and for my own future sanity.

Five Things that Mildly Annoy Me in Clojure

This infamous blog post suggests that someone familiar with a language should be able to name five things they hate about it. "Hate" is a strong word, but I decided to think of five things I find mildyly annoying about Clojure, my favorite language of the moment.

Hashing integers

Clojure automatically converts integers between Integer, Long and BigInteger as needed to prevent overflow. This is good. Integers of the various classes test as equal too. This is also good.

user> (= 123 (int 123) (long 123) (bigint 123))
true

So would you expect this?

user> (hash-map (int 123) :foo (long 123) :bar (bigint 123) :baz)
{123 :foo, 123 :bar, 123 :baz}

Yes, each of the integer classes, though equal via =, do not have the same hash value when put into a hash-map. This is because:

user> (.equals (int 123) (long 123))
false

This is a wart inherited from the JVM. See here for discussion and explanation.

What's more, if you print this map and then try to read it back in, the integers will be read as int, long or bigint arbitrarily depending how big they are. This means you may not get the same class of object back that you output originally.

user> (def x {(bigint 123) :foo})
#'user/x
user> (= x x)
true
user> (def y (read-string (pr-str x)))
#'user/y
user> (= x y)
false
user> (class (first (keys y)))
java.lang.Integer
user> (class (first (keys x)))
java.math.BigInteger

This means that if you ever use integers as hash keys, you must be very careful to cast them all to the same integer type manually.

Metadata doesn't work on everything

Clojure lets you stick arbitrary metadata on various objects. This is higly useful; you can decorate objects with information that doesn't affect the value of the object. However metadata doesn't work everywhere.

user> (with-meta "foo" {:bar :baz})
java.lang.ClassCastException: java.lang.String cannot be cast to clojure.lang.IObj (NO_SOURCE_FILE:0)

You can only stick metadata on certain Clojure objects like Symbols, Vars, Refs, Agents, all of the Clojure collections and so on. You can't stick metadata on, say, a String or an Integer, because those are closed Java classes and can't be touched. It would be nice if you could.

use vs. require vs. import vs. load vs. ...

There are a startling number of ways to import a library into your code in Clojure. You have to choose from load, import, require, use, refer, and so on. Some work on Java classes, some work on Clojure libs. Some of them import symbols into your namespace, some of them don't. Some of them take strings as arguments, some take symbols, some take quoted lists of symbols, some take quoted lists of symbols with sub-lists of arguments. And all of these can be and usually are weirdly inlined into a namespace declaration, with a completely different list-quoting style.

So in Ruby you can do this:

require 'util'
require 'config'
require 'whatever'

Whether it's a gem, or a Ruby source file sitting locally, it all works the same as long as the load path is set up right.

But in Clojure, you do this (actual code from an IMAP library I wrote):

(ns qt4-mailtray.mail
  (:import (java.util Properties)
           (javax.mail Session Store Folder Message Flags Flags$Flag FetchProfile FetchProfile$Item)
           (javax.mail.internet InternetAddress))
  (:use clojure.contrib.str-utils))

This can quickly become unwieldy, especially if you start using the :as or :only or :rename arguments. It's made worse by Java's insane API's full of a billion classes that you need to import to do simple things. (And those things with dollar signs are mangled Java inner class names.) Clojure also lacks the ability to import a whole package worth of classes at once using java.io.* syntax, so you must name all of the classes explicitly.

every? vs some.

This is such a trite pet-peeve that it's barely worth mentioning, but it seems to be brought up repeatedly and endlessly on the Clojure mailing list so at least I'm not the only one bugged by it.

Clojure has a function (every? pred coll) which tests whether every item in a collection tests true via some predicate. To test whether every item in a collection tests false, we have not-any?. And we have a not-every? which tests whether any item tests false.

user> (every? even? [2 4 6])
true
user> (not-every? even? [2 4 6])
false
user> (not-any? even? [2 4 6])
false

Now what would you expect a function to be called which tests whether any item in a collection tests true via some predicate? If you said any? you are wrong! It's some.

Note that some isn't a predicate (hence no question mark in the name); it doesn't return true or false, as above, but rather returns the result of running pred on an item in coll.

user> (some identity [nil 1 2 3])
1

any? is pretty easy to write so it doesn't matter that much. Probably many people have an identical function sitting in some utils.clj file on their systems.

(defn any? [pred coll]
  (when (seq coll)
    (if (pred (first coll))
      true
      (recur pred (next coll)))))

Stack trace madness

Give this function:

(defn foo []
  (throw (Exception. "BARFED")))

What does the stack trace look like in SLIME when you call foo? Like this:

java.lang.Exception: BARFED (NO_SOURCE_FILE:0)
  [Thrown class clojure.lang.Compiler$CompilerException]

Restarts:
 0: [ABORT] Return to SLIME's top level.
 1: [CAUSE] Throw cause of this exception

Backtrace:
  2: swank.commands.basic$eval_region__729.invoke(basic.clj:36)
  3: swank.commands.basic$listener_eval__738.invoke(basic.clj:50)
  4: clojure.lang.Var.invoke(Var.java:346)
  5: user$eval__1506.invoke(NO_SOURCE_FILE)
  6: clojure.lang.Compiler.eval(Compiler.java:4580)
  7: clojure.core$eval__4016.invoke(core.clj:1728)
  8: swank.core$eval_in_emacs_package__336.invoke(core.clj:55)
  9: swank.core$eval_for_emacs__413.invoke(core.clj:123)
 10: clojure.lang.Var.invoke(Var.java:354)
 11: clojure.lang.AFn.applyToHelper(AFn.java:179)
 12: clojure.lang.Var.applyTo(Var.java:463)
 13: clojure.core$apply__3269.doInvoke(core.clj:390)
 14: clojure.lang.RestFn.invoke(RestFn.java:428)
 15: swank.core$eval_from_control__339.invoke(core.clj:62)
 16: swank.core$eval_loop__342.invoke(core.clj:67)
 17: swank.core$spawn_repl_thread__474$fn__505$fn__507.invoke(core.clj:173)
 18: clojure.lang.AFn.applyToHelper(AFn.java:171)
 19: clojure.lang.AFn.applyTo(AFn.java:164)
 20: clojure.core$apply__3269.doInvoke(core.clj:390)
 21: clojure.lang.RestFn.invoke(RestFn.java:428)
 22: swank.core$spawn_repl_thread__474$fn__505.doInvoke(core.clj:170)
 23: clojure.lang.RestFn.invoke(RestFn.java:402)
 24: clojure.lang.AFn.run(AFn.java:37)
 25: java.lang.Thread.run(Thread.java:619)

Yeouch. Now imagine that the above error is coming not from a simple function, but from some random line among hundreds of lines of source code.

Stack traces in Clojure will often tell you little to nothing about what is causing the error, or more importantly, where it's coming from in your code. Clojure functions are translated into Java classes when they're run through the JVM. Often can't even see the name of the function that's throwing the error; names are mangled into things like user$eval__1473.invoke, which is really really confusing when you use anonymous functions.

Per Jason Wolfe and Randall Schulz sometimes you can get a better stack trace if you dig a bit deeper:

user> (.printStackTrace (.getCause *e))

java.lang.Exception: BARFED
    at user$foo__1503.invoke(NO_SOURCE_FILE:1)
    at user$eval__1509.invoke(NO_SOURCE_FILE:1)
    at clojure.lang.Compiler.eval(Compiler.java:4580)
    at clojure.core$eval__4016.invoke(core.clj:1728)
    at swank.commands.basic$eval_region__729.invoke(basic.clj:36)
    at swank.commands.basic$listener_eval__738.invoke(basic.clj:50)
    at clojure.lang.Var.invoke(Var.java:346)
    at user$eval__1506.invoke(NO_SOURCE_FILE)
    at clojure.lang.Compiler.eval(Compiler.java:4580)
    at clojure.core$eval__4016.invoke(core.clj:1728)
    at swank.core$eval_in_emacs_package__336.invoke(core.clj:55)
    at swank.core$eval_for_emacs__413.invoke(core.clj:123)
    at clojure.lang.Var.invoke(Var.java:354)
    at clojure.lang.AFn.applyToHelper(AFn.java:179)
    at clojure.lang.Var.applyTo(Var.java:463)
    at clojure.core$apply__3269.doInvoke(core.clj:390)
    at clojure.lang.RestFn.invoke(RestFn.java:428)
    at swank.core$eval_from_control__339.invoke(core.clj:62)
    at swank.core$eval_loop__342.invoke(core.clj:67)
    at swank.core$spawn_repl_thread__474$fn__505$fn__507.invoke(core.clj:173)
    at clojure.lang.AFn.applyToHelper(AFn.java:171)
    at clojure.lang.AFn.applyTo(AFn.java:164)
    at clojure.core$apply__3269.doInvoke(core.clj:390)
    at clojure.lang.RestFn.invoke(RestFn.java:428)
    at swank.core$spawn_repl_thread__474$fn__505.doInvoke(core.clj:170)
    at clojure.lang.RestFn.invoke(RestFn.java:402)
    at clojure.lang.AFn.run(AFn.java:37)
    at java.lang.Thread.run(Thread.java:619)

This one at least mentions foo by name but you're still going to have a headache after a few hours of those stack traces.

Conclusion

So that's five things. You will notice a common theme. Most of these issues are inherited from the JVM. This is to be expected, I suppose. There's no way you can wrap one language in another without a few compromises.

But these things aren't show-stoppers. They are minor annoyances compared to the benefits you get from using the JVM, i.e. the good performance, tons of libraries, cross-platformness, and so on. Clojure is fun enough to work with and wart-less enough that it took me well over two weeks to write this post.

(If you were expecting me to mention loop/recur and the lack of native TCO in the JVM, you were PAINFULLY WRONG. No one who uses Clojure loses sleep over native TCO. It's largely a non-issue that's endlessly repeated by people looking for an excuse to pass up Clojure in favor of $their_pet_language. To each his own, but I have never found myself caring the slightest about loop/recur.)

ABLE

I learned recently of ABLE, which is a minimalistic GUI editor + REPL for Common Lisp. You download it and run it and there you go, Common Lisp with lots of included libraries, no Emacs trial-by-fire necessary.

This is a really great idea in my opinion. I wish I'd had this a year or two ago. Common Lisp has too many barriers for newbies to get started. Anything that removes a few of them can only benefit the Lisp world.

I paid for music

As a general rule, I don't pay for music. The main reason of course is that the music industry are a bunch of thugs. If you don't know that already, you've been living under a rock for the past few decades. I won't even buy music for other people as a gift if I can help it.

Recently however I did buy music, specifically Jonathan Coulton's latest DVD. JoCo releases his music under Creative Commons, which is awesome, and when you buy it (from What Are Records) you get MP3s that are not infested with DRM, which is also awesome. When you buy that particular DVD, you get a DVD of the concert, a music CD of the same concert, AND you can immediately download MP3s of said concert while you wait for the DVD in the mail. All for $20. Well worth it for such quality music.

I first heard most of JoCo's music via shaky concert recordings on Youtube and via MP3s acquired "elsewhere" (nearly all of which are free downloads on Joco's website though); otherwise I'd never even have known he existed. And yet I ended up giving him my money, happily and willingly, and probably will again. Amazing how things turn out.

The other music I bought recently is Stephen Lynch. Again I heard most of his music first on Youtube. Again I gleefully spent money on his latest CD because it's good music and because it's DRM-less and thug-less entertainment and a good portion of that money is going to the artists.

Most of the music I like comes from Japan or various corners of Europe. Amazon sells a few (very few) Japanese music CDs, for between $50 and $90 each (plus shipping). Do you know how much it costs to ship a stream of bytes from Japan to the US via the intertubes? Hint, it's not $90. How does a stream of bytes increase $90 in value when it's written onto a piece of plastic?

These are strange times. There's such disparity between what the average person believes is right and wrong on the internet and what the law says is lawful and unlawful. This kind of disparity can't last forever. My high school history teacher said that in America at least, a law that is opposed by the majority of citizens in the country never lasts long; I think that's true. And it's as it should be. In a few decades, we're going to look back at how things were in the 90's and 00's and laugh.

More Clojure Mandelbrot Goodness

* This page is a child of "Clojure: ASCII Mandelbrot Set".

After my brief stint in the world of fractal geometry and Clojure, I decided to make a real Mandelbrot set viewer. The resulting source code is here. Here's a simple output (click for bigger version):

/clojure/mandelbrot/thumbs/mandelbrot-smooth.png

It's a pretty naive implementation, barely 100 lines of code, but even with my brute-force approach, given a liberal sprinkling of type hints it runs fast enough. Programming Swing from Clojure couldn't be easier (though I doubt programming Swing from any language is ever really enjoyable, it's a painful bunch of libraries).

There's a discussion of different coloring algorithms on Wikipedia, but even after reading that, getting this thing to look good was difficult. I don't know enough math for it. I ended up cheating and I colored a couple of them in the GIMP, so I could use them as desktop wallpapers.

/clojure/mandelbrot/thumbs/mandelbrot-rainbow.png /clojure/mandelbrot/thumbs/mandelbrot-rainbow-2.png /clojure/mandelbrot/thumbs/mandelbrot-rainbow-3.png

/clojure/mandelbrot/thumbs/mandelbrot-rainbow-4.png /clojure/mandelbrot/thumbs/mandelbrot-rainbow-5.png

There are some more PNGS over here including one that's 16000x16000 (producing it almost melted my CPU last night).

Who needs a DB?

My blog is still working, in spite of my best efforts to crash it. So that's good. But lately I've been thinking that an SQL database is a lot of overkill just to run a little blog like this.

My blog only has around 450 posts total (over the course of many years), and about an equal number of user comments (thanks to all commenters!). Why do I need a full-blown database for that? All of my posts plus comments plus all meta-data is only 2 MB as a flat text file, 700k gzipped.

By far the most complicated part of my blog engine is the part that stuffs data into the database and gets it back out again in a sane manner (translating Clojure data to SQL values, and back again; splitting up my Clojure data structures into rows for different tables, and then re-combining values joined from multiple tables into one data structure). Eliminating that mess would be nice.

Inevitably I ended up with some logic in the database too: enforcing uniqueness of primary keys, marking some fields as NOT NULL, giving default values and so on. But a lot of other logic was in my Clojure code, e.g. higher-level semantic checking, and some things I wanted to set as default values were impossible to implement in SQL.

Wouldn't it be nice for all the logic to be in Clojure? And the data store on disk to be a simple dump of a Clojure data structure? I can (and did) write a few macros to give me SQL-like field declaration and data validation, for uniqueness of IDs and data types etc. For my limited needs it works OK.

The next question is what format to use for dumping to disk. Happily Clojure is Lisp, so dumping it as a huge s-exp via pr-str works fine, and reading it back in later via read-string is trivial.

Some Java data types can't be printed readably by default, for example java.util.Dates, which print like this:

#<Date Wed May 20 22:39:00 PDT 2009>

The #<> reader macro deliberately throws an error if you try to read that back in, because the reader isn't smart enough to craft Date objects from strings by default. But Clojure is extensible; you can specify a readable-print method for any data type like this:

(defmethod clojure.core/print-method java.util.Date [o w]
  (.write w (str "#=" `(java.util.Date. ~(.getTime o)))))

Now dates print as

#=(java.util.Date. 1242884415044)

and if you try to read that via read-string, it'll create a Date object like you'd expect.

user> (def x (read-string "#=(java.util.Date. 1242884415044)"))
#'user/x
user> (class x)
java.util.Date
user> (str x)
"Wed May 20 22:40:15 PDT 2009"

Storing data in a plain file has another benefit of letting me grep my data from a command line, or even edit the data in a text editor and re-load it into the blog (God help me if that's ever necessary).

Having multiple threads banging on a single file on disk is a horrible idea, but Clojure refs and agents and transactions handle that easily. But I do have to work out how not to lose all my data in case the server crashes in the middle of a file update. (I've lost data (in a recoverable way) due to a server crash in the middle of a MySQL update too, so this is a problem for everyone.) Perhaps I'll keep a running history of my data, each update being a new timestamped file, so old files can't possibly be corrupted. Or use the old write-to-tmp-file-and-rename-to-real-file routine. Or heck, I could keep my data in Git and use Git commands from Clojure. It'd be nice to have a history of edits.

If this idea works out I'll upload code for everything to github, as usual.

Clojure: ASCII Mandelbrot Set

Did you know there's this neat Lisp message board where from time to time someone posts a short problem similar in spirit to the infamous RubyQuiz?

Not a lot of people have participated so far, hopefully that changes. I participated this time; the problem is to render the Mandelbrot Set in ASCII. Here's my Clojure version (based loosely on this one).

(ns mandelbrot
  (:refer-clojure :exclude [+ * <])
  (:use (clojure.contrib complex-numbers)
        (clojure.contrib.generic [arithmetic :only [+ *]]
                                 [comparison :only [<]]
                                 [math-functions :only [abs]])))

(defn- mandelbrot-seq [x y]
  (let [z (complex x y)]
    (iterate #(+ z (* % %)) z)))

(defn- mandelbrot-char [x y]
  (loop [c 126
         m (mandelbrot-seq x y)]
    (if (and (< (abs (first m)) 2)
             (> c 32))
      (recur (dec c) (rest m))
      (char c))))

(defn- mandelbrot-line [xs y]
  (apply str (map #(mandelbrot-char % y) xs)))

(defn- m-range [min max num-steps]
  (range min
         max
         (/ (+ (abs min)
               (abs max))
            num-steps)))

(defn mandelbrot [rmin rmax imin imax]
  (let [rows 30
        cols 50
        xs (m-range rmin rmax cols)
        ys (m-range imin imax rows)]
    (dorun (map #(println (mandelbrot-line xs %)) ys))))

(comment
  ;Example run:
  (mandelbrot -2.0 1.0 -1.5 1.5)
"
~~~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
~~~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
~~~~~~~~~}}}}}}}}}}}}|||||||||}}}}}}}}}}}}}}}}}}}}
~~~~~~~}}}}}}}}|||||||||||||||||||||}}}}}}}}}}}}}}
~~~~~~}}}}}||||||||||||||{{{{zlxz{{{||||}}}}}}}}}}
~~~~~}}}}|||||||||||||{{{{{zzyxpvlz{{{||||}}}}}}}}
~~~~}}}|||||||||||||{{{{{{zzyxvnpwyzz{{{||||}}}}}}
~~~}}|||||||||||||{{{{{{zzyyws   .vyzzz{{|||||}}}}
~~~}||||||||||||{{{{{zzxwwwvus   muvxyywz{|||||}}}
~~}|||||||||||{{{zzzzyyu= p         oteqpz{|||||}}
~~||||||||||{zzzzzzyyyvtm              oxz{{|||||}
~}|||||{{{zyvwxxxxxxxwrG                vuz{|||||}
~||{{{{{zzzywsMsqRovvs                  pxz{{|||||
~|{{{{{zzzyxsq      pj                  `xz{{|||||
~{{{{yyyxwsrp                           wyz{{|||||
~?:3 3 #                              ovxzz{{|||||
~{{{{yyyxwsrp                           wyz{{|||||
~|{{{{{zzzyxsq      pj                  `xz{{|||||
~||{{{{{zzzywsMsqRovvs                  pxz{{|||||
~}|||||{{{zyvwxxxxxxxwrG                vuz{|||||}
~~||||||||||{zzzzzzyyyvtm              oxz{{|||||}
~~}|||||||||||{{{zzzzyyu= p         oteqpz{|||||}}
~~~}||||||||||||{{{{{zzxwwwvus   muvxyywz{|||||}}}
~~~}}|||||||||||||{{{{{{zzyyws   .vyzzz{{|||||}}}}
~~~~}}}|||||||||||||{{{{{{zzyxvnpwyzz{{{||||}}}}}}
~~~~~}}}}|||||||||||||{{{{{zzyxpvlz{{{||||}}}}}}}}
~~~~~~}}}}}||||||||||||||{{{{zlxz{{{||||}}}}}}}}}}
~~~~~~~}}}}}}}}|||||||||||||||||||||}}}}}}}}}}}}}}
~~~~~~~~~}}}}}}}}}}}}|||||||||}}}}}}}}}}}}}}}}}}}}
~~~~~~~~~~}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
"
)

And here's some obligatory Jonathan Coulton.

Microsoft, you still surprise me

I use Windows XP at work (not by choice) and I've been continually saying "no" when it tried to install SP3. Why? No tangible reason other than that decades of experience with Windows has shown me that any time you touch any system files or settings in Windows, crap breaks. When it comes to Windows, you set things up and then like a teetering house of playing cards, you back away slowly and try not to breathe.

Which brings us to the other day. I first noticed something was up when a got a popup dialog on my work machine asking me every 15 minutes whether I wanted to Reboot Now or Reboot Later. Confused, I clicked "later" but again and again and again this prompt appeared. After hours of this interrupting my futile attempts at work I relented; I laboriously shut down my half-dozen command prompts and carefully-placed Vim sessions and various server daemons and all the tools I got to look forward to re-opening after Yet Another Unnecessary Reboot, and then I rebooted.

So then XP left me alone and all was well with the world. Ha, just kidding, it started doing the same thing again almost immediately. Reboot Now or Reboot Later? I hatefully tolerated this for as long as I could but it was a futile battle. Microsoft won in the end and I rebooted again.

A few other people at work reported the same thing on their systems, so I thought maybe it was a virus, but I checked a few things and noticed a shiny new SP3 installed on my system (so my initial guess was close). Somehow SP3 was forced onto my machine, not sure if it was the sysadmins pushing it out or Microsoft's doing, but either way: why was it possible to install a Service Pack on my machine without my even being aware it happened? I do not consider this a good thing.

In any case, after the second reboot, strange things happened. My taskbar settings were all reverted to defaults and I noticed my Address Bar was missing. The Address Bar is a little URL/file path bar in the taskbar where you can type a file path and open an Explorer window quickly. One of the very few semi-useful bits of the XP interface.

But it was gone. What happened? A short Google later and I learned that Microsoft removed the feature in SP3 permanently, by design. Why? Because of anti-monopoly regulatory concerns.

Wow. So it turns out I wasn't disappointed, and a few dozen cards toppled from the shaky tower as I watched, helpless. Not the end of the world, but what an annoyance.

The reason I bothered blogging this is because, hilariously enough, you can still add the Address Bar back in SP3. As I read somewhere or other, probably here, you simply 1) Drag a "My Computer" icon to the top of the screen to make a useless "My Computer" toolbar, 2) Right click that and add the Address Bar, which is still an option there, 3) Drag that Address Bar to your main taskbar, 4) Remove the useless toolbar from above. And then you have your Address Bar back. Oops!

So, in summary:

  1. Two forced reboots via 20 repeated un-ignorable popup prompts.
  2. Service Pack installed without my knowledge or consent.
  3. Useful piece of functionality removed.
  4. Item 3 caused by a history of monopolistic business practices and the resulting legal fallout.
  5. Functionality in question removed so incompetently that it can be added back anyways in a matter of seconds.
  6. Another hour of my life sucked into the black hole of the Microsoft Windows User Experience™, forever lost.

Git tutorial

Finally I found a good Git tutorial that starts from the absolute basics and goes steadily through more advanced things. I highly recommend it.

Crackberry Acquired

All I ever wanted out of life was to SSH to my computer from a cell phone. That dream has finally come true.

Up to this point I have not owned a cell phone. I bought one a few years back, then I returned it and got a refund because it was pointless. Communicating with other human beings via spoken voice? How trite. My current employer gave me a phone for free but I never used it.

But nowadays cell phones are pretty much mini computers that happen to be able to make phone calls as a side effect. I almost got an iPhone, but I am very wary about hype. Apple's business practices turn me off; the app store is a shystering waiting to happen, their crappy proprietariness makes me puke, their overblown marketing and "image" makes me puke even more. I don't want an MP3 player in my phone; my Cowon D2 is far superior to any silly iPod. And as I tried the touch screen keyboard, I quickly realized that the Blackberry's physical keys win in that category by a mile.

So I got a Blackberry Bold and I'm pretty happy with it so far. I have yet to make a single phone call, but I've put it to good use. I installed all kinds of silly stuff on there, including an SSH client so I can do system maintenance while driving. (Not really, don't worry.) I can look at Google maps when I get lost, which happens embarrassingly often in my car. I can look at Slashdot from the sushi restaurant. I can get the weather updated every 15 minutes, which saves me from rotating my head 25 degrees and looking out the window.

I still object to certain cell phone things on principle. Paying $3 for a 15-second song clip as a ring tone for example; the insanity of this is almost physically painful to me. The Blackberry let me set any old MP3 I wanted as the ring tone though, which is nice.

Paying for text messages is almost as painful. How can it cost a quarter to send 160 bytes of text to another phone, when the whole freaking internet costs orders of magnitude less? How do cell phone companies get away with this? It's such a racket. But I can put IM clients on my phone and use email and I have "unlimited" data transfer each month, so that's nice. (And I really grilled the salesperson about what "unlimited" means. She said some people go into the gigabytes of transfer each month without consequence, so it looks like I need to find a torrent client now!)

Maybe one of these days I'll call someone. What a novel concept.