Blog

All Posts (58)

Apple Maps GPS is way better than Google Maps GPS

Because:

* It gives you the information you need (turn arrow and distance to the turn) in a high-contrast, large font. With Google Maps, I glance down and I can hardly read the information – it's dangerous.

* The street lines are thicker and contrast strongly against the background (unlike Google Maps, which is white on cream).

* The view is tilted, like a regular GPS, unlike Google Maps, which is straight down.

Read more…

Vine video as the sonnet of this generation

I have been wondering what is the attraction of Vine videos - those six-second looping videos.

Then today I was reading about the sonnet (16-line poems), and how popular they were in England in the 16th century. Maybe Vine is the sonnet of this generation?

Consider the following description of the sonnet - note that it has some similarity to a Vine video with its 6-second constraint:

Very often these love poems were addressed to a small circle of congenial spirits—fellow poets, wits, ladies of fashion—whom the poet wished to amuse. The way to impress this company was to exhibit special ingenuity in devising new variations on the old theme or in polishing one's work to an impressively high gloss. The reproach that such poetry is “insincere” would have astonished any Elizabethan. The rules were there with all the weighty authority of tradition behind them, and they were there to be followed. To depart from them would be as incomprehensible as if a chess player were to propose a change of rules to his opponent in order that he “might express himself better”.

—Adventures in English Literature

Read more…

Programming by Equivalence

I would like to introduce a programming technique called “Programming by Equivalence”. You can't use it all the time, but when you can use it, it’s great.

Problem: You have an existing feature X, and you want to implement a similar feature Y.

Solution: For every line of code for X, create a line of code for Y.

The beauty of this technique is:

  1. You don't need to think much - just find each line of code for X and make an equivalent line of code for Y.
  2. You know when you are done: when every line of code for X has an equivalent line of code for Y.

Here is a script that can help you to do 1 and 2 above. Just tweak the variables in the script to match your own code. Suppose you have implemented Google +1 buttons in your codebase, and now you want to add LinkedIn buttons. Run the script, and you will get output like:

lib/XG_ConfigHelper.php::: return B()->getMainConfig('googlePlusOneEnabled') === '1';
lib/components/bundle/controllers/EntryController.php::: if (R('XG_ConfigHelper')->isGooglePlusOneEnabled()) {
lib/components/bundle/lib/EntryListService.php::: if (R('XG_ConfigHelper')->isGooglePlusOneEnabled()) {
lib/components/bundle/lib/MustacheTransformer.php::: ->socialButtons()->googlePlusOneButton($entry);
lib/mustache-templates/v1/bundle/article/list.mustache::: {{{googlePlusOneButtonHtml}}}
. . . . . . . . . .

Note that the script looks for the X string (in my case, googlePlusOne) and checks if a corresponding Y string exists. The script outputs any X lines that do not have corresponding Y lines.

Using this technique, the creation of feature Y becomes mechanical. You keep running the script, and keep implementing the lines that it finds. If there are any lines that are false-positives, you mark those lines in the script. When the script's output is empty, you're done.

Thus you can be quite confident that you have thoroughly implemented the new feature.

Read more…

Wow. Just yesterday I opened one of my favorite books, Revolution in the Valley, containing war stories about the creation of the Macintosh computer. I was reading all of the introductory matter carefully (the scans of an engineer's notebook) when I noticed that one of the pages contained signatures of several original members of the Mac team. Note in the accompanying photo the signatures of Woz, Bill Atkinson, Daniel Kottke, Caroline Rose, Andy Hertzfeld, Bud Tribble, and Susan Kare (with the ⌘ symbol).

This is really cool. It's funny to think that this is inside a book I bought for $12.99 from a used bookstore in Victoria BC several years ago.

Read more…

Literate Programming is a style of programming invented by Donald Knuth in which, instead of the usual style of embedding comments in code, you instead embed code in comments. The result is a long narrative of prose (describing how you are writing the code) with code snippets interspersed throughout.

This never caught on. Perhaps because programmers don't like writing documentation? However, I have noticed something interesting. Sophisticated programmers today write lengthy commit messages. It is an example set by the Linux kernel committers - check out their wonderfully descriptive commit messages. (I first learned about the high quality of Linux kernel commit messages from 5 Useful Tips For A Better Commit Message.)

It strikes me now - I wonder if commit messages are the new Literate Programming. It is said that programmers don't like writing documentation, but they seem to willingly put a lot of effort into writing long, descriptive commit messages. Here's an example of a meaty commit message from the Linux kernel:

commit d940878632e63d7f0c2af5e4ebfcf5135c48dcfb
Author: Jan Kara <jack@suse.cz>
Date:   Wed Jul 11 23:16:25 2012 +0200

    jbd: Fix assertion failure in commit code due to lacking transaction credits

    ext3 users of data=journal mode with blocksize < pagesize were occasionally
    hitting assertion failure in journal_commit_transaction() checking whether the
    transaction has at least as many credits reserved as buffers attached.  The
    core of the problem is that when a file gets truncated, buffers that still need
    checkpointing or that are attached to the committing transaction are left with
    buffer_mapped set. When this happens to buffers beyond i_size attached to a
    page stradding i_size, subsequent write extending the file will see these
    buffers and as they are mapped (but underlying blocks were freed) things go
    awry from here.

    The assertion failure just coincidentally (and in this case luckily as we would
    start corrupting filesystem) triggers due to journal_head not being properly
    cleaned up as well.

    Under some rare circumstances this bug could even hit data=ordered mode users.
    There the assertion won't trigger and we would end up corrupting the
    filesystem.

    We fix the problem by unmapping buffers if possible (in lots of cases we just
    need a buffer attached to a transaction as a place holder but it must not be
    written out anyway). And in one case, we just have to bite the bullet and wait
    for transaction commit to finish.

    Reviewed-by: Josef Bacik <jbacik@fusionio.com>
    Signed-off-by: Jan Kara <jack@suse.cz>
    (cherry picked from commit 09e05d4805e6c524c1af74e524e5d0528bb3fef3)
    Signed-off-by: Willy Tarreau <w@1wt.eu>

Reminiscent of Literate Programming, no?

Read more…

Lisphp at Ning - Sample Code

Here is the Lisp that I have written as an experiment with Lisphp. It is probably not great Lisp code—I don't have prior experience with Lisp—so any suggestions for improvement are welcome. I encourage others to make their Lisphp code publicly available so that we can all learn. Another Lisphp example I have found is balrog.

This code is for rendering an activity feed (like a Facebook Wall). It takes a bunch of activity-item objects, processes them, then feeds them into a Mustache template. The processing step sets up variables for the Mustache template to use.

For new Lisp programmers, I recommend the use of an editor plugin that automatically indents your Lisp code. See my first Lisphp blog post for related recommendations.

render-network-feed.lisp

;;; Top-level module for rendering an activity feed for the network.
;;;
;;; @param string title  the title for the activity section
;;; @param array feed-events  the feed-event objects, parsed from JSON
;;; @param integer excerpt-length  the length at which to excerpt activity items
;;; @param string like-type  the naming scheme for likes: like, promote, or favorite
;;; @param string network-name  the name of the network
;;; @param string network-url  the URL of the network
;;; @param string network-icon-url  48x48 network icon
;;; @return string  HTML for the network feed

(import 'lib/components/activity/lib/process-feed-events.lisp')
(import 'lib/components/activity/lib/add-type-specific-properties.lisp')

;;; Filters, caches, and processes the given network feed events.
;;;
;;; @param array feed-events  the feed-event objects
;;; @param array options  options: excerpt-length
;;; @return array  the feed-event objects after processing
(define (process-feed-events feed-events options)
        ;; Remove rolled-up feed events for now, until we implement handling
        ;; for them.
        (setf! feed-events (remove-rollup-feed-events feed-events))
        (setf! feed-events (add-basic-properties feed-events options))
        (setf! feed-events (add-type-specific-properties feed-events options)))

(setf! processed-feed-events
       (process-feed-events feed-events
                            (hash 'excerpt-length'   excerpt-length
                                  'like-type'        like-type
                                  'network-name'     network-name
                                  'network-url'      network-url
                                  'network-icon-url' network-icon-url)))

;; Send the feed-events into the Mustache template.
((-> B renderMustache)
 'activity/section.mustache'
 (hash
   'title' title
   'feed-events' processed-feed-events))

process-feed-events.lisp

;;; Functions for processing activity events.

(use floor)
(use xg_elapsed_time)

;;; Removes rolled-up feed events. This is a temporary measure until we
;;; implement handling of rollups.
(define (remove-rollup-feed-events feed-events)
        (filter (lambda (feed-event)
                  (not (array-get feed-event 'rollUpType')))
                feed-events))

;;; Adds basic properties, such as humanReadableDate, to each of the feed events
(define (add-basic-properties feed-events options)
        (map (lambda (feed-event)
               (let* ([content-id (array-get feed-event 'event' 'properties' 'contentId')])
                 (arr feed-event
                      (hash (event-type feed-event) true
                            'humanReadableDate'     (human-readable-date feed-event)
                            'content'               (to-content-properties content-id (at options 'excerpt-length'))))))
             feed-events))

;;; Returns the event type suffixed with "Type", e.g., createBlogPostLikeType.
(define (event-type feed-event)
        (. (array-get feed-event 'event' 'eventType') 'Type'))

;;; Returns a friendly date for the event.
(define (human-readable-date feed-event)
        (xg_elapsed_time
          ;; Prefix timestamp with @ so that strtotime will understand it
          (. '@' (floor (/ (array-get feed-event 'event' 'createdDate') 1000)))
          nil nil false))

Read more…

Lisphp at Ning: Custom functions

In my previous post, I mentioned that we are experimenting with Lisphp at Ning, and we have some custom functions that seem to be helpful:

  • import: imports a lisp file: (import 'foo/bar/baz.php')
  • php: runs a PHP function without having to import it: (php :htmlentities 'foo')
  • cons: prepends an item to an array: (cons 'strawberry' flavors)
  • hash: creates an array of key-value pairs: (hash 'key1' 'value1' 'key2' 'value2')
  • array-set: sets an item on a multidimensional array: $flavors['foo']['bar'] = 'baz' is (array-set flavors 'foo' 'bar' 'baz')
  • array-get: gets an item from a multidimensional array: $flavors['foo']['bar'] is (array-get flavors 'foo' 'bar')
  • arr: array_replace_recursive()
  • environment: this is the Lisp environment itself added as a variable, to allow you to check if a function exists: (exists-at? environment 'my-function')

Here is the code that we use to call Lisphp. Note that you have a choice of runFile() or runCode(); also note the custom functions. You may need to make some modifications to get this to run in your own environment:

<?php

/**
 * Parses Lisphp files.
 */
class XG_Lisp {

    /**
     * Executes a Lisp file using Lisphp.
     *
     * @param string $path  the path to the lisp file
     * @param array $env  names and values to add to the environment;
     *                             see Lisphp_Environment
     * @return  the result
     * @see http://jona.ca/blog/lisphp-at-ning-introduction
     */
    public function runFile($path, $env = []) {
        return $this->run(Lisphp_Program::load(NF_APP_BASE . '/' . $path), $env);
    }

    /**
     * Executes Lisp code using Lisphp.
     *
     * @param string $code  the Lisp code
     * @param array $env  names and values to add to the environment;
     *                             see Lisphp_Environment
     * @return  the result
     */
    public function runCode($code, $env = []) {
        return $this->run(new Lisphp_Program($code), $env);
    }

    /**
     * Executes a Lisp program using Lisphp.
     *
     * @param Lisphp_Program $program  a Lisp program object
     * @param array $env  names and values to add to the environment;
     *                             see Lisphp_Environment
     * @return  the result
     */
    protected function run($program, $env = []) {
        $environment = Lisphp_Environment::full();
        foreach ($env as $name => $value) {
            $environment[$name] = $value;
        }
        $environment['B'] = B(); // XG_BaseService
        // Import a PHP file: (import 'lib/components/activity/lib/foo.lisp')
        $environment['import'] = new Lisphp_Runtime_PHPFunction(function ($path) use ($environment) {
            $program = Lisphp_Program::load(NF_APP_BASE . '/' . $path);
            return $program->execute($environment);
        });
        // Prepends an element to a list
        $environment['cons'] = new Lisphp_Runtime_PHPFunction([$this, 'cons']);
        $environment['->$'] = new XG_Lisp_GetField;
        $environment['array-get'] = new Lisphp_Runtime_PHPFunction([$this, 'arrayGet']);
        $environment['array-set'] = new Lisphp_Runtime_PHPFunction([$this, 'arraySet']);
        $environment['hash'] = new Lisphp_Runtime_PHPFunction([$this, 'hash']);
        $environment['var_dump'] = new Lisphp_Runtime_PHPFunction('var_dump');
        $environment['arr'] = new Lisphp_Runtime_PHPFunction('array_replace_recursive');
        $environment['environment'] = $environment;
        return $program->execute($environment);
    }

    /**
     * Prepends an element to a list.
     *
     * @param mixed $item  the item to prepend
     * @param array|Lisphp_List $list  the list to prepend the item to
     * @return array|Lisphp_List  a new list
     */
    public function cons($item, $list) {
        if (is_array($list)) {
            return array_merge([$item], $list);
        }
        return new Lisphp_List(array_merge([$item], $list->getArrayCopy()));
    }

    /**
     * Retrieves an item from a multidimensional array
     *
     * @param array $array  the array to read from
     * @param string $key1  the first key
     * @param string $key2  the second key, etc.
     * @return mixed  the value at the given keys
     */
    public function arrayGet() {
        $args = func_get_args();
        $result = array_shift($args);
        foreach ($args as $key) {
            if (!is_array($result)) {
                return null;
            }
            if (!array_key_exists($key, $result)) {
                return null;
            }
            $result = $result[$key];
        }
        return $result;
    }

    /**
     * Sets an item on a multidimensional array
     *
     * @param array $array  the array to read from
     * @param string $key1  the first key
     * @param string $key2  the second key, etc.
     * @param string $value  the value to set
     * @return mixed  the new multidimensional array
     */
    public function arraySet() {
        $args = func_get_args();
        $array = array_shift($args);
        $subtree = &$array;
        $value = array_pop($args);
        $lastKey = array_pop($args);
        foreach ($args as $key) {
            if (!array_key_exists($key, $subtree)) {
                $subtree[$key] = [];
            }
            $subtree = &$subtree[$key];
        }
        $subtree[$lastKey] = $value;
        return $array;
    }

    /**
     * Converts a list into an array of key-value pairs.
     *
     * @param string $key1  the first key
     * @param string $value1  the first value
     * @param string $key2  the second key
     * @param string $value2  the second value, etc.
     * @return array  the key-value pairs
     */
    public function hash() {
        $args = func_get_args();
        $hash = [];
        for ($i = 0; $i < count($args); $i += 2) {
            $hash[$args[$i]] = $args[$i+1];
        }
        return $hash;
    }

}

Read more…

Lisphp at Ning - Introduction

At Ning, we are experimenting with Lisphp, which allows us to call Lisp from PHP and vice versa. It's like an oasis of functional programming in the midst of PHP.

There isn't much on the web on Lisphp, so I am writing some blog posts (in my Lisphp category) about my experiences with it.

First, some helpful resources:

You'll notice that Lisphp does not come with any documentation other than what is on the Github page. A list of all the functions is in the Environment.php file. Here are brief descriptions of what some of the functions do:

  • define - defines a function or global variable
  • let - sets local variables
  • let* - sets local variables - the definitions can refer to each other
  • setf! - sets a local variable in an "imperative" style: (setf! foo 5). For setting local variables, prefer let first, followed by let*, followed by setf!
  • lambda - creates an anonymous function
  • apply - applies a function to an array of arguments
  • list - creates a Lisphp list: (list) or (list 'a' 'b' 'c')
  • array - creates a PHP array: (array) or (array 'a' 'b' 'c')
  • do - executes code several times in a loop
  • car - returns the first item in a list: (car flavors)
  • cdr - returns the remaining items in the list (i.e., not the first one): (cdr flavors)
  • at - returns the value at the given key: (at flavors 'key')
  • set-at! - sets the value at the given key: (set-at! flavors 'key' 'value')
  • unset-at! - unsets the value at the given key: (unset-at! flavors 'key' 'value')
  • exists-at? - does isset() on the value at the given key: (exists-at? flavors 'key')
  • count - returns the number of items in the list: (count flavors)
  • map - applies a function to every item in a list
  • filter - filters out items from a list
  • fold - (aka "reduce") goes through a list to create a single value
  • if - if statement
  • cond - switch statement
  • = - ==
  • == - ===
  • !=, !==, <, >, <=, >=, +, -, /, *, %, not, and, or, nil, true, false
  • . - concatenates strings
  • isa? - returns whether the object is an instance of the given class: (isa? foo <ArrayObject>)
  • string - strval()
  • substring - substr()
  • string-upcase - strtoupper()
  • string-downcase - strtolower()

I'm not sure about the following - if you know, let me know:

  • eval
  • quote
  • symbol
  • macro
  • dict - I'm not exactly sure how this works. I made a replacement called "hash"—see below.

I also added the following custom functions - I'll give the code in my next blog post:

  • import: imports a lisp file: (import 'foo/bar/baz.php')
  • php: runs a PHP function without having to import it: (php :htmlentities 'foo')
  • cons: prepends an item to an array: (cons 'strawberry' flavors)
  • hash: creates an array of key-value pairs: (hash 'key1' 'value1' 'key2' 'value2')
  • array-set: sets an item on a multidimensional array: $flavors['foo']['bar'] = 'baz' is (array-set flavors 'foo' 'bar' 'baz')
  • array-get: gets an item from a multidimensional array: $flavors['foo']['bar'] is (array-get flavors 'foo' 'bar')
  • arr: array_replace_recursive()
  • environment: this is the Lisp environment itself added as a variable, to allow you to check if a function exists: (exists-at? environment 'my-function')

Finally, some tips:

  • See if your editor has a plugin that will automatically indent your Lisp code. For example, Sublime Text has a lispindent plugin that will indent your code whenever you press Enter; you can also press Command+I to re-indent the selected code.
  • Sometimes you may need to dive into the Lisphp code to fix things. This is a good opportunity to learn how Lisphp works, and to contribute back by submitting a pull requests. I submitted two pull requests and they were accepted immediately.
  • To import a constant: (use +XG_Model::PLAINTEXT+). Now you can reference +XG_Model::PLAINTEXT+.
Read more…

Sublime Text Window-Splitting Bliss

I'm trying out Sublime Text again, and I'm really missing how easy it is to split windows in jEdit. In jEdit, you just do:

  • Command+2 to split the window horizontally.
  • Command+3 to split the window vertically.
  • Command+0 to unsplit the window.

Well, it turns out you can do this in Sublime Text! First, install the Origami plugin. Then add the following keybindings:

    { "keys": ["super+0"], "command": "destroy_pane", "args": {"direction": "self"} },
    { "keys": ["super+2"], "command": "create_pane_with_cloned_file", "args": {"direction": "right"} },
    { "keys": ["super+3"], "command": "create_pane_with_cloned_file", "args": {"direction": "down"} }

Now you can split your windows super-easily. Origami's default keybindings are pretty hard to remember and use, but the above is all you need.

Read more…

How I am writing commit messages now

I am trying a new way of writing commit messages based on Thoughtbot's 5 Useful Tips For A Better Commit Message. Basically my commit messages now look like this:

[Ticket number]: Summary: 1-line description: what and why

Problem: ...

Solution: ...

Side-effects: ...

Example:

[BDZL-4222] Summary: Digest's link "To control which emails you receive" is incorrect

Problem: The digest core has a link to /profiles/profile/emailSettings. However, this link is the old Bazel URL; in Bedazzle, the URL has changed to /main/profilesettings/email.

Solution: Make /profiles/profile/emailSettings redirect to /main/profilesettings/email.

Side effects: /profiles/profile/emailSettings responds with a 302 to /main/profilesettings/email, instead of a 404.

We'll see how long I can keep this up. The quality of these commit messages is great though - they give the why and the how of your commit.

Read more…

JWZ on commenting code

I love this exchange between Peter Seibel and Jamie Zawinski from the book Coders at Work. It captures what I like about well-documented code.

Seibel: Earlier you said something about writing code in order to make it easier to read, which ties into maintenance. What are the characteristics that make code easier to read?

Zawinski: Well, comments obviously. Writing down what the assumptions are and what this does. If it's building up a data structure, describing the layout of it. A lot of times I find that pretty helpful. Especially in writing Perl code when it's like, uh, well, it's a hash table and values are bunch of references to lists, because the data structures in Perl are just nuts. Do I need a right arrow here to get to this? I find examples like that to be helpful.

I always wish people would comment more, though the thing that makes me cringe is when the comment is the name of the function rephrased. Function's called push_stack and the comment says, This pushes to the stack. Thank you.

You've got to say in the comment something that's not there already. What's it for? Either a higher-level or a lower-level description, depending on what's most important. Sometimes the most important thing is, what is this for? Why would I use it? And sometimes the most important thing is, what's the range of inputs that this expects?

Long variable names. I m not a fan of Hungarian notation, but I think using actual English words to describe things, except for loop iterators, where it's obvious. Just as much verbosity as possible, I guess.

Read more…

Edward Feser's road from atheism to theism

I was reading a blog post by philosophy professor Edward Feser describing his conversion from atheism to theism. It is interesting to note the reasons that led to his change of mind:

  • "The first of them had to do instead with the philosophy of language and logic. . . As the arguments sank in over the course of months and years, I came to see that existing naturalistic accounts of language and meaning were no good."
  • "At first, and like so many undergraduate philosophy majors, I took the materialist line for granted.  Mental activity was just brain activity.  What could be more obvious?  But reading John Searle’s The Rediscovery of the Mind destroyed this illusion, and convinced me that the standard materialist theories were all hopeless."
  • "Most importantly, though, Lockwood’s book introduced me to Bertrand Russell’s later views on these issues, which would have a major influence on my thinking ever afterward.  Russell emphasized that physics really gives us very little knowledge of the material world.  In particular, it gives us knowledge of its abstract structure, of what can be captured in equations and the like.  But it gives us no knowledge of the intrinsic nature of matter, of the concrete reality that fleshes out the abstract structure.  Introspection, by contrast, gives us direct knowledge of our thoughts and experiences.  The upshot is that it is matter, and not mind, that is the really problematic side of the mind-body problem."
  • "Second, a complete naturalistic explanation of intentionality is impossible."
  • "It was also while still a naturalist that I first started to take a serious interest in Aristotelianism, though at the time that interest had to do with ethics rather than metaphysics. . . One consequence of this was that I always took teleology seriously, because it was so clearly evident a feature of ordinary practical reasoning."
  • "As I argue in The Last Superstition, many of the so-called “traditional” problems of philosophy are really just artifacts of the anti-Scholastic revolution of the moderns."
  • "Fregean and related arguments had gotten me to take very seriously the idea that something like Platonic realism might be true.  (I would later see that Aristotelian realism was in fact the right way to go, but the basic anti-naturalistic move had been made.)  The arguments of Searle and others had shown that existing versions of materialism were no good.  Russellian arguments had shown that modern science and philosophy had no clear idea of what matter was in the first place.  Whatever it was supposed to be, though, it seemed it was not something to which one could assimilate mind, at least not if one wanted to avoid panpsychism.  Naturalism came to seem mysterious at best.  Meanwhile, Aristotelian ideas had a certain plausibility.  All that was needed was some systematic alternative to naturalism."
  • "Then there was Aquinas. . . It was all very strange.  Aquinas’s arguments had a certain power when all of this metaphysical background was taken account of.  And there was a certain plausibility to the metaphysics."
  • "The only reason for not taking Aquinas and similar thinkers seriously seemed to be that most other academic philosophers weren’t taking them seriously.  And yet as I had come to learn, many of them didn’t even understand Aquinas and Co. in the first place, and their own naturalism was riddled with problems."
  • "As I taught and thought about the arguments for God’s existence, and in particular the cosmological argument, I went from thinking “These arguments are no good” to thinking “These arguments are a little better than they are given credit for” and then to “These arguments are actually kind of interesting.”  Eventually it hit me: “Oh my goodness, these arguments are right after all!”"
Read more…

Favorite things to do

If you ever wonder what your favorite things to do are, or what's the best way to spend your leisure time, may I offer these wonderful suggestions taken from an unlikely source—the section on the Sunday rest from the Catechism of the Catholic Church:

  • Family. “Christians will also sanctify Sunday by devoting time and care to their families and relatives, often difficult to do on other days of the week.”
  • Good works. “Sunday is traditionally consecrated by Christian piety to good works and humble service of the sick, the infirm, and the elderly.”
  • Prayer. “On Sundays and other holy days of obligation, the faithful are to refrain from engaging in work or activities that hinder the worship owed to God, the joy proper to the Lord's Day, the performance of the works of mercy, and the appropriate relaxation of mind and body.”
  • Study. “Sunday is a time for reflection, silence, cultivation of the mind, and meditation which furthers the growth of the Christian interior life.

My personal favorite is Study. Here are some of the resources that I like to immerse myself in on those rare occasions that I have blocks of free time:

  • Music: Listen. Music appreciation course CDs and textbook.
  • Philosophy: Aristotle: the desire to understand.
  • Poetry: The New Penguin Book of English Verse. Unlike most poetry anthologies which are organized by author, this organizes the poems by date of composition. So it feels like a kind of time machine.
  • Religion: Introduction to the Devout Life. St. Francis de Sales.
  • Art: The Story of Art. Generously illustrated story of the artistic masterpieces of the Western world.
Read more…

Beautiful correspondences in the Christian religion

In Christianity, the following things are analogous. The correspondence is rather beautiful - so much so that it is one of the things that convinces me of the truth of Christianity:

  • A wedding feast; the banquet table.
  • The crucifixion of Christ; the cross.
  • Jewish sacrifice; the altar.
  • The Eucharist; the altar.
  • The marital act; the marital bed.

I'm sure someone could easily write a book on the beauty of the relationships between these things.

Read more…

Classic books on programming

Yesterday I spent too much time checking out the books in 10 Books That Will Substitute a Computer Science Degree. It was discussed on Hacker News. Many of the books come from the ACM's list of classic books. To see a description of the book, click the book's name on the ACM site.

Several of the books are out of print, but some are available freely online. The ACM also offers PDFs of several of the books for purchase, for about $20.

Read more…