blog.kfish.org

My name is Conrad Parker, and I live in Kyoto, Japan. I work with Renesas in Tokyo, designing the Linux multimedia architecture for a new line of mobile processors; and for Wikimedia Foundation, working on Ogg integration for Mozilla Firefox. I am also working towards a PhD in Computer Science at Kyoto University. Free software projects include the Sweep sound editor and the Annodex media system, and various smaller ones that you can read about here.

Sunday, 1 March 2009

Random code: Pretty printing durations in Haskell

Recently I've really enjoyed reading blog posts which just explain a little bit of code, so that's what this is. I had this code lying around from a few months ago so I added some context and links. It combines two of my favourite things: Annodex and Haskell!

YouTube's video offset syntax

Some time last year, YouTube introduced a feature which allows you to specify a hyperlink that plays a video from a given time offset. If you used the syntax on a random video site, it would look like this:

http://www.example.com/player.html#t=3m54s

That syntax for this is very close to that which we use in Annodex for Temporal URIs, now running on Archive.org (and soon on Wikipedia):

http://www.example.com/video.ogv?t=3:54

Two differences:

1. YouTube uses a fragment instead of a query parameter.

A fragment is something starting with '#' that tells the client to jump to a particular offset in the document -- in general the fragment text is never seen by the server. In the case of YouTube the HTML page contains JavaScript that tells the embedded Flash video player to seek to the offset in the video.

Fragments are useful in this use case, where you are instructing the embedding web page to play the video from a given time offset. How it actually retrieves the video from the network is not specified, but importantly there is no requirement for the embedding web page to be reloaded.

(This distinction between fragments and queries is part of the W3 Media Fragments WG discussion on syntax).

2. The syntax uses unit markers h, m, s to separate the parts of time, whereas our specification uses the kind of specifiers common in industrial equipment (and clock radios).

Perhaps one advantage of the format YouTube have chosen is readability: sometimes it is difficult to read times such as 03:36:14.

http://www.example.com/video.ogv?t=3:54
http://www.example.com/video.ogv?t=00:03:54.000
http://www.example.com/video.ogv?t=npt:00:03:54.000
http://www.example.com/video.ogv?t=smpte-25:00:03:54::0

We had a recent discussion about these issues in the Media Fragments WG: Action-28: updated syntax document with time formats. I'm pretty happy with the syntax we have settled on, allowing for both readable short timestamps and more accurate long ones.

Pretty printing of durations

Anyway, I was bored so I hacked up a sweet fold to display the format used by YouTube.

Haskell hackers use folds like C programmers use for loops; the Haskell wiki page Fold is a beautiful introduction to the topic. My favourite Web 1.0 interactive visualization of a left fold is at foldl.com (and also be sure to check out its companion site for right folds, foldr.com).

Here's a concise fold that gets us most of the way to the right syntax:

> ts = [("ms", 1000), ("s", 60), ("m", 60), ("d", 24), ("y", 365)]
>
> duz ms = ss
>   where (ss, _) = foldl (\(ss, x) (s, y) -> (show (rem x y) ++ s ++ ss, quot x y)) ("", ms) ts

Yeah, concise. Read it slow! if it was in C or Python, that one-liner would be a 10 or 5 line loop.

You might say that you use the fold function to iterate through a list of time units, and at each step of the iteration you do an integer division by the unit, label the remainder, and pass the quotient on to the next step of the iteration. A real Haskell programmer, however, might say something like "you fold the duration quotiently through the units, labelling into the syntax!", with much wringing of hands and wishful glances for abstract ponies. Fold is a verb, because functions are alive! Quotiently is not a word.

A problem with duz (apart from the crappy name) is that it shows times like 0y0h3m54s0ms. The next implementation of duration strips the leading and trailing zeroes:

> dur ms = years:rest
>   where (rest, years) = foldl (\(ds, x) y -> ((rem x y):ds, quot x y)) ([], ms) [1000, 60, 60, 24, 365]
>
> duration ms = concat $ map (\(n, s) -> show n ++ s) (takeWhile (not . zero) $ dropWhile zero labelled)
>   where labelled = zip (dur ms) ["y", "d", "h", "m", "s", "ms"]
>         zero (n, _) = (n==0)

eg. to display the duration of 2^32 milliseconds:

*Main> duration (2^32)
"49d17h2m47s296ms"

*Main> duration 3600000
"1h"

Fold is a generic list processing device; if you want to limit the amount of the list that is processed, you can use functions like takeWhile and dropWhile. These will take, or drop, elements from the list as long as some criterion is satisfied; you can use them both together to trim both the start and end of the list. Of course you can use these on the input list to limit what data is processed; but because Haskell evaluates lazily, you can also use these on the output list to limit how much of the processing is actually done (like in duration above). The bits of the evaluation that don't really need to get done, aren't: the idea of doing them is written down (on a "thunk") and thrown away. Burn your todo lists! Be lazy lazy lazy! Haskell rules. Do you like verbs?

Labels: ,

2 Comments:

Blogger meteficha said...

But, what about 'duration $ 60*60*1000 + 10'? You should use 'filter ((/= 0) . fst)' instead of those take/drop.

And, for your enjoyment, an alternative implementation:

import Control.Arrow

duration ms = concatMap (\(s,n) -> show n ++ s) . filter ((/= 0) . snd) . zip ["y", "d", "h", "m", "s", "ms"] . uncurry (:) $ foldl (\(x,ds) -> quotRem x >>> second (:ds)) (ms,[]) [1000, 60, 60, 24, 365]

Ugly, strange, contrived, or beautiful? Who knows... but it is a 204-char one-liner! =p

8 March 2009 03:21  
Blogger Conrad Parker said...

meteficha, nice :-)

Actually, I was never really into tuples. They always felt kinda wierd and anonymous, and too unstructured to be useful. You can see above, I thought nothing of decoupling them.

I'd read about Arrows, and knew they were neat; but still they seem a bit contrived. Now I'm reading your examples here and I realize what I was missing: a good grasp of all those tuple functions I'd been rejecting.

Like fst: 'filter ((/= 0) . fst)' is cleaner.

Like uncurry! 'uncurry (:)' awakens demons.

So the Arrow syntax gives us some extra cool plumbing, but to use it well we also need the basic tuple functions. Worth thinking about more, thanks :-)

8 March 2009 07:32  

Post a Comment

Links to this post:

Create a Link

<< Home