Showing posts with label Elisp. Show all posts
Showing posts with label Elisp. Show all posts

Saturday, January 4, 2014

Debian Testing, Pi and Git

That was a vacation, I guess.

It was suspiciously taxing, all in all. Time off from work hasn't been nearly as relaxing since we had a kid, but that's a digression. Over the past little while, I've managed to finally make use the 120G solid state drive I picked up half a year ago, install various distros, and put together about one third of a utility to ease a project or two I'm working on in my spare time.

New Drive

It's at once larger and smaller than the last one. On the one hand, thanks to its smaller physical profile, I can fit it into my laptop with no mods. On the other hand, df -h says 106G[1] instead of ~28G.

That's it, nothing else to see here.

Fresh Install

Since I was between drives anyhow, I took the opportunity to get the fresh version of Debian up and running. That was worth it, by the by, if for no other reason than they've apparently poured enough bucketfulls of time into the networking code that I can now reliably connect to my wifi access point even if I'm not within two meters of it. They also seemed to lick a problem I kept running into wherein the shutdown process would hang the machine[2].

There were a few changes in my install routine, which is still vaguely based on

## temporarily add
## deb http://packages.linuxmint.com debian import
## and `contrib non-free` to /apt/sources.lisp

apt-get install firmware-ralink firmware-realtek
apt-get install screen make emacs24 git gitk wicd-curses pmount htop gnupg unetbootin
apt-get install mplayer feh pacpl imagemagick x-window-system dmenu xmonad gimp inkscape firefox
apt-get install python-pip sbcl vrms

## remove the temporary repos

There are a couple of changes there from my usual. Firstly, Firefox has become my go-to browser. Its absorbed most of the goodness from Chromium, including the reduced toolbar footprint and fantastic JS console. It also has support for adblock, and a fairly good RSS feed reader, and it's no longer slow as molasses[3]. This raises the problem of the Debian packaging though; in the official repos, apt-get install firefox gets you a pretty ham-fisted re-brand with no plugin support called "Iceweasel". What I ended up doing, as you can see above, is temporarily adding the Linux Mint repo to install that[4]. Secondly, I'm installing emacs24 rather than just plain emacs. This is because the current default for emacs in Jessie is Emacs 23.something, and that doesn't have one of the main features I'm looking to finally adopt.

Emacs 24 supports package out of the box. In practice, this means adding

(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/") t)

to my .emacs instead of toting my old .emacs.d/ around. I can't actually remember every library I used to have around, so the list I settled on this time out ended up being

aes auto-complete autopair highlight-parentheses htmlize skewer-mode magit markdown-mode paredit redo+ smart-tab yasnippet

Which covers pretty much everything. Oh, one thing. I spent about half an hour figuring out what was going wrong with my .emacs config; libraries I was certain had been installed were coming back with not found errors when I tried to require them. It turns out that when you add a directory to the load path, you don't automatically add all its subdirectories. As you can see by the above list of packages, I use quite a few, each of which gets its own sub-directory in .emacs.d/elpa/, and wildcards don't work here either. So I was forced to add the following to convenience.el, just to save myself the tedium

(defun starts-with-dot-p (path)
  (= (aref path 0) ?.))

(defun list-subdirectories (path)
  (let ((all (mapcar 
              (lambda (name) (concat (file-name-as-directory path) name))
              (remove-if #'starts-with-dot-p (directory-files path)))))
    (remove-if-not #'file-directory-p all)))

(defun add-to-load-path (dirs)
  (mapc (lambda (p) (add-to-list 'load-path p)) dirs))

then called this near the top of that .emacs file:

(add-to-load-path (list-subdirectories "~/.emacs.d/elpa"))

This let me continue as normal. The only omission from that emacs package list is slime, which I've lately been installing from sbcl or what-have-you with (ql:quickload :quicklisp-slime-helper) rather than through Emacs itself. It works exactly as well as you'd expect, which is to say flawlessly.

Dicking Around With Pis

Doing that got me into an installing mood, so I also formatted a fresh couple of SD cards with the latest versions of ARM Arch and Raspbian respectively. I did this with the vague intention of getting deal to work with one or both, and it looks like that'll take a bit more work than just a straight-up ql:quickload. Differences before I get to that though.

The RPi arch is much closer to what I'm used to on my laptop. A brutally minimal installation of the few core utilities you need to get basic shit done, and nothing else. Specifically, it gives you pacman, perl, a working ssh server and a minimally intrusive wireless connection mechanism that could replace wicd-curses for me. Raspbian, by contrast, bundles a mandatory window environment along with a bunch of crap that's probably nice for most humans looking to use it as a desktop replacement, but that I'll never end up touching. Also, they bundle Scratch as well as Python 2 and 3. Finally, while they do provide an ssh server, it's off by default, and the first time you boot a Raspbian image, it forces raspi-config, which means that you must connect a Raspbian Pi to a monitor and keyboard at least once.

That minimalism ends up biting Arch a bit though; it doesn't come with the standard raspi-config utility, which lets you mess around with the hardware to some small extent, and easily resize the installation partition to fill the SD card[5]. The other thing that bites ARM Arch in the ass, as far as I'm concerned, is the fact that its package manager has very few of the things I want to install. Out of my usual menagerie, I found screen, make, clisp, python, emacs and nothing else. By contrast, I had to apt-get --purge a bunch of things over on Raspbian, but I was eventually able to get it working with an almost copy of my laptop environment.

Almost, because Lisp still has some problems.

Specifically, clisp segfaults on both ARM Arch and Raspbian when you try to load anything with quicklisp, while the ARM ccl failed to run at all on Arch[6]. Raspbian did run ccl appropriately, but errored out on me for two reasons. Firstly, there's something unsupported about the :ironclad MD5 digest, and secondly, the ARM architecture seems to treat bivalent streams differently than x86. Which means that even running its custom house server, :deal errored out.

I'll be trying to fix that over the next little while.

cl-git-fs

Finally, on a merely semi-related note, I'm working on a couple of projects on my own time that are eventually going to want to do some sort of file management. And I figured it would be nice not to have to bring git into it manually after the fact. To that end, I took a look at how gitit manages the trick of using git as a faux-database for its wiki pages. It's not that complicated, as it turns out. And here's the result of spending an hour or two porting that piece of functionality to Common Lisp.

The biggest problem I'm running into is that there isn't a standard shell-command or run-program defined in the various Lisps I want to support.

I'm tossing it up to my github, but calling it 0.01 because there's a fuckton of functionality and documentation missing. In particular, it currently only supports SBCL on Linux, and a few of the external API functions still return raw string results, rather than properly parsed CLOS instances. The documentation and parsing will be a priority no matter what, but I'll only see how it runs on other platforms and implementations as I need to deploy to them.


Footnotes

1 - [back] - Of course, the drive box says 128G, so Samsung and all drive manufacturers are lying shitbags, but I'm digressing again.

2 - [back] - And therefore keep drawing power until a forced shutdown.

3 - [back] - which it was last time I played around with it.

4 - [back] - No, since you ask, I've never just straight up tried Mint. It has something in common with most of the distros I get recommended, which is that it cribs heavily from Debian on everything that matters, and then tries to differentiate on the desktop environment almost entirely. Not that this is bad for end users I guess, but as you can see from the x-window-system and xmonad items in that installation list above, I do not use what you would think of as "a desktop environment". Don't let that stop you from trying it, of course, but I'm not going to.

5 - [back] - You can still do this externally via gparted when you image your SD card.

6 - [back] - running the included binary gave me a "wrong architecture" error, even though there's no way that's accurate.

Thursday, August 1, 2013

REBOL Mode

I need a short break here. It's nowhere near done yet, not even the pieces I wanted to put together. But, at the risk of ending up with another piece of detritus littering my blog, I need to let off some steam and talk about where I'm going with this.

For the past couple of days, I've been busy working on writing a replacement for rebol.el and running into some not altogether unexpected headaches. The file I just linked you hasn't been updated since about 2001, and doesn't include a license block. After attempting to contact its current host and getting no response, I just went ahead and started from scratch. I had a few goals from the outset:

Proper Highlighting

The current rebol.el was put together for REBOL2, and thus lacks highlighting for certain symbols that have been introduced since. The one that sticks out most in my mind is the symbol funct, which you saw semi-humorously higlighted as funct in the third section last time.

Jump-To-Definition

Some kind of binding in a mode that lets you jump to the file and line of the definition of a given symbol. Not entirely sure what the interaction there is going to be, probably just a key-binding that jumps for the thing-at-point. This one looks like it would be pretty simple to pull off actually; when a file is loaded, record the position of any assignments in it. Assignments are simple to find, since there's exactly one way to do it, and it involves adding a single-character suffix to whatever word you're assigning.

REPL

There isn't a run-rebol, in the style of run-python or run-lisp, and I'd like one.

Send Region

Fairly self-explanatory. Or maybe not? In Lisp modes, there's typically a binding to evaluate the current s-expression, either C-c C-c or C-M-x. When you hit it, the effect is to evaluate the surrounding block into the REPL. There isn't a send-region command in the current rebol-mode, probably because they don't directly implement an in-Emacs-REPL, but since I want the second, I'd also like the first.

Documentation Display

Just a simple, straight-forward way to display help about a particular symbol in a separate buffer. Nothing fancy, move along. Ok, in a future edition, it would be nice if the return text was all linkified and highlighted so that you could click/<Ret> through docs, but that can wait.

Source Display

This one might be a bit more functional. You see, you can use the source function to get a source dump of almost[1] any REBOL3 word. It may or may not be useful at all, but it would be pretty cool to build a buffer that would let you load such source output, change it, then send it back to the REPL when you save.

Argument Hints

If you've used things like SLIME, you'll appreciate this one. As you're typing, a summary of the arguments to the thing you're typing shows up in the minibuffer. This is trivial in Lisp, because of the way everything is parenthesized pretty consistently. It turns out to be quite a headache in REBOL3, and basically necessitates interacting with some sort of running runtime system. Here's an example, pretend these are all actual REBOL words:

foo bar baz mumble |

Assuming that pipe represents my cursor, what should the mode display in your minibuffer, and how would you find out? Near as I can tell, you'd need to ask a running r3 interpreter with the words foo, bar, baz and mumble defined. What it would do is:

  • check if mumble is a function, and if so, print the arg-hint for mumble, otherwise
  • check if baz is a function of more than one argument, and if so print the arg-hint for baz, otherwise
  • check if bar is a function of more than two arguments, and if so print the blah blah blah

until you run out of words to check. Another open question is: how far back should the mode look for relevant symbols? For the moment, I've settled on "To the beginning of the previous block, or the first assignment, whichever comes first", but that's probably not the best approach to take.

Where I've Got So Far

At the moment, I'm about a quarter of the way there by my reckoning. And I've run into some issues, both expected and unexpected. The mode as currently posted here, implements proper highlighting, a basic REPL, a basic documentation display, a basic source display, and a hacked-together send-region. I've already gone through the problems with argument hinting in an almost-purely whitespace-delimited language; there was only one completely unexpected problem and two little gotchas I ran into. Lets start small:

Gotcha the first is that REBOL path strings aren't fully cross-platform. They do the standard Unix thing, and auto-convert for Windows, but if you include a drive letter, which the Windows version of Emacs does by default, your string won't be recognized as a file. As a result, I need that extra snippet to sanitize Windows paths.

Gotcha the second is another cross-platform, or possibly cross-version, issue. For some reason the Linux edition of the r3 comint buffer prints its input before its return value. That is

(defun proc-filter (process msg)
  (message "%s" msg))

(set-process-filter proc #'proc-filter)

;; on Linux

(process-send-string proc "source source")

;; Output to *Message* is
;;
;;    source source
;;    source: make function ! [[ 
;;    ..
;;    >>

;; on Windows

(process-send-string proc "source source")

;; Output to *Message* is
;;
;;    source: make function ! [[ 
;;    ..
;;    >>

Not a huge deal, except that I have to deal with it if I want to succeed in my master plan of running an r3 interpreter behind the scenes. Which brings me to the big, unexpected piece of code I had to write. This actually took a few tries, as I came to grips with the situation.

(defun r3-process-filter (proc msg)
  "Receives messages from the r3 background process.
Processes might send responses in 'bunches', rather than one complete response,
which is why we need to collect them, then split on an ending flag of some sort.
Currently, that's the REPL prompt '^>> '"
  (let ((buf ""))
    (setf buf (concat buf msg))
    (when (string-match ">> $" msg)
      (mapc #'r3-ide-directive 
            (split-string buf "^>> "))
      (setf buf ""))))

(defun r3-send! (string)
  "Shortcut function to send a message to the background r3 interpreter process"
  (process-send-string r3-rebol-process (concat string "\n")))

(set-process-filter r3-rebol-process #'r3-process-filter)

(defun r3-ide-directive (msg)
  (let* ((raw-lines (butlast (split-string msg "\r?\n")))
         ;; the linux edition seems to return the function call before its output. Might also be an Emacs version issue.
         (lines (if (eq system-type 'gnu/linux) (rest raw-lines) raw-lines)))
    (when lines
      (cond ((string-match "NEW-KEYWORDS: \\(.*\\)" (first lines))
             (let ((type (intern (match-string 1 (first lines)))))
               (setf (gethash type r3-highlight-symbols) (rest lines))
               (r3-set-fonts)))
            ((string-match "HELP: \\(.*\\)" (first lines))
             (get-buffer-create "*r3-help*")
             (with-current-buffer "*r3-help*"
               (kill-region (point-min) (point-max))
               (insert ";;; " (match-string 1 (first lines)) " ;;;\n\n")
               (mapc (lambda (l) (insert l) (insert "\n")) (rest lines)))
             (pop-to-buffer "*r3-help*"))
            ((string-match "SOURCE" (first lines))
             (ignore-errors (kill-buffer "*r3-source*"))
             (get-buffer-create "*r3-source*")
             (with-current-buffer "*r3-source*"
               (mapc (lambda (l) (insert l) (insert "\n")) (rest lines))
               (r3-mode))
             (pop-to-buffer "*r3-source*"))))))

The third piece there isn't terribly important[2]. The gist of it is that I want to run a separate r3 process, and communicate it for certain things. In order to do that, I have to attach a listener to the process. Then, whenever I send a string to the process, it will respond with a process id and message to that listener.

The catch I wasn't counting on was that output arrives in "bunches". Which is to say, if you send three or four commands, you're going to get back strings of ~400 characters, each containing either a partial response, a full response or multiple full/partial responses. Because I'm not expecting responses much larger than a couple thousand characters, I can get away with just buffering until output lets up, but that might not be the best general strategy.

I'll talk about this a bit more after I've had some more time to work on it. Right now?

I. Need. Sleep.

Cheers.


Footnotes

1 - [back] - Before you ask, yes, it's entirely possible to get the source of source. The only words you can't introspect on in this way are native!s, which are implemented in C rather than REBOL.

2 - [back] - As a note to self, I'm going to have to re-write pieces of it. Both for speed, and because I'm repeating blocks for each condition. That needs to be a mini-dsl instead of manual code.

Monday, June 17, 2013

Elm First Impressions

For the past little while, I've been poking around a new language named Elm. A Haskell-like web front-end language with a heavy focus on FRP. Actually, no, it's not like Haskell, its syntax is Haskell except for a few omissions[1], a couple justifiable small changes, and a couple pointlessly gratuitous differences[2]. To the point that the actual, official recommendation is to just use Haskell mode to edit Elm files.

This works pretty well, except for one thing: Elm has a built-in reader macro for Markdown input. Using this feature in Haskell mode plays all kinds of hell with your indentation and highlighting. Enough that I thought it worth-it to hack a workaround in using two-mode-mode. This is far from ideal, but bear with me. You need to get two-mode-mode from that previous link, do a search/replace for mode-name into major-mode, and delete the line that reads (make-local-hook 'post-command-hook). Then, you have to add the following to your .emacs somewhere:

(require 'two-mode-mode)
(setq default-mode (list "Haskell" 'haskell-mode)
      second-modes (list (list "Markdown" "\[markdown|" "|\]" 'markdown-mode)))

and then run two-mode-mode whenever you're editing .elm files. The end result is that, whenever you enter a markdown block with your cursor, your major mode will automatically change to markdown-mode, and change back to haskell-mode when you leave. There has to be a better solution than this, probably involving one of the other Multiple Modes modules, and I'll put some thought into it when I get a bit of time.

Installation/Basics

Installing is ridiculously easy. If you've ever installed a module for Haskell, you won't have trouble. It's just cabal update; cabal install elm elm-server. Do the update first, like it says there; the language hasn't reached 1.0 status as of this writing, which means that it's quite likely there will be significant changes by the time you get around to following these instructions.

You write code into .elm files, which you can either preview dynamically or compile. You do the dynamic preview thing by running elm-server in your working directory. That starts up a server listening on http://localhost:8000 that automatically compiles or re-compiles any .elm file you request. That server runs on Happstack, and does a good enough job that the official elm-lang site seems to serve directly from it.

If you're like me though, you prefer to use static files for your actual front-end. You can use elm --make --minify [filename] to generate a working .html file[3] that you can serve up along with the elm-runtime from whatever application server you want to use.

Enough with the minutia though. Really, I'm here to give you a paragraph or two on what I think about the language.

What I think about the Language

The usual disclaimers apply.

  • you'll easily find more people who are familiar with JS/HTML than those who are familiar with Elm
  • if you use it, there's an extra[4] abstraction layer between you and the final front-end
  • using it forces your users to enable JavaScript. Ostensibly, you can use the compiler to generate noscript tags, but all these seem to do is statically document what the page would do if JS was on.

That second one in particular means that once again, you really should learn JavaScript before trying to use Elm to save yourself from it.

Once you get past that, it's quite beautiful and elegant. Much better than plain JS for some front-end work. Not that that's a very high bar.

There's some stuff conspicuously missing, like my beloved SSEs, and some basic DOM interactions including draggable and an arbitrary, element-triggered click event. The approaches available out-of-the-box are respectively, Drag Only One Element That You Can't Drop and Detect Mouse Location On A Click, Then Dispatch Based On It. Neither of those seem very satisfying. In fact, the proposed workarounds look strictly worse to me than the "callback hell" this language is trying to save me from.

Those shortcomings are just getting me more interested, to be honest. The reason being that it looks like it's possible to implement additional native functionality fairly easily, so all it'll do is cause me to spend some time writing up the appropriate, signal-based libraries to do these things.

Overall first impressions so far are good, though I'm seriously questioning how useful this language is going to be for more complicated interfaces. In the short term, I'll test out its shallow limits by writing a new WebMote front-end.

I'll let you know how it goes.


Footnotes

1 - [back] - Which I'm pretty sure will eventually be addressed. I particularly miss full sections and where, though you'd think the multi-line function declarations would be the biggest gap.

2 - [back] - For no reason I could see, : is Elm's type annotation operator, while :: is Elm's cons. It's precisely the opposite in Haskell, and buys little enough that I hereby formally question the decision. Similar reasoning seems to apply to the operator <|, which seems to do exactly the same thing as Haskells' $, except that it's twice as long.

3 - [back] - Or separate .html and .js files, if you also passed the -s flag.

4 - [back] - Not particularly stable, yet.

Sunday, January 6, 2013

Autopair, Paredit and Bitching

First, lets get the useful information out of the way. I've been using the fantastic paredit to edit Lisp code for a while now, and the slightly-less-fantastic-but-generally-useful autopairs to help with parentheses/curlies/quotes/what-have-you in other languages[1]. There's a little blurb on the Emacs wiki page about using the two of them together, which implies that (autopair-global-mode) should automatically respect paredits primacy in Lisp modes, but that doesn't seem to happen. When I tried editing Elisp, or Common Lisp or Clojure with that fix in place, I got some odd edge-case behavior.

Specifically, highlighting a region to parenthesize it produced an extraneous close-paren and backspace suddenly didn't balance deletions. I'm not sure this is the most elegant way forward, but the way I wound up fixing it is

(defvar *paredit-modes*
  '(common-lisp-mode lisp-mode emacs-lisp-mode clojure-mode lisp-interaction-mode)
  "A list of modes wherein I use paredit.")

(require 'autopair)
(autopair-global-mode)

(autoload 'paredit-mode "paredit"
  "Minor mode for pseudo-structurally editing Lisp code." t)

(defun custom-paredit-mode ()
  (progn (paredit-mode +1) 
         (define-key paredit-mode-map (kbd "<C-left>") 'backward-sexp)
         (define-key paredit-mode-map (kbd "<C-right>") 'forward-sexp)))

(dolist (mode *paredit-modes*)
  ;; Activate paredit and deactivate autopair in lisp modes
  (add-hook (intern (concat (symbol-name mode) "-hook"))
            (lambda ()
              (custom-paredit-mode)
              (setq autopair-dont-activate t)
              (autopair-mode -1))))

*paredit-modes* is just a list of language modes where I want paredit, and therefore not autopair. I want autopair globalized because I use it in every mode where I'm not using paredit, so it's much easier to set exceptions than to exhaustively list ever non-lisp language mode. After both modes are included, I iterate over *paredit-modes* with a dolist and set the appropriate hooks.

Right, that's the useful part out of the way. I'm going to bitch now, and I'd appreciate some privacy.

Bitching

I'm starting to feel bored with what I'm doing. The last time I felt this way was back in college, where I suddenly went from producing two finished illustrations and about ~30 sketches per week to just under .01 and 2 respectively. I doubt the same thing is going to happen with programming because of what I think programming is, but it's still kind of disconcerting that I'm emotionally beyond my own control. Over the past couple of months, I've been working pretty hard on three or four personal projects and one giant project at work, most of it in Python.

That's kind of depressing. Not that Python is a horrible language, mind you, it's ok and it's terse enough once you get to grip with its particular way of doing things, and most of its libraries aren't as over-engineered as the stuff I've found myself needing to include over in Clojure-land, but it lacks a feature or three that I've gotten very used to. You'll note that those links don't all point to things from the same language[2].

I remember reading about something called The Curse of the Traveller a little while ago. It seemed like an interesting concept, but not one I'd be able to relate to since I don't actually like traveling[3]. It feels painfully relevant here though. Re-phrasing it for the language enthusiast

The more languages you learn, the more things you see that appeal to you, but no one language has them all. In fact, each language has a smaller and smaller percentage of the things you love, the more languages you learn. It drives you, even subconsciously, to keep looking, for a language not that's perfect (we all know there's no Shangri-La), but just for a language that's "just right for you." But the curse is that the odds of finding "just right" get smaller, not larger, the more you experience. So you keep looking even more, but it always gets worse the more you see.

In theory, language users have an out because we can technically build a "just-right" language. In practice, it turns out that implementing your own garbage collection, optimizing compiler, streams, i/o, package management, web-server, asynchronous web-server etc. is a lot harder than just living with a reasonably popular language you already know[4]. Also, there are very few things lonelier than being the only one who knows your language of choice. The end result is that, while I've probably been getting more stuff done in Python/JavaScript than I'd ever gotten done in a comparable time-period, I've been getting skull-fuckingly bored doing it. And I'm really not sure what the solution is.

Realistically

This is the effect of having a single quarter wherein I

  • had a child. I mean, my wife had him, but I still lost plenty of sleep as a result.
  • got sick enough that I couldn't exercise for a good week and a half.
  • went through the holiday garbage typical of the season.

Any one of those could probably put me in a funk on its own. The combination, as well as the evil fucking grey winter that's just descending on Toronto streets, is enough to explain the mood and then some. So I mean, that's that. I was planning on starting another project or two in the next little while, but all things considered, maybe I owe myself a bit of relaxation for a change. Metaphorically spin down the drives and kick my feet up, at least 'till I get the chance to get a good sprint or two and a few winks in.


Footnotes

1 - [back] - I still don't use auto-pair+, but you should feel free to.

2 - [back] - Although, as usual, it wouldn't be very difficult to put together 90-95% adequate versions of the rest if you have access to defmacro, which is why Common Lisp tends to be my language of choice if I have any say in the matter at all. As proof, I submit the article count by language over there in the sidebar.

3 - [back] - I got that shit out of my system before I turned 12, and I have no more desire for it, thank you very much.

4 - [back] - Or picking up a Lisp that hits enough of the bases then defmacroing the additional facilities you need.

Friday, December 14, 2012

Not Optimizing Haskell

The flu can go fuck itself in its nonexistent, viral ass. This shit will not beat me. While I run down the clock, I'm profiling more things to make me feel a bit better.

First off, neither GHCi nor Haskell mode comes with an interactive profiler. Or, as far as I can tell, any utilities to make batch profiling any easier. The way you profile Haskell programs is by installing the profiling extensions

apt-get install libghc-mtl-dev libghc-mtl-prof

compiling your program with the profiling flags on

ghc -prof -auto-all -o outFile yourFile.hs

and then running the result with some different profiling flags.

./outfile +RTS -p

That should create a file called outFile.prof in the directory you just ran it from, and that file will contain a well formatted couple of tables that will tell you where your space and time cost-centers are.

So... lets automate this.

(defun ha-custom-profile-buffer ()
  (interactive)
  (find-file-other-window 
   (ha-custom-profile-haskell-file (buffer-file-name))))

(defun ha-custom-profile-haskell-file (abs-filename)
  "Compiles the given file with profiling, 
runs it with the +RTS -p flags and returns
the filename of the profiling output."
  (assert (string= "hs" (file-name-extension abs-filename)))
  (let* ((f-name (file-name-sans-extension abs-filename))
         (tmp (make-temp-file f-name))
         (tmp-name (file-name-nondirectory tmp))
         (tmp-dir (file-name-directory tmp)))
    (message "Compiling...")
    (shell-command (format "ghc -prof -auto-all -o %s '%s'" tmp abs-filename))
    (message "Profiling...")
    (shell-command (format "%s./%s +RTS -p" tmp-dir tmp-name))
    (concat tmp-name ".prof")))

Those functions are both now part of my ha-custom mode. The big one takes a Haskell file, compiles it to a tempfile with the appropriate flags, runs the result with the other appropriate flags, and returns the name of the profiling output file. The little function takes the current buffer and runs it through the big one, then opens the result in a new window. That should make it a bit easier to actually do the profiling.

Actually Profiling Haskell

We started with pretty much the same thing as the Lisp code. And, I'll strip the printing elements again for the purposes of this exercise; we're not interested in how inefficient it is to actually produce a grid based on our model of the world.

module Main where
import Data.List (group, sort, concatMap)
import Data.Set

lifeStep :: Set (Int, Int) -> Set (Int, Int)
lifeStep cells = fromList [head g | g <- grouped cells, viable g]
  where grouped = group . sort . concatMap neighbors . toList
        neighbors (x, y) = [(x+dx, y+dy) | dx <- [-1..1], dy <- [-1..1], (dx,dy) /= (0,0)]
        viable [_,_,_] = True
        viable [c,_] = c `member` cells
        viable _ = False

runLife :: Int -> Set (Int, Int) -> Set (Int, Int)
runLife steps cells = rec (steps - 1) cells
  where rec 0 cells = cells
        rec s cells = rec (s - 1) $! lifeStep cells

glider = fromList [(1, 0), (2, 1), (0, 2), (1, 2), (2, 2)]
blinker = fromList [(1, 0), (1, 1), (1, 2)]
gosperGliderGun = fromList [(24, 0), (22, 1), (24, 1), (12, 2), (13, 2), (20, 2), (21, 2), (34, 2), (35, 2), (11, 3), (15, 3), (20, 3), (21, 3), (34, 3), (35, 3), (0, 4), (1, 4), (10, 4), (16, 4), (20, 4), (21, 4), (0, 5), (1, 5), (10, 5), (14, 5), (16, 5), (17, 5), (22, 5), (24, 5), (10, 6), (16, 6), (24, 6), (11, 7), (15, 7), (12, 8), (13, 8)]

main :: IO ()
main = putStrLn . show $ runLife 5000 gosperGliderGun

It's almost the same, actually, because we determine frequencies differently. Instead of doing a single traversal of the corpus, we do what looks like a much more expensive operation composing group onto sort onto concatMap neighbors. In a book, that would be called "foreshadowing".

A first run-through of M-x ha-custom-profile-buffer gives us

        Fri Dec 14 21:48 2012 Time and Allocation Profiling Report  (Final)

           life21765U60 +RTS -p -RTS

        total time  =       30.15 secs   (30153 ticks @ 1000 us, 1 processor)
        total alloc = 24,382,856,840 bytes  (excludes profiling overheads)

COST CENTRE        MODULE    %time %alloc

lifeStep.grouped   Main       57.4   53.6
lifeStep.neighbors Main       24.7   40.9
lifeStep           Main       11.4    5.5
lifeStep.viable    Main        6.5    0.0


                                                                    individual     inherited
COST CENTRE               MODULE                  no.     entries  %time %alloc   %time %alloc

MAIN                      MAIN                     46           0    0.0    0.0   100.0  100.0
 CAF                      Main                     91           0    0.0    0.0   100.0  100.0
  gosperGliderGun         Main                     97           1    0.0    0.0     0.0    0.0
  main                    Main                     92           1    0.0    0.0   100.0  100.0
   runLife                Main                     93           1    0.0    0.0   100.0  100.0
    runLife.rec           Main                     94        5000    0.0    0.0   100.0  100.0
     lifeStep             Main                     95        4999   11.4    5.5   100.0  100.0
      lifeStep.viable     Main                     99    10002308    6.5    0.0     6.5    0.0
      lifeStep.grouped    Main                     96        4999   57.4   53.6    82.1   94.5
       lifeStep.neighbors Main                     98     2314620   24.7   40.9    24.7   40.9
 CAF                      Data.Set                 90           0    0.0    0.0     0.0    0.0
 CAF                      GHC.Conc.Signal          87           0    0.0    0.0     0.0    0.0
 CAF                      GHC.IO.Handle.FD         80           0    0.0    0.0     0.0    0.0
 CAF                      GHC.IO.Encoding          74           0    0.0    0.0     0.0    0.0
 CAF                      GHC.IO.Encoding.Iconv    62           0    0.0    0.0     0.0    0.0

We're actually only interested in that small table, so I'll omit the exhaustive one for the future. Basically, yes. grouped and neighbors are the resource-hogs here. Even still, this compares favorably against the Common Lisp infinite plane version; both in terms of program complexity and in terms of runtime. Not to mention that the initial CL version actually crashed at ~3000 iterations because it doesn't like tail recursion.

Anyhow, the first thing we're doing this time is limiting the size of the world.

inRange :: Ord a => a -> a -> a -> Bool
inRange low n high = low < n && n < high

lifeStep :: Int -> Set (Int, Int) -> Set (Int, Int)
lifeStep worldSize cells = fromList [head g | g <- grouped cells, viable g]
  where grouped = group . sort . concatMap neighbors . toList
        neighbors (x, y) = [(x+dx, y+dy) | dx <- [-1..1], dy <- [-1..1], 
                            (dx,dy) /= (0,0), inSize (dx+x) (dy+y)]
        inSize x y = inR x worldSize && inR y worldSize
        inR = inRange 0
        viable [_,_,_] = True
        viable [c,_] = c `member` cells
        viable _ = False

runLife :: Int -> Int -> Set (Int, Int) -> Set (Int, Int)
runLife worldSize steps cells = rec (steps - 1) cells
  where rec 0 cells = cells
        rec s cells = rec (s - 1) $! lifeStep worldSize cells

main :: IO ()
main = putStrLn . show $ runLife 50 5000 gosperGliderGun

That's gonna do the same thing it did yesterday; prevent massive, processor-fucking overpopulation.

        Fri Dec 14 22:03 2012 Time and Allocation Profiling Report  (Final)

           life21765GEE +RTS -p -RTS

        total time  =        1.61 secs   (1608 ticks @ 1000 us, 1 processor)
        total alloc = 1,132,473,192 bytes  (excludes profiling overheads)

COST CENTRE        MODULE  %time %alloc

lifeStep.grouped   Main     46.5   41.2
lifeStep.neighbors Main     23.4   37.8
inRange            Main     11.1   11.9
lifeStep           Main      6.2    3.0
lifeStep.viable    Main      6.1    0.0
lifeStep.inSize    Main      3.6    6.0
lifeStep.inR       Main      2.9    0.0

Granted, inRange is on the map as a cost center, but this shaved ~28 seconds off the final run time, I'm gonna call that fair enough. Given the numbers we were posting yesterday, I'm almost tempted to call this good enough. Lets see where it all goes, shall we? Step size of

50

        Fri Dec 14 22:06 2012 Time and Allocation Profiling Report  (Final)

           life21765TOK +RTS -p -RTS

        total time  =        0.03 secs   (29 ticks @ 1000 us, 1 processor)
        total alloc =  18,129,192 bytes  (excludes profiling overheads)

COST CENTRE        MODULE  %time %alloc

lifeStep.grouped   Main     55.2   42.0
lifeStep.neighbors Main     17.2   36.9
inRange            Main     13.8   11.7
main               Main      3.4    0.1
lifeStep           Main      3.4    3.2
lifeStep.inSize    Main      3.4    5.8
lifeStep.inR       Main      3.4    0.0

We've seen 5000 already, so

50 000

        Fri Dec 14 22:07 2012 Time and Allocation Profiling Report  (Final)

           life21765gYQ +RTS -p -RTS

        total time  =       15.94 secs   (15942 ticks @ 1000 us, 1 processor)
        total alloc = 11,262,873,192 bytes  (excludes profiling overheads)

COST CENTRE        MODULE    %time %alloc

lifeStep.grouped   Main       45.3   41.2
lifeStep.neighbors Main       23.0   37.8
inRange            Main       12.7   11.9
lifeStep           Main        6.6    3.0
lifeStep.viable    Main        5.9    0.0
lifeStep.inSize    Main        3.8    6.0
lifeStep.inR       Main        2.4    0.0

5 000 000

        Fri Dec 14 22:37 2012 Time and Allocation Profiling Report  (Final)

           big +RTS -p -RTS

        total time  =     1594.43 secs   (1594429 ticks @ 1000 us, 1 processor)
        total alloc = 1,125,606,873,896 bytes  (excludes profiling overheads)

COST CENTRE        MODULE    %time %alloc

lifeStep.grouped   Main       45.4   41.2
lifeStep.neighbors Main       23.6   37.8
inRange            Main       12.5   11.9
lifeStep           Main        6.2    3.0
lifeStep.viable    Main        5.8    0.0
lifeStep.inSize    Main        3.6    6.0
lifeStep.inR       Main        2.6    0.0

It's funny, after just clipping the board, we start getting much better numbers with unoptimized Haskell than we saw with unoptimized Common Lisp. That's not really much of a victory, since optimized lisp was handily beating the numbers we're putting down today, but it's also not the showdown I want to see. I want to know how optimized Haskell stacks up, and I want to know how Gridless Life stacks up to a gridded implementation. Back to Rosetta Code, I guess. Second verse same as the first; added a grid-appropriate gun[1] and stripped all but the final printing code.

import Data.Array.Unboxed
import Data.List (unfoldr) 

type Grid = UArray (Int,Int) Bool
 -- The grid is indexed by (y, x).
 
life :: Int -> Int -> Grid -> Grid
{- Returns the given Grid advanced by one generation. -}
life w h old =
    listArray b (map f (range b))
  where b@((y1,x1),(y2,x2)) = bounds old
        f (y, x) = ( c && (n == 2 || n == 3) ) || ( not c && n == 3 )
          where c = get x y
                n = count [get (x + x') (y + y') |
                    x' <- [-1, 0, 1], y' <- [-1, 0, 1],
                    not (x' == 0 && y' == 0)]
 
        get x y | x < x1 || x > x2 = False
                | y < y1 || y > y2 = False
                | otherwise       = old ! (y, x)
 
count :: [Bool] -> Int
count = length . filter id

grid :: [String] -> (Int, Int, Grid)
grid l = (width, height, a)
  where (width, height) = (length $ head l, length l)
        a = listArray ((1, 1), (height, width)) $ concatMap f l
        f = map g
        g '.' = False
        g _   = True
 
printGrid :: Int -> Grid -> IO ()
printGrid width = mapM_ f . split width . elems
  where f = putStrLn . map g
        g False = '.'
        g _     = '#'
 
split :: Int -> [a] -> [[a]]
split n = takeWhile (not . null) . unfoldr (Just . splitAt n)

gosperGliderGun = grid
    ["........................#.........................",
     "......................#.#.........................",
     "............##......##............##..............",
     "...........#...#....##............##..............",
     "##........#.....#...##............................",
     "##........#...#.##....#.#.........................",
     "..........#.....#.......#.........................",
     "...........#...#..................................",
     "............##....................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     "..................................................",
     ".................................................."]

printLife :: Int -> (Int, Int, Grid) -> IO ()
printLife n (w, h, g) = printGrid w . last . take n $ iterate (life w h) g
 
main = printLife 50 gosperGliderGun

Ok, lets rev this sucker up.

50

        Fri Dec 14 22:29 2012 Time and Allocation Profiling Report  (Final)

           life-grid21765tiW +RTS -p -RTS

        total time  =        1.32 secs   (1319 ticks @ 1000 us, 1 processor)
        total alloc = 891,555,608 bytes  (excludes profiling overheads)

COST CENTRE MODULE  %time %alloc

life.get    Main     59.9   50.4
life.f.n    Main     30.5   41.7
life.f      Main      3.9    3.0
count       Main      3.5    0.8
life        Main      2.0    3.9

5000

        Fri Dec 14 22:32 2012 Time and Allocation Profiling Report  (Final)

           life-grid217656sc +RTS -p -RTS

        total time  =      133.77 secs   (133771 ticks @ 1000 us, 1 processor)
        total alloc = 90,810,516,640 bytes  (excludes profiling overheads)

COST CENTRE MODULE  %time %alloc

life.get    Main     59.1   50.5
life.f.n    Main     31.3   41.8
count       Main      3.5    0.8
life.f      Main      3.4    3.0
life        Main      2.4    3.9

That's ... almost sad enough not to be funny. Almost. Do note for the record that this is an order of magnitude up from the gridless version with the same inputs. And when you think about what's involved in each traversal of each corpus, it kind of becomes obvious why that is. The grids' corpus traversal always has 2500 stops. The gridless traversal is somewhere between 50 and 100 for a comparably populated board of the same size. 2500 is our worst case, and we'll probably never hit it.

I'm not even going to bother profiling the higher steps with this approach if 5000 took two minutes. I do still want to see how low we can go, and how we'd go about it.

The first thought I have is to try out that iterate approach, rather than recurring manually

runLife :: Int -> Int -> Set (Int, Int) -> Set (Int, Int)
runLife worldSize steps cells = last . take steps $ iterate (lifeStep worldSize) cells

main :: IO ()
main = putStrLn . show $ runLife 50 50000 gosperGliderGun

Yes, it's more elegant. But will it blend?

        Fri Dec 14 22:45 2012 Time and Allocation Profiling Report  (Final)

           life21765UId +RTS -p -RTS

        total time  =        0.01 secs   (12 ticks @ 1000 us, 1 processor)
        total alloc =  20,022,728 bytes  (excludes profiling overheads)

COST CENTRE      MODULE  %time %alloc

runLife          Main     41.7   36.0
lifeStep.grouped Main     33.3   52.1
lifeStep         Main     25.0   11.7

Hm.

I'm gonna go ahead and put that one down to a profiler error, especially since running the same program in interactive mode confers no such magical acceleration. This does kind of call the process into question somewhat though...

Oh, well, I'm meant to be exploring. Lets pull the same incremental stuff we did with CL yesterday. Firstly, we're already using Set here, so the member check is already as tight as it's going to get. Our last valid profiler ping told us that lifeStep.grouped is where the big costs are paid, so lets see if we can't reduce them somewhat.

import qualified Data.Map as Map 
  (Map, lookup, insert, adjust, delete, fromList, toList)

frequencies :: [(Int, Int)] -> Map.Map (Int, Int) Int
frequencies list = rec list $ Map.fromList []
  where inc = Map.adjust (+1)
        rec [] m = m
        rec (cell:rest) m = rec rest newM
          where newM = if Nothing == Map.lookup cell m
                       then Map.insert cell 1 m
                       else inc cell m

lifeStep :: Int -> Set (Int, Int) -> Set (Int, Int)
lifeStep worldSize cells = fromList [fst g | g <- grouped cells, viable g]
  where grouped = Data.List.filter viable . Map.toList . frequencies . concatMap neighbors . toList
        neighbors (x, y) = [(x+dx, y+dy) | dx <- [-1..1], dy <- [-1..1], 
                            (dx,dy) /= (0,0), inSize (dx+x) (dy+y)]
        inSize x y = inR x worldSize && inR y worldSize
        inR = inRange 0
        viable (_,3) = True
        viable (c,2) = c `member` cells
        viable _ = False

We've added a Map of frequencies, rather than doing the naive group . sort thing. We've also had to tweak viable just a bit to accomodate.

        Fri Dec 14 22:54 2012 Time and Allocation Profiling Report  (Final)

           life21765ucp +RTS -p -RTS

        total time  =        2.41 secs   (2406 ticks @ 1000 us, 1 processor)
        total alloc = 1,216,439,760 bytes  (excludes profiling overheads)

COST CENTRE          MODULE    %time %alloc

frequencies.rec.newM Main       41.2   17.4
lifeStep.neighbors   Main       16.6   35.2
frequencies.inc      Main       12.4   12.2
lifeStep.viable      Main        9.4    4.7
inRange              Main        8.1   11.1
lifeStep.grouped     Main        3.4    8.2
lifeStep             Main        3.4    2.8
lifeStep.inSize      Main        2.3    5.6
lifeStep.inR         Main        1.4    0.0
frequencies.rec      Main        1.3    2.8

That's ... hm. Actually an increase of about a second. Maybe it does comparatively better on bigger data-sets?

main :: IO ()
main = putStrLn . show $ runLife 50 50000 gosperGliderGun
        Fri Dec 14 22:57 2012 Time and Allocation Profiling Report  (Final)

           life217657mv +RTS -p -RTS

        total time  =       23.96 secs   (23961 ticks @ 1000 us, 1 processor)
        total alloc = 12,100,319,760 bytes  (excludes profiling overheads)

COST CENTRE          MODULE    %time %alloc

frequencies.rec.newM Main       39.7   17.4
lifeStep.neighbors   Main       16.0   35.2
frequencies.inc      Main       13.3   12.2
lifeStep.viable      Main        9.5    4.7
inRange              Main        8.6   11.1
lifeStep.grouped     Main        3.6    8.2
lifeStep             Main        3.4    2.8
lifeStep.inSize      Main        2.6    5.6
lifeStep.inR         Main        1.8    0.0
frequencies.rec      Main        1.4    2.8

Nope. It actually does comparatively worse.

Hmmm.

I'm going to cut it here for now. I think I've done enough damage. I won't be putting the latest up[2] for obvious reasons. Yes, I peeked ahead, which is why I knew this particular optimization wouldn't work in Haskell early enough to foreshadow it, but I still wanted to formalize my thoughts about it.

It's hard not to learn something from playing with a languages' profiler. This experience tells me that I might have the wrong model in my head, or it might be that predicting where a traversal will happen is a lot more difficult in lazy languages, or, as I suspect from the latest profiler readouts, it might be that a Haskell Maps' lookup speed isn't constant time. The reason I suspect this is that some of our biggest cost centers are now frequencies.rec.newM (which does a Map.lookup each call) and frequencies.inc (which manipulates a particular element of a Map, so I assume a lookup is part of it).

I'm off to read up on Haskell data structures and test these hypotheses.

Oh. And heal up.


Footnotes

1 - [back] - (by the way, this makes clear that whatever the performance comparisons come down to, the gridless version has a more elegant notation)

2 - [back] - (though the limited-size version and the gridded competitor will be checked in)

Wednesday, December 14, 2011

Teensy Mode

Ok, this isn't one of those bullshit times where I just say "I'm going to bed" and then end up spending three hours explaining various shit at the expense of wakefulness the following day. Seriously, just one thing, I added a very quick, simple teensy-mode module to my emacs-utils project over at github. It turns out that I can't focus to save my life, but it has been damn enjoyable jumping into code so thoroughly after the set of weeks I've just been through.

The relevant bits are really just

(defun teensy-compile-current-file ()
  (interactive)
  (shell-command (format "%s %s" teensy-compile-file-command buffer-file-name)))

(defun teensy-compile-project ()
  (interactive)
  (shell-command teensy-compile-project-command))

(defun teensy-load-hex-file ()
  (interactive)
  (let ((hex-files (directory-files (file-name-directory buffer-file-name) nil "hex$"))
        (command (format "%s -mmcu=%s -wv " teensy-loader-program teensy-processor-type)))
    (cond ((not hex-files) (message "No hex file found"))
          ((= 1 (length hex-files)) (shell-command (concat command (car hex-files))))
          (t (shell-command (concat command (completing-read "Hex File: " hex-files)))))))

(defun teensy-compile-project-load-file ()
  (interactive)
  (progn (teensy-compile-project)
         (teensy-load-hex-file)))

the rest of it is various Emacs boilerplate like key and customization declarations. The one interesting thing I've learned from this experience is that the :options keyword does pretty much jack shit. In a hook customization, you can presumably at least see the list, but it still doesn't constrain your choices to what's there. It might be nice to have the option, since the customization I had in mind was the chip-type of your Teensy board (a required option that needs to be correct or it'll hang your chip, so it's sort of critical to get it correct).

I'm also flaking out on actual project compilation, opting to keep the make command involved, which has some seriaah fuck this, I'm going to sleep.

Saturday, March 26, 2011

A Little Bit of Elisp

I've had too much Common Lisp coding at work this week. I basically did two 12 hour sessions across Wednesday and Thursday, then a 4 hour on Friday with a little time off for fighting PHP-related fires and a bit of sleep. So today, I took a break. And what did I do on my break, you might ask?

I hacked Emacs Lisp.

I tell ya, my fiancee loves me. That's right. I took a break from my Common Lisp day-job by getting up on Saturday and dusting off some old Elisp code I had lying around. I touched up some git-mode customizations, an etags library I sometimes use, and my .emacs itself, but my main target was the blog-mode module (which I've actually been using to write these articles, except for one awkward brush with a markdown converter). It has served, but the code was far from elegant, and there were a couple of features I've been meaning to add, but never quite got around to, always telling myself to just get through the blog post instead. The code is still far from elegant, so I won't talk about that, but the features are there.

First thing, and probably the most pressing, is that those nice highlighted code-blocks were getting annoying. It would work fine for plain gray text (which I use sometimes, in small inline snippets), but to do it properly, I had to paste code into a separate buffer, turn on the correct highighting mode, run htmlize-buffer on it, then paste it back into the blog post and maybe tweak it for good measure. I figured that my ideal interaction would be the code auto-detecting what language I'm using and highighting correctly, but one step back would be asking for a highlighting mode and applying it to the code I wanted to htmlize. So here's how that looks

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; <pre> and <code> definitions
(definsert code-block "<pre>" "</pre>")
(definsert inline-code "<code>" "</code>")

;; region versions are more complicated to accomodate htmlize
(defun region-to-inline-code (code-mode)
  "HTMLize just the current region and wrap it in a <code> block"
  (interactive "CMode name: ")
  (let* ((start (region-beginning))
         (end (region-end))
         (htmlified (get-htmlified-region start end code-mode)))
    (delete-region start end)
    (insert-inline-code)
    (insert htmlified)))

(defun region-to-code-block (code-mode)
  "HTMLize the current region and wrap it in a <pre> block"
  (interactive "CMode name: ")
  (let* ((start (region-beginning))
         (end (region-end))
         (result (get-htmlified-region start end code-mode)))
    (delete-region start end)
    (insert-code-block)
    (insert result)))

(defun get-htmlified-region (start end code-mode)
  "Returns a string of the current region HTMLized with highlighting according to code-mode"
  (let ((htmlified nil))
    (clipboard-kill-ring-save start end)
    (get-buffer-create "*blog-mode-temp*") ;;using 'with-temp-buffer here doesn't apply correct higlighting
    (with-current-buffer "*blog-mode-temp*"
      (funcall code-mode)
      (clipboard-yank)
      (setq htmlified (substring (htmlize-region-for-paste (point-min) (point-max)) 6 -6)))
    (kill-buffer "*blog-mode-temp*")
    htmlified))

I pasted that block in from my code file, highlighted it, then typed C-c C-p emacs-lisp-mode [ret], in case you were wondering. The result was that pretty block above. region-to-code-block and region-to-inline-code are actually the same function except for which insert they use, and I would factor that out if it ever got to the point that there needed to be a third function doing the same, but it doesn't seem worth it for just two functions.

EDIT:

Ok, ok goddammit. Here. They're simplified now.

(defun region-to-inline-code (code-mode)
  "HTMLize just the current region and wrap it in a <code> block"
  (interactive "CMode name: ")
  (htmlized-region code-mode #'insert-inline-code))

(defun region-to-code-block (code-mode)
  "HTMLize the current region and wrap it in a <pre> block"
  (interactive "CMode name: ")
  (htmlized-region code-mode #'insert-code-block))

(defun htmlized-region (code-mode insert-fn)
  (let* ((start (region-beginning))
         (end (region-end))
         (result (get-htmlified-region start end code-mode)))
    (delete-region start end)
    (funcall insert-fn)
    (insert result)))
Sun, 27 Mar, 2011

I uh, also put in an edit block function and a footnote manager[1]. The edit blocks are pretty self-explanatory; just a block with a date at the bottom to indicate when I did the thing. After a couple of definition macros[2], it's actually a one-liner.

(deftag edit "<span class=\"edit\">EDIT:\n\n" (concat "\n" (format-time-string "%a, %d %b, %Y" (current-time)) "</span>"))

The footnote manager is a bit more complex. I've actually been doing them manually for the last little while, which started to get frustrating[3]. The process was to put a numbered tag down with <a name="somethingHopefullyUnique">, and hook it up to a correspondingly numbered [back] link at the bottom of the page, then write the footnote, then find my way back. The linking turns out to be the hardest part there, because these posts potentially get displayed together on my blog, so I had to be very careful to make the name unique across the entire blogs' history, not just within that article[4]. With this new function, instead it's C-c f to insert a fresh footnote, or C-c C-f to convert the selected region to a footnote. The links are generated and numbered automatically, so all I have to do is actually write the footnote[5].

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; footnote definitions
(defun insert-footnote ()
  "Inserts footnote, and a return link at the bottom of the file. 
   Moves point to footnote location."
  (interactive)
  (progn (footnotes-header)
         (let ((footnote-name (format-time-string "%a-%b-%d-%H%M%S%Z-%Y" (current-time)))
               (num (number-to-string (+ 1 (count-footnotes)))))
           (insert "<a href=\"#foot-" footnote-name "\" name=\"note-" footnote-name "\">[" num "]</a>")
           (goto-char (point-max))
           (insert "\n\n" num " - <a href=\"#note-" footnote-name "\" name=\"foot-" footnote-name "\">[back]</a> - "))))

(defun region-to-footnote ()
  "Inserts a footnote at point and return link at the bottom. Moves the current region to the end of the file. 
   Leaves point where it is."
  (interactive)
  (save-excursion (kill-region (region-beginning) (region-end))
         (insert-footnote)
         (yank)))

(defun footnotes-header ()
  "Inserts footnote header if not already present"
  (unless (save-excursion (search-forward blog-footnote-header nil t))
    (save-excursion 
      (goto-char (point-max))
      (insert "\n\n" blog-footnote-header))))

(defun count-footnotes ()
  "Returns the number of footnotes in the current file. Used for human-readable note labels"
  (interactive)
  (save-excursion
    (if (not (search-forward blog-footnote-header nil t))
        0
      (let ((count -1))
        (while (progn (setq count (1+ count))
                      (search-forward "<a href=\"#note-" nil t)))
        count))))

Boy, that's playing hell with the highlighting right now. It's fairly self-explanatory; count-footnotes counts up how many footnotes I have left, footnotes-header checks if there's a footnote header in the post already[6], insert-footnote just creates a new footnote/backlink and takes me to the bottom of the page to write it, and finally, region-to-footnote takes the current region and converts it to a new footnote (leaving the point where it is).

Even though it's a simple, and specific[7] piece of code, I still learned a lot by testing it out like this. Specifically, the code formatting functions need to accept nil as an argument[8] (which should take 5 minutes), and the footnote section needs a way to re-number footnotes and jump between corresponding note/back links (which seems like it could take a while).

I'm going to sleep now though; I'll leave those features for the next time I need a break from Common Lisp.

EDIT:

Ok, so it was actually slightly less than 5 minutes to get the code argument done; one line change did it (see if you can guess which one)

(when (fboundp code-mode) (funcall code-mode))

The latest is now up at github.

Sun, 27 Mar, 2011

Footnotes

1 - [back] - And yes, since you ask, I am basically using this post as a way to test the editing mode I'm talking about.

2 - [back] - If you want to see the definition macros, check out the github page I started for my little utility files. The documentation is extremely light, but it's only because I fully expect to be the only one using these.

3 - [back] - To the point that I would frequently include a long, rambling paranthetical comment instead of putting the damned thought in a footnote, where it belongs. Interface difficulties really do lead to a lot of shoddy work, it seems.

4 - [back] - The way I'd been doing that was by using the article name and a number in the href and name parameters. The mode is actually better, using a date and timestamp.

5 - [back] - I still haven't found a way to automate writing these columns, but that's not the same as saying it can't be done.

6 - [back] - And adds one if it doesn't exist yet.

7 - [back] - Which is to say, it had a very specific goal in mind.

8 - [back] - (and default to fundamental-mode in that case)