Yi diary example

A diary example

For years now, I’ve used a flat text file to contain all my notes, work-in-progress and stuff like that. I can hit f12 in emacs to jump to (or leave) the ~/diary.txt file, and meta-f12 to a new entry to the diary (with a datestamp). The diary file looks something like this:

------------------------------------------------------------------
* Sat Jan 12 17:10:43 2008

Was messing around with yi again.  Must remember to run the
BrainM monad so that I don't forget it all.

------------------------------------------------------------------

To implement this in yi, we’ll define a ‘diaryNew’ function which finds the diary.txt file and adds a timestamped section to the top of the file.

Simplified version

To make things easy, we’ll omit the timestamp for now and return to it later. The code is then fairly straightforward:

module YiConfig (yiMain,diaryAdd) where

...

diaryFile = "/tmp/diary.txt"

diaryAdd :: YiM ()
diaryAdd = do 
  fnewE diaryFile
  withBuffer addDiaryEntry

addDiaryEntry :: BufferM ()
addDiaryEntry = do
  let separator = (replicate 72 '-') ++ "\n"
  moveTo 0
  insertN separator
  insertN "* Today \n"
  insertN "\n"
  savingPointB $ insertN ("\n\n" ++ separator)

fnewE is equivalent to find-file in emacs. Notice that addDiaryEntry records the position of point midway through inserting the text so that it can leave the cursor in the right place at the end.

Adding the timestamp

Now let’s add a proper timestamp. Once again, since haskell is a pure language there is no function which “returns the current time”. Instead, there is a function, Time.getClockTime, which returns a “recipe” which, when run, will yield the current time. The question therefore becomes how do we run that recipe?

Let’s start with some code builds such a recipe:

import Time 

...

timestamp :: IO String
timestamp = do
  clock <- getClockTime 
  putStrLn "Got the clock time"
  cal <- toCalendarTime clock
  return $ calendarTimeToString cal

How can we get at the actual timestamp value? We’ve only got an “IO String” which is more like a recipe that, when run by someone else, yields a String. Unfortunately, it’s not clear who can run it for us. We certainly can’t do it ourself because “IO a” is an opaque type. The answer is in the next section, but I’ll go on a quick digression about the IO monad first.

The important thing about the IO monad is that it forces an explicit ordering onto calculations (something which haskell doesn’t normally do). The “timestamp” function above is saying “get the clock time THEN print a message to stdout” whereas normally in haskell the runtime can choose to evaluate your code in whatever order it likes.

You could think of the IO monad as being a State monad (with the state of the whole outside world). Therefore, values of type “IO String” would just be recipes which potentially read/update that state and then yield a String.

In practise, the haskell runtime never actually needs to maintain the whole “state of the world”. That’s because the only thing you can do with an IO value is to return it from main and let the haskell runtime run the recipe. The haskell runtime takes advantage of the fact that there is a strict ordering on the computations. This means that it can just execute the computation step by step, happily utilizing (non-haskell) functions with side-effects, like the C functions printf or getc.

Timestamps in Yi

So now we’re ready to bolt our “timestamp” function (with type “IO String”) into yi. We’ll first augment our addDiaryElement helper to take the string timestamp as an argument. Then we’ll change “diaryAdd” to somehow get the timestamp string out of the IO monad.

import Control.Monad.Trans

...

addDiaryEntry :: String -> BufferM ()
addDiaryEntry timestring = do
  let separator = (replicate 72 '-') ++ "\n"
  moveTo 0
  insertN separator
  insertN $ "* " ++ timestring ++ "\n"
  insertN "\n"
  savingPointB $ insertN ("\n\n" ++ separator)

diaryAdd :: YiM ()
diaryAdd = do 
  fnewE diaryFile -- sets current buffer
  timestring <- liftIO timestamp
  withBuffer (addDiaryEntry timestring)

That was surprisingly easy. We can turn an “IO String” recipe into a “YiM String” recipe just by using the liftIO function.

To understand this, we need to look at the definition of “YiM” in Keymap.hs. To make the YiM monad, we started with the IO monad, and used the ReaderT monad transformer to mix in Reader functionality. This means that IO values need to be “lifted” into the compound monad. There is a general purpose “lift” function in the MonadTrans type class, as well as “liftIO” in the MonadIO type class. In other words, the YiM monad provides us with a route right up to the top level IO value returned from main. Except, because yi is a dynamic haskell applications, the details are a little more complex.

Of course, you can just think of “liftIO” as a magic way of turning an IO value into a YiM value. :-)

Concise-ification

A bit more restructuring gives a more terse version of the functions:

timestamp :: IO String
timestamp = getClockTime >>= toCalendarTime >>= return . calendarTimeToString

addDiaryEntry :: String -> BufferM ()
addDiaryEntry timestring = do
  moveTo 0
  insertN $ unlines [ dashes, "* " ++ timestring, "", "", "", dashes ]
  replicateM_ 3 lineUp
    where dashes = (replicate 72 '-')

Next is another example involving i/o; implementing the emacs shell-command function in yi.

Back to comment-region example, up to the index, or onwards to shell-command example.