Missing Version Numbers

Back in June it was announced that the next Perl release would be called Perl 7. A quick look back at Perl's history reveals that this is a little unusual. Perl 5 was originally released in October 17, 1994 and served its users well for a number of years. In 2000 Perl 6 was announced as a ground-up redesign of the language and it spent a long time in development. It eventually surfaced in 2015 but in 2019 was renamed "Raku" to distinguish it from traditional Perl. This leaves Perl 6 as a sort of "orphaned" version number.

In the meantime while Raku née Perl 6 was being developed, the team behind Perl 5 had delivery numerous point releases and retained an active community of loyal users. Many of these had no desire to migrate to Raku and wanted a number of quality of life improvements in their language, ecosystem and libraries. The sort of changes being proposed were bigger than you'd expect from a point release, so a new major version was required. Hence Perl 7 - the real next version of Perl.

So we now have quite an interesting situation, let's take a look at the release history:

Version Date Comment
Perl 4.000 21st March 1991
Perl 5.000 17th October 1994
Perl 6 - Actually "Raku", little resemblance to Perl 5
Perl 7 Planned 2021+ Refined and improved Perl 5

Over the years many commercial and open source projects have skipped version numbers leaving their own little version holes with their own various explanations. Today we'll take a look at a number of similar situations like this, and we'll try to understand what happened in each case and why.

A fairly typical Perl program which calculates π


Perl was not the first language to skip a version, in fact it wasn't even the first scripting language beginning with "P" to skip version 6. A quick glance at PHP's release history reveals the following:

Version Date Comment
PHP 4.0.0 22 May 2000
PHP 5.0.0 13 July 2004
PHP 6.0.0 - Never released
PHP 7.0.0 3 Dec 2015

So what's the story here? Well in common with most software written in the early '00s PHP did not have particularly good Unicode support. The developers behind PHP recognised this and planned a rewrite - PHP 6 - that used Unicode to represent strings internally, specifically UTF-16. Work began in 2005 and would continue for a few years. While some of the planned PHP 6 features were completed on schedule, the Unicode rewrite stalled and became a major blocker to the release. In the meantime Unicode did indeed begin to take off ...

... however UTF-16 didn't. UTF-8, a different standard, rapidly emerged as the preferred Unicode standard on the web. This was particularly problematic since it became apparent that PHP 6 had some serious performance issues dealing with converting strings to and from UTF-8, and since web applications were PHP's bread and butter this was a big deal. In 2010, five years into the PHP 6's development the project was abandoned. Much of the non-Unicode features of PHP6 were ported into PHP 5 and released in version 5.4.

In 2014 as a new major release of PHP was being planned the topic of what it should be called was raised and it was decided that for the sake of removing any confusion or ambiguity the next version of PHP would be PHP 7. There are plenty of justifications given, but the first one given in the RFC I linked really seals it

First and foremost, PHP 6 already existed and it was something completely different. The decimal system (or more accurately the infinite supply of numbers we have) makes it easy for us to skip a version, with plenty more left for future versions to come.

This makes plenty of sense - PHP 6 was already a thing, and it's not like integers are a finite resource. If this was Ubuntu and they were considering discarding a good adjective/animal pair then perhaps it'd be slightly different

ECMAScript 4

Brendan Eich developed Javascript over the course of 10 short days at Netscape, and it soon became the defacto scripting language for the web. This status was confirmed by Ecma who issued a number of standards and referred to it as "ECMAScript". Nowadays they release these standards on a yearly basis (the latest being ECMAScript 2020, published June 2020) but at the end of 2009 we had five ... kinda:

Version Date Comment
ECMAScript 1 June 1997
ECMAScript 2 June 1998
ECMAScript 3 December 1999
ECMAScript 4 -
ECMAScript 5 December 2009

The first two largely captured and formalised the existing Javascript behaviour, but the committee began to get a little more adventurous in the third version, introducing features like regular expressions and try/catch. I can't find a very good summary other than the Wikipedia one (which seems to be copy-pasted everywhere) and I'm not going to read the entire spec.

Anyway the folks at Ecma recognised the potential of rich client-side web applications, and recognised that in its current state Javascript aka ECMAScript was not particularly well suited this task in its current state:

Though flexible and formally powerful, the abstraction facilities of ES3 are often inadequate in practice for the development of large software systems. ECMAScript programs are becoming larger and more complex with the adoption of Ajax programming on the web and the extensive use of ECMAScript as an extension and scripting language in applications. The development of large programs can benefit substantially from facilities like static type checking, name hiding, early binding and other optimization hooks, and direct support for object-oriented programming, all of which are absent from ES3.
So they intended to address this and more by creating ECMAScript 4, which was supposed to completely overhaul Javascript. Object-Oriented Programming was to be embraced (with classes and interfaces being introduced) and a rich type system was to back it it up (with type annotations available to assist with correctness and provide the runtime with hints to help with compile and runtime optimisations). Numeric values would no longer just be limited to floating point - developers would now gain access to byte, int, uint, double, and even decimal numeric types. They'd even be able to define Algebraic Data Types which, at the time, was only really available in niche languages like Haskell, SML, Ocaml and has only recently started to break into the mainstream. The ability to structure applications using namespaces and packages would make building larger applications composed of many separate modules more manageable. So while previous specifications either captured the status quo or made small incremental enhancements, ES4 was to be a complete redesign of the language with sweeping changes on a scale the committee hadn't yet encountered.

This concerned a number of people - most notably it drew the ire of Microsoft's IE team, who were concerned that many of the changes were not backwards-compatible. Chris Wilson, platform architect of the IE team at the time said this:
For ECMAScript, we here on the IE team certainly believe that thoughtful evolution is the right way to go; as I've frequently spoken about publicly, compatibility with the current web ecosystem - not "breaking the Web" - is something we take very seriously. In our opinion, a revolution in ECMAScript would be best done with an entirely new language, so we could continue supporting existing users as well as freeing the new language from constraints (including the constraint of performantly supporting scripts written in the old language)
Brendan Eich did not agree, and wrote a blog post titled "Open letter to Chris Wilson" which accused Wilson of being disingenuous, and Microsoft of suddenly proposing ECMAScript 3.1 without regard for the ES4 effort:
You seem to be repeating falsehoods in blogs since the Proposed ECMAScript 4th Edition Language Overview was published, claiming dissenters including Microsoft were ignored by me, or “shouted down” by the majority, in the ECMAScript standardization group
These ES3.1 proposals came suddenly in March after years of passive participation by Microsoft, while the rest of the group worked on ES4. They have not been touched since April.
It was clear that there would be no agreement. Microsoft effectively held all the cards - IE was at that time the dominant browser by some distance. If Ecma somehow pushed forward with ES4, it's entirely possible that Microsoft could've just ignored it altogether. ES4 was abandoned, and years later ES5 was proposed with slightly more modest, incremental changes. This is a strategy that has continued to this day - where the language evolves but remains backwards compatible. Some ES4 features (such as classes) ended up ECMAScript anyway, and in a curious twist Microsoft created Typescript which takes even more ES4 features like interfaces, union types and type annotations.


IPv4 and IPv6 are pretty well known - but you never really hear of any other versions. IP specification took shape towards the end of the late 1970s. As engineers struggled to figure out the exact problem they were solving and the way it should be solved they quickly issued a number of iterations and ultimately produced what we now know as IPv4. In the 1990s it became apparent that the number of devices accessing the Internet would ultimately exceed the number of available addresses IPv4 could provide (a bit less than 4.3 billion when you factor in reserved blocks) - so a new version of the protocol was drafted called IPv6. There were of course other changes and motivations in IPv6, but literally the first bullet point in the IPV6 spec deals with increasing this address space - bumping it up to 128 bits (good for 340,282,366,920,938,463,463,374,607,431,768,211,456 addresses).

Trying to produce a coherent history is a little difficult due to how both TCP and IP were reshaped and redefined during the early period. Version headers in packets are resized and rebased from one version to the next. Thankfully we have RFC 755 which puts its foot down and authoritatively states the following timeline:
    Version Date Comment
    IPv0 March 1977
    February 1978
    February 1978
    IPv3 February 1978

    IPv4 June 1978
    IPv5 - Abandoned?
    IPv6 December 1995

    Note: For IPv6 we could linked RFC 1883 (where it was first specified) or RFC 8200 (which made a few additional changes to the protocol)

    So the only real missing version is IPv5 - which actually spent a couple of decades being defined as the "Internet Stream Protocol", never really went anywhere and is effectively abandoned. Honestly though if you're interested you should check out this article by Matthäus Wander. I spent ages bouncing around different RFCs and IENs and couldn't really piece everything together until Matthäus laid it all out so go read his article.

    IPv6 adoption is slow and steady, currently sitting at 33% worldwide as of 16th October 2020

    Node.js 1.0.0 to 3.0.0

    Node.js came into existence in 2009, promising developers that they could implement performant backend code in a simple language like Javascript thanks in no small part to Google's V8 engine. I personally played with it a bit every now and then and used things like Ghost, but no more than every couple of months. Then one day I tried to run some example code only to find it didn't work and was 5 major versions out of date. I was pretty confused and tried to figure out if there was some reason I was so far behind. A quick look through Node.js' release history reveals the reason behind my confusion:

    Version Date Comment
    Node.js 0.10.0 11 March 2013
    Node.js 0.11.0 28 March 2013
    Node.js 0.12.0 6 February 2015
    Node.js 1.0.0 - Never existed
    Node.js 2.0.0 - Never existed
    Node.js 3.0.0 - Never existed
    Node.js 4.0.0 8 September 2015
    Node.js 5.0.0 29 October 2015

    So between February and September in 2015 Node.js jumped from version 0.12 to 4.0, and then a month later to 5.0. So what happened here? Well apparently a number of members of the Node.js community were dissatisfied with Joyent's stewardship of the project, forked it under the name "io.js" and released a handful of major versions - 1.0, 2.0 and 3.0.

    Ultimately Joyent, io.js and the community reached some sort of agreement, merged their codebases, and released the new version as Node.js 4.0. So if we amend our timeline with to include the io.js releases ...

    Version Date Comment
    Node.js 0.10.0 11 March 2013
    Node.js 0.11.0 28 March 2013
    io.js 1.0.0 14 January 2015
    Node.js 0.12.0 6 February 2015
    io.js 2.0.0 4 May 2015
    io.js 3.0.0 4 August 2015
    Node.js 4.0.0 8 September 2015
    Node.js 5.0.0 29 October 2015

    .. everything looks a little bit clearer.

    Java 2.x to 4.x

    I have only ever been casual observer of Java - I used it a little at University for my Data Structures & Algorithms modules but rarely touched it otherwise. Whenever I needed to think about Java versions it was because it was used by a program I needed to use, and I never quite followed its versioning. I just know that I never really knew what I was using. It was basically "Java" on versions 1.0 and 1.1, then "Java 2 Standard Edition" for a while after ... but retained the "1.x" major version - so versions 1.2, 1.3 and 1.4. Then at some point we were dealing with Java/J2SE 5.0 aka J2SE 1.5.

    Version Date Comment
    JDK 1.0 January 1996
    JDK 1.1 February 1997
    J2SE 1.2 December 1998
    J2SE 1.3 May 2000
    J2SE 1.4 February 2002
    J2SE 2.0 - Never existed
    J2SE 3.0 - Never existed
    J2SE 4.0 - Never existed
    J2SE 5.0 February 2004
    Java SE 6.0 December 2006

    Sun talk a little bit about the version number here but really here's all you need to know:
    Both version numbers "1.5.0" and "5.0" are used to identify this release of the Java 2 Platform Standard Edition. Version "5.0" is the product version, while "1.5.0" is the developer version. The number "5.0" is used to better reflect the level of maturity, stability, scalability and security of the J2SE.

    The number "5.0" was arrived at by dropping the leading "1." from "1.5.0". Where you might have expected to see 1.5.0, it is now 5.0 (and where it was 1.5, it is now 5).

    This was some years before the concept of semantic versioning came about so I suspect this is mostly just a kind of marketing effort - they realised they'd stuck with 1.x for a while and couldn't really come up with a good path to a 2.x and had kind-of already burned that  by using "J2SE" so decided to hit the reset button with 5.0.

    DirectX 4

    Anyone who played PC games within the last 25 years will have had some interaction with DirectX - the collective name for Microsoft's suite of graphics, sound and networking APIs meant for gaming.

    Version Date Comment
    DirectX 1 30 September 1996
    DirectX 2
    5 June 1996
    DirectX 3 15 September 1996
    DirectX 4 - Never released
    DirectX 5 4 August 1997
    DirectX 6 7 August 1998

    After Microsoft released DirectX 3 they launched two successor projects concurrently - one was a smaller, incremental release (DirectX 4) and the other was a larger release with a swathe of new functionality (DirectX 5). Apparently after Microsoft consulted with the community they discovered that there was little interest for the features planned for DirectX 4, and a lot of enthusiasm for DirectX 5. Microsoft simply ceased development on DirectX 4 and focused on delivering DirectX 5. Fun fact: around this time John Carmack called DirectX's D3D "a horribly broken API"

    X-Wing vs TIE Fighter, one of the games to use DirectX 5

    Watcom 5.0 and earlier

    Back in the 1980s Open Source development tools were not yet widespread, so you could buy compilers from a few different companies. One such company was Watcom, which was formed by Unversity of Waterloo students and had a fair amount of success with a Fortran compiler to which they later added a C frontend. Their compilers initially targeted IBM's mainframe architectures (Series 1, System/370 etc) but as 1980s bore on the the IBM PC platform started to take off. In 1988 they launched their first C compiler for IBM platforms - Watcom 6.0

    Version Date Comment
    Watcom 5.0 - Never existed
    Watcom 6.0 1988
    Watcom 7.0 1989
    Watcom 8.0 1990
    At the time the dominant compiler products were Borland and Microsoft C compilers - both of which were on version 5. In an effort to be seen as competitive (or perhaps to be recognised as better than) their competitors, they launched it with version 6.0 - literally one-upping their more established rivals. Watcom's C compiler was for a brief moment hot property - famed for being one of the better optimising compilers available for the PC it was widely used in the gaming industry, notably by the newly formed id software who would go on to use it to build Doom and Doom 2 (more on these at filfre.net). There are also references to Watcom in the Quake source code, but I don't know if that means anything.

    I couldn't find a screenshot of anything related to Watcom 6.0 but here's the Watcom 9.0 debugger "WVIDEO"

    Slackware 5 and 6

    For some reason Slackware was the first ever Linux distro I used. I have no idea why I chose it, but looking back it would've been better if I had a more user-friendly one like RedHat or SuSE. Using software on Slackware often involved compiling it yourself, which usually meant hunting down, downloading and compiling dependencies (possibly even encountering more dependencies which require you to hunt down, download and compile sub-dependencies (possibly even encountering more dependencies ... etc)). Between May and October in 1999 they jumped from using 4.0 to 7.0

    Version Date Comment
    Slackware 1.0 17 July 1993
    Slackware 2.0 2 July 1994
    Slackware 3.0 30 November 1995
    Slackware 4.0 17 May 1999
    Slackware 5.0 - Never existed
    Slackware 6.0 - Never existed
    Slackware 7.0 25 October 1999

    The reason given for skipping these versions is explained in the FAQ which is still up:
    I think it's clear that some other distributions inflated their version numbers for marketing purposes, and I've had to field (way too many times) the question "why isn't yours 6.x" or worse "when will you upgrade to Linux 6.0" which really drives home the effectiveness of this simple trick. With the move to glibc and nearly everyone else using 6.x now, it made sense to go to at least 6.0, just to make it clear to people who don't know anything about Linux that Slackware's libraries, compilers, and other stuff are not 3 major versions behind. I thought they'd all be using 7.0 by now, but no matter. We're at least "one better", right? :)
    Let's take a look at the latest version numbers of some of the "other distributions" at the time of Slackware 7.0's release - October 1999:
    • Mandrake 6.1 (released September 1999)
    • RedHat 6.1 (released September 1999)
    • SuSE 6.2 (released August 1999)
    • Debian 2.1 (released March 1999)
    I'm sure you'll see a pretty clear difference - Mandrake, RedHat and SuSE were all commercial linux distributions in competition with each other. So it's understandable that they wouldn't want to seem out of alignment or behind . As for Debian, it was always more of a hackers distro - it was and still is very much Free Software at it's core (which you'll know if you downloaded the default ISO and tried to get your wifi working...) so presumably they felt no pressure to take part in this.

    So going back to Slackware - this always puzzled me. In my opinion Slackware was more similar to Debian than to the commercial distros. I imagine users would've been perfectly content to move on from Slackware 4 to Slackware 5 without worrying about what that version number implied. I don't think it's controversial to suggest that Debian has the last laugh. Mandrake became Mandriva then largely disappeared. RedHat the company has pivoted to where it's distro is not its main focus. SuSE still exists but is relatively niche compared to Debian and its derivative distributions.

    Slackware 4 running KDE 1. Sidenote: that World of Spectrum page looks amazing on what looks like a very old Netscape Navigator

    SuSE, Mandrake and Ubuntu oddities

    There are three other major Linux distributions which seemingly have missing versions - SuSE, Mandrake and Ubuntu. They all have initial releases which are not 0.x or 1.x and which have simple explanations so I've lumped them all together here.

    Version Date Comment
    SuSE Linux 4.0 - Never existed
    SuSE Linux 4.1 - Never existed
    SuSE Linux 4.2 May 1996
    SuSE Linux 4.3 September 1996

    The first release of SuSE Linux was version 4.2. This is a reference to Hitchhiker's Guide to the Galaxy where a computer called Deep Thought calculates the answer to "the Ultimate Question of Life, The Universe, and Everything" as 42. Incidentally the first version of its package manager YaST is 0.42

    Version Date Comment
    Mandrake 4.0 - Never existed
    Mandrake 5.0 - Never existed
    Mandrake 5.1 July 1998
    Mandrake 5.2 December 1998
    Mandrake 5.3 February 1999

    Mandrake Linux's first ever release in July 1998 was version 5.1. It was based on RedHat Linux 5.1 so it's likely they just borrowed that, perhaps they intended to stay aligned with RH's releases and wanted to show they were compatible.

    Version Date Comment
    Ubuntu 2.xx - Never existed
    Ubuntu 3.xx - Never existed
    Ubuntu 4.10 October 2004
    Ubuntu 5.04 April 2005
    Ubuntu 5.10 October 2005

    The final Linux distro I'll talk about is dubious to include as there's not really any missing or skipped version numbers really. Ubuntu's first ever release was 4.10 - so it would seem that they skipped versions 1, 2 and 3. However Ubuntu's version numbers are based on the year and month of release, so version 4.10 implies a release date of October 2004, 5.04 is April 2005 and so on.


    I only scratched the surface of this subject, really. If look around then you'll start to see missing versions everywhere. For example, what happened to Windows 9? How about .NET [Core] 4, MySQL 6 and 7 or Akumulátor 2? As soon as you apply an sequential version to some software you make a statement a statement about how that software relates to any previous or future releases - whether or not you adhere to a strict set of rules like semver. This is generally intended to help users and it largely works as intended but does not really give any strong prescription for how to handle abandoned releases or forked-then-remerged projects. As a consequence we have to be cool with the idea that skipping a version or two is perhaps preferable than introducing confusion or even discarding any notion of sequence whatsoever - imagine wondering if it's safe to upgrade from PHP e1daa19 to PHP caf3c64.

    Jupyter Notebook - problems with dotnet-try under IIS

    I keep encountering things when setting up Jupyter! After I got it running under IIS I went to bed then picked it up the next evening, tried to install the C# and F# kernels from dotnet try. Of course I hit two snags:

    Issue 1. C# and F# Kernels aren't available at all

    I installed it as usual by opening up Windows Terminal, source-ing my venv, installing dotnet-try globally and installing the dotnet try kernel. I suspected I was gonna encounter something a little odd - I wasn't sure if the dotnet try would respect the venv when I installed jupyter. Turns out the kernel got installed in a weird location (under AppData/Roaming rather than in my venv dir). I did a bit of experimenting and this was how I ended up resolving the issue. First I updated the PATH so that it included both the venv directory and the dotnet tools directory:

        <aspNetCore processPath="python.exe" 
            <environmentVariable name="PYTHONPATH" value="." />
            <environmentVariable name="PATH" value="%PATH%;C:\Users\sean\.dotnet\tools;.\venv\Scripts" />

    Then I opened the terminal and ran the kernel install steps once more:

    Once that was in place I could create a new notebook and run F# code:

    Issue 2. Notebooks weren't saving

    I was getting a little "Method Not Allowed" notification when I made some changes to my notebooks - so things like saving, renaming and deleting notebooks didn't work. The issue was that IIS was preventing HTTP verbs other than GET and POST (so PUT, PATCH and DELETE were getting blocked). The solution for me was to remove WebDAV handler and module - which again involved adding a couple of lines to my Web.config. It's maybe easier to just show the final Web.config to see everything together:

    <?xml version="1.0" encoding="utf-8"?>
          <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
          <remove name="WebDAV" />
          <remove name="WebDAVModule" />
        <aspNetCore processPath=".\venv\Scripts\python.exe" 
            <environmentVariable name="PYTHONPATH" value="." />
            <environmentVariable name="PATH" value="%PATH%;C:\Users\sean\.dotnet\tools;.\venv\Scripts" />
    So yet another hack - I'm starting to wonder if I'm using it in an unintended or unexpected way. I've put it all up on a git repo for anyone who wants to quickly get it up and running without thinking too hard.

    Jupyter Notebook - Running under IIS on Windows

    I use Jupyter Notebooks quite a lot for personal development, and to do this I set up an instance on a DigitalOcean droplet. However I also wanted to do something similar for some tasks at work which can access resources only available in our internal network. Since we use Windows that means I need to do something a little different. I could just use Anaconda to install and load it, but I find it a little clunky, I don't like how it tries to manage everything for you, I wanted a slightly nicer url than https://localhost:8000 and more importantly I wanted it to just be there without having to start anything up. So I set out to get Jupyter Notebook running on IIS using the ASP.NET Core Module V2 on http://notebook.local.

    Make sure Python, IIS and the ASP.NET Core Hosting Bundle are installed. Open up a Terminal (I'm using bash, Powershell is probably file but I don't like it), create an new directory where our notebook application will be served, setup a venv there, and run the activation script, upgrade pip so that it doesn't complain at us and then finally install Jupyter Notebook:

    $ cd ~/source/repos
    $ mkdir notebook
    $ python -m venv venv
    $ source venv/Scripts/activate
    $ pip install --upgrade pip
    $ pip install notebook
    To start with let's just run it on its own to check that it works:

    $ jupyter notebook
    [I 20:39:42.322 NotebookApp] The port 8888 is already in use, trying another port.
    [I 20:39:42.328 NotebookApp] Serving notebooks from local directory: C:\Users\sean\source\external
    [I 20:39:42.329 NotebookApp] Jupyter Notebook 6.1.4 is running at:
    [I 20:39:42.329 NotebookApp] http://localhost:8889/?token=e27aa8d7754e1bb078dfdf48e7c55032ac3551360389fd65
    [I 20:39:42.329 NotebookApp]  or
    [I 20:39:42.329 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
    [C 20:39:42.476 NotebookApp]
        To access the notebook, open this file in a browser:
        Or copy and paste one of these URLs:

    So here you can see why I'm doing all this - http://localhost:8888 isn't quite as nice as https://notebook.local. Next let's create a logs folder where our stdout logging will live and notebooks folder where we'll keep our notebook files.

    $ mkdir logs notebooks

    Next we'll create a Python script called launch.py that will launch Jupyter:

    import os, sys
    from IPython.terminal.ipapp import launch_new_instance
    from IPython.lib import passwd
    import warnings
    # set the password you want to use
    notebook_password = "test"
    sys.argv.append("--NotebookApp.port=" + os.environ["ASPNETCORE_PORT"])
    sys.argv.append("--NotebookApp.password=" + passwd(notebook_password))

    This script is kind of interesting, but we'll go into it later once we've got everything up and running. Next we will need to create a Web.config file that'll be used by IIS and ANCMV2 to call this script with the Python executable and libraries from our venv. So let's create the following:

    <?xml version="1.0" encoding="utf-8"?>
          <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
        <aspNetCore processPath=".\venv\Scripts\python.exe" 
            <environmentVariable name="PYTHONPATH" value="." />

    Next we'll set up the new Site in IIS - so open up IIS Manager (hit Start and start typing "Internet Information Services" - it'll appear in the suggestions) then:

    1. in the left-hand menu right click the top-level node, hit "Add Website..."
    2. in the dialog that appears call the site whatever you like
    3. in the "Physical Path" section click the "..." button, and choose the directory your notebooks live in
    4. in the "Binding" section type "notebook.local" in the "Host name" section

    Now nearly everything's in place we just need to modify our local DNS to resolve "notebook.local" to so add the following line to C:/Windows/System32/drivers/etc/hosts (you'll need to be an Administrator to do so):    notebook.local

    So now we have a Website bound to "notebook.local" running under IIS, which is configured to use ASP.NET Core Runtime Module V2 to launch a Python script that will run Jupyter Notebook. Let's fire up a browser and navigate to http://notebook.local and enter the password test:

    It loads nicely so let's just create a simple Python 3 notebook to confirm everything works end-to-end:

    Excellent. Ok so let's rewind a little bit and ask the question: why do we use a special script instead of having our Web.config file launch jupyter notebook directly? Well that would be ideal, but a bit of a hack required. Notice in launch.py that we grab the ASPNETCORE_PORT environment variable and use that as our port? Well that contains the port IIS wants to communicate with our app in - so we need to hook Jupyter up to listen on that port. While it is possible to call jupyter notebook --port 8080 to specify the port we want to use, it's actually not possible to use the ASPNETCORE_PORT variable in our Web.config. These environment variables are not expanded in our arguments="..." attribute, (see issue #117 in the aspnet/AspNetCoreModule repo on GitHub) - so we need to run a script that grabs this from the Environment and sets it up. It's a bit hacky, but it gets us where we need to be.

    setxkbmap - prevent layout amnesia

    I use both the US* and the Czech keyboard layouts on my home desktop, but I use dwm which is quite minimalist and doesn't come with any way to switch between these. Nor should it - dwm is a window manager for hackers, so I bodged my own solution that displays the currently selected layout in dmenu and added the following section to my ~/.xprofile that let's me cycle between them using right-Windows key:
    # enable US and CZ layouts, toggle with right-win, other possible grp:* values are...
    #    grp:alt_caps_toggle         Alt+Caps Lock
    #    grp:alt_space_toggle        Alt+Space
    #    grp:menu_toggle             Menu
    #    grp:lwin_toggle             Left Win
    #    grp:win_space_toggle        Win+Space
    #    grp:rwin_toggle             Right Win
    setxkbmap -layout us,cz                 
    setxkbmap -option                       
    setxkbmap -option "grp:rwin_toggle"

    However I noticed that periodically this would stop working, I'd be stuck on the US layout and my right-windows key would do nothing. Re-running the three setxkbmap commands would fix things up. 

    This eventually annoyed me and I resolved to figure it out. Apparently Linux has some power-management module that will cause some USB devices to disconnect, and when they reconnect they appear as a different device without these keyboard settings.

    Unsuprisingly my bodge was responsible for this. The correct solution here was to set my XKBLAYOUT to "uz,cz" and to add "grp:rwin_toggle" to my XKBOPTIONS in /etc/default/keyboard:

    $ cat /etc/default/keyboard 
    # Consult the keyboard(5) manual page.

    Now my layout switching works a little better and my keyboard no longer has bouts of amnesia. That BACKSPACE="guess" line sure does look odd though, I'll need to come back to that at some point.

    * = I have no need for the "pound" symbol anymore and US-layout keyboards are much more available

    No, THIS is What Peak Hello World Looks Like

    An article titled "This Is What Peak Hello World Looks Like" did the rounds on Hacker News (discussion here), where someone took the plain old printf("hello, world!")in C and transformed it step-by-step beyond recognition. This was a noble effort, but today I discovered an easier way to make Hello World more insane in a slightly different way:

        $ dotnet new console -o hello -lang F#
        The template "Console Application" was created successfully.
        Processing post-creation actions...
        Running 'dotnet restore' on hello/hello.fsproj...
          Restore completed in 209.4 ms for /home/sean/dev/dotnet/hello/hello.fsproj.
        Restore succeeded.
        $ cd hello
        $ cat Program.fs 
        // Learn more about F# at http://fsharp.org
        open System
        let main argv =
            printfn "Hello World from F#!"
            0 // return an integer exit code

    That's it, an absolute monster of a program. But wait, "This is just a plain old Hello World in .NET! What's so insane about that?" I hear you ask. Well we're not quite done yet, let's get ready to share the executable with our colleague who wants to use our app so we'll add <PublishSingleFile>true</PublishSingleFile> to hello.fsproj - which will generate a single self-contained executable binary - and publish it:

        $ dotnet publish -r linux-x64 -c Release
        Microsoft (R) Build Engine version 16.5.0+d4cbfca49 for .NET Core
        Copyright (C) Microsoft Corporation. All rights reserved.
          Restore completed in 5.27 sec for /home/sean/dev/dotnet/hello/hello.fsproj.
          hello -> /home/sean/dev/dotnet/hello/bin/Release/netcoreapp3.1/linux-x64/hello.dll
          hello -> /home/sean/dev/dotnet/hello/bin/Release/netcoreapp3.1/linux-x64/publish/
        $ ./bin/Release/netcoreapp3.1/linux-x64/publish/hello
        Hello World from F#!
        $ ls -lh ./bin/Release/netcoreapp3.1/linux-x64/publish/hello
    -rwxr-xr-x 1 sean sean 78M May 17 22:47 ./bin/Release/netcoreapp3.1/linux-x64/publish/hello

    There she blows! An absolute monster - a 78MB Hello World executable! This is actually a bit unfair, since it includes some stuff we might not need, so let's add <PublishTrimmed>true</PublishTrimmed> to hello.fsproj and rebuild:

        $ dotnet publish -r linux-x64 -c Release
        Microsoft (R) Build Engine version 16.5.0+d4cbfca49 for .NET Core
        Copyright (C) Microsoft Corporation. All rights reserved.
          Restore completed in 33.8 ms for /home/sean/dev/dotnet/hello/hello.fsproj.
          hello -> /home/sean/dev/dotnet/hello/bin/Release/netcoreapp3.1/linux-x64/hello.dll
          Optimizing assemblies for size, which may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
          hello -> /home/sean/dev/dotnet/hello/bin/Release/netcoreapp3.1/linux-x64/publish/
        $ ./bin/Release/netcoreapp3.1/linux-x64/publish/hello
        Hello World from F#!
        $ ls -lh ./bin/Release/netcoreapp3.1/linux-x64/publish/hello
    -rwxr-xr-x 1 sean sean 46M May 17 22:51 ./bin/Release/netcoreapp3.1/linux-x64/publish/hello

    Ok, slightly better but that's still a hefty 46MB for Hello World. This is a bit disingenuous however - what's happening is that I've configured the project to be able to produce a self-contained executable, to do this the .NET Core runtime is bundled in the hello binary. So it's a little bit like distributing a little bit of application [byte]code, a bytecode interpreter, a JIT compiler and runtime libraries.

    Jupyter - problems with dotnet-try

    Since .NET Try supports F# officially I wanted to switch over my Jupyter notebook instance to use that instead of IFsharp. But I ran into a couple of frustrating issues that I wanted to document in case anyone else hit them and didn't know how to debug the issue.

    Issue 1. dotnet-try installs but isn't usable

    I followed Scott Hanselman's instructions to install dotnet-try, but when I tried to execute dotnet try jupyter install it seemed as though dotnet-try wasn't installed at all:

    $ dotnet tool install dotnet-try --global
    You can invoke the tool using the following command: dotnet-try
    Tool 'dotnet-try' (version '1.0.19553.4') was successfully installed.

    $ dotnet try jupyter install Could not execute because the specified command or file was not found. Possible reasons for this include: * You misspelled a built-in dotnet command. * You intended to execute a .NET Core program, but dotnet-try does not exist. * You intended to run a global tool, but a dotnet-prefixed executable with this name could not be found on the PATH.

    After a lot of head scratching I dug into the actual docs and learned that both .NET Core 3.0 and .NET Core 2.1 SDKs should be installed, while I only had .NET Core 3.1's SDK So after a quick sudo apt install dotnet-sdk-3.0 dotnet-sdk-2.1 I was successfully able to install the kernel and list it:

    $ jupyter kernelspec list
    Available kernels:
      .net-csharp    /home/notebook/.local/share/jupyter/kernels/.net-csharp
      .net-fsharp    /home/notebook/.local/share/jupyter/kernels/.net-fsharp
      mit-scheme     /usr/local/share/jupyter/kernels/mit-scheme
      python3        /usr/local/share/jupyter/kernels/python3

    Issue 2. Jupyter can't run dotnet-try

    However even though it was installed, each time I tried to create a new F# notebook Jupyter would give an error saying that it was unable to connect to the kernel. After taking a quick look at my logs I saw the same error as before!

    Feb 09 13:19:11 aviemore jupyter[837]: [I 13:19:11.169 NotebookApp] KernelRestarter: restarting kernel (1/5), new random ports
    Feb 09 13:19:11 aviemore jupyter[837]: Could not execute because the specified command or file was not found.
    Feb 09 13:19:11 aviemore jupyter[837]: Possible reasons for this include:
    Feb 09 13:19:11 aviemore jupyter[837]: * You misspelled a built-in dotnet command.
    Feb 09 13:19:11 aviemore jupyter[837]: * You intended to execute a .NET Core program, but dotnet-try does not exist.
    Feb 09 13:19:11 aviemore jupyter[837]: * You intended to run a global tool, but a dotnet-prefixed executable with this name could not be found on the PATH.
    Feb 09 13:19:14 aviemore jupyter[837]: [I 13:19:14.180 NotebookApp] KernelRestarter: restarting kernel (2/5), new random ports
    Feb 09 13:19:14 aviemore jupyter[837]: Could not execute because the specified command or file was not found.
    Feb 09 13:19:14 aviemore jupyter[837]: Possible reasons for this include:
    Feb 09 13:19:14 aviemore jupyter[837]: * You misspelled a built-in dotnet command.
    Feb 09 13:19:14 aviemore jupyter[837]: * You intended to execute a .NET Core program, but dotnet-try does not exist.
    Feb 09 13:19:14 aviemore jupyter[837]: * You intended to run a global tool, but a dotnet-prefixed executable with this name could not be found on the PATH. 
    I restarted the service, the server, checked and it took a while to realise the root cause. What happened was, even though dotnet-try was in PATH when I switched over to my jupyter use, it wasn't the case when I ran it via systemd. It seems that /etc/profile - which runs all the scripts in /etc/profile.d, one of which adds the ~/.dotnet/tools dir to PATH - is not used when systemd starts the service. I don't know the correct way to address this, but I wrote a quick script to setup PATH correctly and added an EnvironmentFile to the [Service] section my notebook.service file to use it:
    After I set this up and restarted notebook.service it was able to access dotnet-try and spin up the F# kernel correctly.

    Fun with manpages

    When reading about a Unix command or C library function it's relatively common to see it suffixed with a number in brackets. This is to make it clear what exactly you're talking about, so if someone is discussing mknod they might write mknod(1) they're talking about the shell command or mknod(2) if they mean the syscall. The number used refers to the manpage section, so to see the manpage for the shell function:

    $ man 1 mknod

    And to see what the syscall is all about:

    $ man 2 mknod

    According the man manpage on my system there are eight standard manpage sections in total, with one non-standard section:

    1. Executable programs or shell commands
    2. System calls (functions provided by the kernel)
    3. Library calls (functions within program libraries)
    4. Special files (usually found in /dev)
    5. File formats and conventions eg /etc/passwd
    6. Games
    7. Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7)
    8. System administration commands (usually only for root)
    9. Kernel routines [Non standard]
    Since something can be present in more than one section, I wondered which symbol had the most manpages so I wrote a script to look through each of the directories in /usr/share/man/man[1-8], list and parse the gzipped filenames (they're usually named symbol.sectionnumber.gz) and then find out the sections they're all present in:
    import os
    import re
    from collections import defaultdict
    manpage_gz_pattern = "(.*)\.\w+.gz"
    manpage_dir_base = "/usr/share/man"
    manpage_sections = range(1, 9)
    manpage_entries = defaultdict(list)
    for manpage_section in manpage_sections:
        manpage_section_dir = os.path.join(manpage_dir_base, f"man{str(manpage_section)}")
        manpage_section_contents = os.listdir(manpage_section_dir)
        for manpage_entry_filename in manpage_section_contents:
            gz_entry = re.match(manpage_gz_pattern, manpage_entry_filename)
            manpage_entry = gz_entry.groups()[0] 
            manpage_entries[manpage_entry] += [(manpage_section, manpage_entry_filename)]
    for section_count in manpage_sections:
        number_of_manpages = len([ m for m in manpage_entries if len(manpage_entries[m]) == section_count])
        print(f"number of manpages in {section_count} sections: {number_of_manpages}")

    The results are:
    $ python -i mancount.py
    number of manpages in 1 sections: 10763
    number of manpages in 2 sections: 107
    number of manpages in 3 sections: 7
    number of manpages in 4 sections: 0
    number of manpages in 5 sections: 0
    number of manpages in 6 sections: 0
    number of manpages in 7 sections: 0
    number of manpages in 8 sections: 1
    There's seemingly a clear winner, you can find a single symbol in all eight standard manpage sections. However this is a little misleading because after a bit of inspection this symbol is "intro" - it is not a shell command, syscall, stdlib function, game or anything like that - it's a manpage that describes a bit about each section.

    So ignoring intro the most common symbols and their manpage entries are
    • mdoc (mdoc.1.gz, mdoc.5.gz, mdoc.7.gz)
    • locale (locale.1.gz, locale.5.gz, locale.7.gz)
    • hostname (hostname.1.gz, hostname.5.gz, hostname.7.gz)
    • passwd (passwd.1ssl.gz, passwd.1.gz, passwd.5.gz)
    • time (time.2.gz, time.3am.gz, time.7.gz)
    • readdir (readdir.2.gz, readdir.3am.gz, readdir.3.gz)
    • random (random.3.gz, random.4.gz, random.7.gz)
    This reveals something else interesting - the section needn't be a number. The two commands are both valid and access completely separate manpages:

    $ man 1ssl passwd
    $ man 1 passwd

    I used to think that each time I opened a manpage I learn something completely new and unexpected - but I never thought I'd find something interesting just by looking at the manpages' gzipped filenames!

    F# - Polymorphic parameter overflow

    I was mucking around in F# over lunch and started thinking about how it assigns names to type variables (see "Automatic Generalization" of the Type Inference page of the F# docs for more info). By way of introduction, let's create a function foo that takes a single parameter without an explicitly defined type and see what its signature looks like:
    > let foo x = x;;
    val foo : x:'a -> 'a
    So the parameter x is assigned the type variable 'a - which makes sense, the first unknown type gets named after the first letter of the alphabet. And of course it follows that if we have a function with two parameters the second type is called ...
    > let foo x y = (x,y);;
    val foo : x:'a -> y:'b -> 'a * 'b
    ... 'b! Ok now what happens when we've got a function with a ridiculous amount of parameters? We'll run out of lower-case letters eventually. When that happens do start using upper-case letters? Non-ASCII? Something else? I quickly hacked together a python script to generate functions with arbitrary parameters, created one with 40 and pasted it into the F# REPL and ...
    > let foo x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12 x13 x14 x15 x16 x17 x18 x19 x20 x21 x22 x23 x24 x25 x26 x27 x28 x29 x30 x31 x32 x33 x34 x35 x36 x37 x38 x39 = (x0,x1,x2,x- 3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20,x21,x22,x23,x24,x25,x26,x27,x28,x29,x30,x31,x32,x33,x34,x35,x36,x37,x38,x39);;
    val foo :
      x0:'a ->
        x1:'b ->
          x2:'c ->
            x3:'d ->
              x4:'e ->
                x5:'f ->
                  x6:'g ->
                    x7:'h ->
                      x8:'i ->
                        x9:'j ->
                          x10:'k ->
                            x11:'l ->
                              x12:'m ->
                                x13:'n ->
                                  x14:'o ->
                                    x15:'p ->
                                      x16:'q ->
                                        x17:'r ->
                                          x18:'s ->
                                            x19:'t ->
                                              x20:'a1 ->
                                                x21:'a2 ->
                                                  x22:'a3 ->
                                                    x23:'a4 ->
                                                      x24:'a5 ->
                                                        x25:'a6 ->
                                                          x26:'a7 ->
                                                            x27:'a8 ->
                                                              x28:'a9 ->
                                                                x29:'a10 ->
                                                                  x30:'a11 ->
                                                                    x31:'a12 ->
                                                                      x32:'a13 ->
                                                                        x33:'a14 ->
                                                                          x34:'a15 ->
                                                                            x35:'a16 ->
                                                                              x36:'a17 ->
                                                                                x37:'a18 ->
                                                                                  x38:'a19 ->
                                                                                    x39:'a20 ->
                                                                                      'a *
                                                                                      'b *
                                                                                      'c *
                                                                                      'd *
                                                                                      'e *
                                                                                      'f *
                                                                                      'g *
                                                                                      'h *
                                                                                      'i *
                                                                                      'j *
                                                                                      'k *
                                                                                      'l *
                                                                                      'm *
                                                                                      'n *
                                                                                      'o *
                                                                                      'p *
                                                                                      'q *
                                                                                      'r *
                                                                                      's *
                                                                                      't *
                                                                                      'a1 *
                                                                                      'a2 *
                                                                                      'a3 *
                                                                                      'a4 *
                                                                                      'a5 *
                                                                                      'a6 *
                                                                                      'a7 *
                                                                                      'a8 *
                                                                                      'a9 *
                                                                                      'a10 *
                                                                                      'a11 *
                                                                                      'a12 *
                                                                                      'a13 *
                                                                                      'a14 *
                                                                                      'a15 *
                                                                                      'a16 *
                                                                                      'a17 *
                                                                                      'a18 *
                                                                                      'a19 *
    So 'a up to 't ... but then numbered 'as after that. I've no idea why it stops at 't, then just counts up from 'a. Interestingly this is the case with F# under Mono on Linux and under the official MS runtime on VS2017 on Windows. OCaml, which F# is heavily influenced by, does not exhibit this behaviour - it refers to the unknown types as a..z, a1..z1, a2...z2, etc.

    That's reasonbaly interesting and all, but since I had already written a script to generate these F# files I figured I'd keep trying with more and more parameters to see what happens. After adding a hundred parameters at a time I soon hit an interesting warning between 400 and 500 params:

    Warning: line too long, ignoring some characters
    It turned out we hit some line-length restriction in either the parser or the readline equivalent that fsi.exe uses. A bit of experimentation later I found that when the function had 429 parameters it parsed just fine ... but 430 parameters caused the warning.

    429 parameters: 4093 characters
    430 parameters: 4104 characters

    Minor sidebar - I was going to just say we hit a restriction where 4096 bytes is the max line length, but obviously 1 character isn't necessarily 1 byte if we're using some Unicode representation, and a nice way to check that we're using Unicode is to punch in identifiers that would definitely be in different encodings in an ASCII representation - I used Georgian and Czech characters:
    > let სამი = 3;;
    val სამი : int = 3
    > let čtyři = 4;;
    val čtyři : int = 4
    > სამი + čtyři;;
    val it : int = 7
    When I changed my program to start each parameter with the Georgian character "ა" (i.e. let bar ა0 ა1... etc) which cannot be represented in a single byte in UTF-8 we hit the error after only 162 parameters. So it's not a character limit, but a buffer of bytes that we filled - and the size of the buffer is ... uh 4096 bytes.

    Since I wanted to pull this thread until I hit some logical conclusion I tweaked my program to split the insanely long lines so they wouldn't hit this error and continued to add more parameters. The next issue I encountered was after trying a function with a couple thousand parameters, where I encountered the following error:
    /home/sean/stdin(6636,1): error FS0039: The value or constructor 'x1328' is not defined. Maybe you want one of the following:
    So it seems we've bumped up into another limit - somehow 1327 parameters is ok but F# loses track of the 1328th one, thinking it doesn't exist. Is this a bug? Probably, there should maybe be a more helpful error message. Is it important? Probably not, if your code contains a function with upwards of a thousand parameters then this is the least of your problems.

    Debian - Building XMMS 1.2.11 on a modern linux system

    Like most people I've recently been consuming all my media via paid streaming services like Netflix and iTunes. The other day however I needed to play an MP3 on my laptop running Debian and instinctively wanted to reach for xmms. Sadly nowadays the original xmms isn't available on Debian, only an "xmms2" package which is much newer and was reworked into some client/server model. I don't really want to figure out how to configure this correctly, to the extent that I was willing to build the original xmms from source ...

    Trying the naive "./configure && make && sudo make install" method doesn't go very well when running Debian stretch:
    sean@seoul ~/d/s/xmms-1.2.11> ./configure
    checking build system type... x86_64-unknown-linux-gnu
    *** The glib-config script installed by GLIB could not be found
    *** If GLIB was installed in PREFIX, make sure PREFIX/bin is in
    *** your path, or set the GLIB_CONFIG environment variable to the
    *** full path to glib-config.
    configure: error: *** GLIB >= 1.2.2 not installed - please install first ***
    As it turns out I wasn't able to find a pre-built version of GLIB 1.x or (a subsequent dependency) GTK 1.x, I found some sources (GLIB 1.2 and GTK+ 1.2) but these were hitting an error when running ./configure which indicated that the CPU wasn't supported. These libraries pre-date the x86-64 era so my processor wasn't recognised. The fix was to simply drop in a newer config.sub. There was one more issue with the G_GNUC_PRETTY_FUNCTION macro but I resolved that too - I put them onto GitHub as glib-1.x and gtk-1.x in case anyone else wants to use this. Installing them is easy:
    $ git clone https://github.com/smcl/gtk-1.x
    $ cd gtk-1.x
    $ ./configure --prefix=/usr && make
    $ sudo make install
    $ git clone https://github.com/smcl/glib-1.x
    $ cd glib-1.x
    $ ./configure --prefix=/usr && make
    $ sudo make install
    Once these are in place we can grab the "latest" old XMMS sources from xmms.org and build those:
    $ curl -LO http://www.xmms.org/files/1.2.x/xmms-1.2.11.tar.gz
    $ tar -xzf xmms-1.2.11.tar.gz
    $ cd xmms-1.2.11
    $ ./configure && make
    $ sudo make install

    Then if all is well then the original (and best!) xmms should be installed into your path, so you can go download some lovely skin of a brushed aluminium late-90s Sony CD player ... though it might be a little bit tiny if you use a HiDPI screen:

    Czech - Cases are hard

    Grammatical cases are a common stumbling block for native English speakers learning another language. This might be because cases are sort of invisible in English and they're not taught in school so it's it's hard to see why they would even matter.

    However in languages like Czech cases are a really important concept, and if you don't wrap your head around them you'll struggle to make yourself understood. To fully comprehend this importance we need an example - Petr and Pavel, who are not friends.

    Accusative - Peter is punching Pavel

    In Czech the verb "to punch" is "bít", whose third person conjugation is "bije" so if we want to describe this situation in Czech we might naively start out with something like this ...

    Petr bije Pavel

    However this isn't quite enough because it's not actually clear who is punching who. You should be able to rearrange the parts of this sentence in many ways in Czech without any ambiguity. But if we can rearrange the sentence as Pavel bije Petr or bije Pavel Petr then how do we tell our Czech friends who is the puncher and who is the punchee? This is where we need to modify case of some words to help clear things up a little.

    In Czech we indicate the subject (puncher) using the nominative case. In our sentence this is Petr and the nominative case of Petr is simply Petr.

    The object (the punchee) of the sentence is indicated using accusative case - which for Pavel is Pavla. Which gives us:

    Petr bije Pavla

    So hopefully you can see why exactly cases are so important, if you don't learn them you're going to confuse a lot of Czech people and have a lot of frustrating conversations. Let's take Petr and Pavel and explore some other cases.

    Genitive - Petr reads Helena's book

    If Petr is finished punching Pavel and just wants chill with his friend Helena's book, we need to use the Genitive case to indicate it belongs to her:

    Petr čte knihu Heleny

    Subject = nominative of Petr = Petr

    Verb = third person singular conjugation of čist = čtu

    Object = kniha in accusative case = knihu

    Possessor = Helena in genitive case = Heleny

    Dative - Petr reads to Pavel

    If he decides to read the book to Pavel in a curious attempt at reconciliation, we need to use the Dative case:

    Petr čte knihu Pavlovi

    Receiver = Pavel in dative case = Pavlovi

    Instrumental - Petr reads with Pavel

    If this reconciliation is successful and Petr reads the book with Pavel we need to use the Instrumental case:

    Petr čte knihu s Pavlem

    Instrument = Pavel in instrumental case = Pavlem

    Locative - Petr reads about Pavel

    Maybe it's weird to describe Pavel as the "instrument", but just go with it because in the next sentence Petr is reading a book about Pavel and in this situation we use the Locative case:

    Petr čte knihu o Pavlovi

    preposition "o" (meaning "about") requires locative case of Pavel = Pavlovi

    Vocative - Petr says goodbye to Pavel

    Finally, to draw this ridiculous situation to a close Petr says goodbye to Pavel where we use the Locative case:

    Na shledanou Pavle!

    addressing Pavel requires vocative case of Pavel = Pavle!


    This is only a quick-n-dirty summary restricted to the seven Czech cases, but it should indicate why each case is important. The Ústav pro jazyk český Akademie věd ČR (Language Institute of the Czech Academy of Science) have a really useful page if you want to see what a word looks like in different cases: http://prirucka.ujc.cas.cz. Just enter your word into the "Slovníková část" section and hit "Hledej"