Perl, Haskell, stuff
So, I got a Macbook Pro from $new_company (about which more later) and, as I believe is traditional, here are some notes about the experience.
Very nice: no rebooting, things just work from more or less the moment you take it out of the box. (Compare with recent Samsung N140 netbook, whose Windows 7 install wanted several reboots to install all the additional antivirus and other crap it comes bundled with.)
My parents, brother, gf, and her mother all have macbooks. And you have to admit that out of the box they come with a rather well thought out set of applications for real people. But of course everyone has different needs, and I immediately installed the following:
(Some are payware, eek! I'm trying them out for 30-day trials, and may get some through work, others I might be persuaded to pay myself, but lets see)
Then some file storage apps:
And some toys for the menu bar:
And miscellaneous bits and bobs:
I've not had time to play with all of these, but the overall quality is pretty good. Installation isn't too bad, though I don't think it's intuitive to "just drag the icon to the applications folder". You can tell it's not intuitive by the fact that every .dmg has a different background image with a large arrow graphic trying to point it. Or by the fact that my parents launch the one or two applications they've downloaded by opening them from a .dmg that they have permanently mounted... sigh...
Ah, there's even an article about this.
But OK, once you know how to do it, it's easy enough. Though I'd like apt, and should look at MacPorts. I did install brew which is nice for command-line apps.
Hardware is nice. Mostly. It doesn't run too hot. I like the lighting on the keyboard. Actually the keyboard is much nicer than I'd expected (One of the few really excellent things about the thinkpad was its keyboard, so I wasn't really expecting all that much from these odd scrabble-keys, but I'll talk about that in more depth shortly).
The new Mac trackpad is OK. I think I'd rather have a button - it's not as if you can click anywhere on the trackpad to click, it has to be near the bottom, so why not just have a button? On the other hand, tap-to-click works fine. Not having physical buttons means that there isn't a middle-mouse button. Which is inconvenient in an ubuntu VM, as it means there's no obvious way to do copy-paste in an xterm... I worried about this for a while, then came up with the obvious workaround instead -- use gnome-terminal... (who needs unicode, colours, or stability anyway?)
The trackpad seems insensitive too for "click; move; click" sequences. OK, this is mainly a problem for FreeCell and DiceWars, but annoying in any case.
The screen is nice too. Oh, the screen: this was largely why I had a sudden wild shift from a 12" supposedly "ultraportable" thinkpad to a 17" MBP... I decided that I couldn't see enough on a small screen. OK, so the 17" is massive... embarrassingly so when you take it out on a train for example, but it does make for a great machine to work on.
There are a couple of things I'm not so pleased with though: though the machine doesn't run hot (like my thinkpad) it does tingle in a semi-electric-shock way when the power adapter is plugged in (a quick google suggests this is a fairly common problem). Also the headphones, while loud and crisp, seem quite noisy. When you have the phones plugged in, every time a noise occurs, the headphone socket then hisses for half a minute, then switches itself off again.
The most annoying thing about the keyboard is the odd positioning of the keys. The # key is hidden away in Alt-3 (but without anything written on the keyboard to hint that). Oddly under my ubuntu VM (with keyboard set to Apple MBP) it insists on RightAlt-3. PgUp/PgDn are Fn-Up/Fn-Dn, which is reasonable but unexpected. Also, there's only one delete key, which behaves badly in Terminal.app until you set the appropriate options correctly (why isn't that the default?). Yet, there are some oddities taking up precious keyboard real estate:
Also, instead of Ctrl being at the very bottom-left (a lovely position, which means you can press Ctrl without using a finger -- I use the pad of my left hand) the "Fn" key is there instead. The Thinkpad shares this idiocy. I've recently been using a 5-year-old HP laptop as a backup, and despite it being underpowered and falling apart, the 2 things I really liked, after using a thinkpad, were a) the Ctrl key being in the right place, and b) that it had a trackpad. (The Thinkpad just had a clitmouse. After 2 years I learnt how to use it, but still found it hateful. Never again. But I digress.)
Another oddity is the media keys. (Where by "oddity", I'm struggling for a politer word than "batshit crazy clusterfuck").
I never really got media keys working satisfactorily under Linux, where they mainly just spoke to the foreground application. So I was hoping that a bit of Apple shiny would go a long way. Now I'm not a usability expert, but I'm hoping that my expectation for what a Play/Pause button should do might not be entirely insane or impractical:
What actually happens is this:
So, if you have VLC open playing a video, and somebody calls you, you press the Pause button. Then:
But that's OK. You can press the Pause button to stop the chaos right?
Apparently this is breakage from 10.6 Snow Leopard, having been sane till 10.5. There is speculation that Apple are trying to make 3rd party apps look bad by not responding "properly" to the media buttons, while Apple's own apps do. If that's true, then it's not working. I don't blame VLC. I don't blame Miro. Dear Apple, you fucked up, please sort it out.
(This isn't just me: various threads including http://discussions.apple.com/thread.jspa?threadID=2122639 are whining about this idiocy. The thread mentions a cunning back: if you have QuickTime open, it acts as a prophylactic against iTunes opening. This is apparently because the 'loginwindow' process has an exception hardcoded into it not to launch iTunes if it detects QT running. So not in the slightest bit insane then.)
I had the same issue with the remote. Yet it's shiny, but if it only wants to play with iTunes and the useless Front Row (where by useless, I mean doesn't play my videos, which is what I expected it to) then it's basically a shiny paperweight. Thanks Apple. I've installed Candelair, which may possibly help improve this but I've been disinclined to test properly.
When the machine resumes from sleep, the external monitor shows static "snow" for half a minute or so. That's not particularly impressive. A quick google suggests that older models had similar problems, perhaps this resurfaces every now and then? I should probably check to see if it's going to be a deteriorating hardware issue.
On the other hand, it actually resumes. From sleep-to-ram. And hibernate-to-disk. Every time. Yes, every time!
(Can you tell I've been using Linux-on-laptop for the last 5 years?)
And though I don't think I'm getting anywhere near the 8 hours advertised, I am to be fair running with a bright screen, a couple of VMs and so on.
This is a mixed bag. Mainly, the windows are less useful/pretty than Gnome: you can't set windows to Keep-on-top. You can only resize using the bottom-right control, no Alt-to-drag option. No ability to maximize a window (ok, so the need for this doesn't come up as often as I'd expected, but when it does, it's annoying), and new windows don't get tiled in pretty places as with gnome. Also, more often than I remember happening under Linux, popup windows get placed underneath the windows that created them. Meh.
I'm not sure whether I like Finder better than Nautilus. It seems to be possible to use drag-to-copy into a folder of folders (rather than into one of the subfolders). On the other hand "type-to-jump-to-filename" seems to not work consistently (I haven't quite figured which state is inhibiting this).
When you're using 2 monitors, Mac's single menu bar (on the top of the screen instead of top of window) becomes a royal pain, as you have to move the pointer from the external screen back to the laptop's one to use it. I guess I should learn the command keys to do this without mouse. Also, if you put the second screen immediately above the first, you suddenly change the menu bar from being "a mile high" to being a thin strip of stuff that you have to target. So don't do that (or learn those key combinations).
Spaces... wasn't sure how to move apps between spaces. But the F8 view is quite cute when you work out what it is (I was confused by it showing me what looked like a 4x2 view instead of a 2x2 one... but of course I have external monitor plugged in... d'oh.) Very cute is that Expose works while you're in the thumbnail view.
Date/Time applet in menubar is underpowered. Compare to the Gnome one that has a month calendar view, world date-time and weather. This one can't even be configured to show the date in the menu bar. It's a prime candidate for replacement with something actually useful.
The only thing I can think of that might explain the weak Date/Time applet is that they are trying to sell you on the Dashboard one? If so, it doesn't work. The Dashboard is a wonderful combination of ugly, confusing, and useless. Every time I opened it (by accident, mostly) I would struggle to either get it to do anything useful or, even better, close again. Luckily there is a 'defaults' setting that prevents it from opening again. Phew. Bizarre.
For dev work, I was planning to use VirtualBox, which has the nice feature of being free. It has a few problems though:
VMWare solves these, though, oddly, it actually seemed slightly harder to setup. Installing the "guest additions" had to download a large package, and instead of running it itself expected me to read the system documentation to know what to do with it (I just ran the perl script from terminal, which works fine... Not sure why the README file couldn't have just said that). Still, minimizing friction between guest and host working is probably worth paying for.
There are some niggles, and some outright baffling brokenness, but actually I'm quite enjoying working on a Mac.
I've been prototyping ways for customers to import data into our system. While some kind of JSON/XML structured data would be easiest for us, our customers like spreadsheets. That's cool of course, and it's easy to mock up imports like this.
| Product | Price | Category |
|---|---|---|
| Brie | £ 2.00 | Dairy |
| Chablis | £ 5.00 | Wine |
Of course not all data fits nicely with this kind of very flat structure. For example multiple values. We could do something like
| Product | Price | Tags |
|---|---|---|
| Brie | £ 2.00 | Dairy,Cheese,Food |
| Chablis | £ 5.00 | Wine,Alcohol,Drink |
but it might be nicer for the customer to organize like this instead:
| Product | Price | Tags |
|---|---|---|
| Brie | £ 2.00 | Dairy |
| Cheese | ||
| Food | ||
| Chablis | £ 5.00 | Wine |
| Alcohol | ||
| Drink |
The problem is that we can't really parse the table row by row, simply.
We have to remember where the definition started, and finalise when the
item has been completely defined (which will happen when the next item
starts).
I started prototyping this in Perl, and ended up with some typical imperative
code like this:
my (@products, $product);
for my $r ( @rows ) {
my $row = get_row_data($r);
if ($row->has_product) {
push @products, $product if $product;
$product = Product->new(
product => $row->product,
price => $row->price
);
} else {
die "No product" unless $product;
}
$product->add_tag($row->tag);
}
push @products, $product;
This is quite yucky. See how I'm repeating the push @products line: first time when seeing a new product, and second when we've exited the loop. Then we have the multiple assignments to $product and so on.
All of which seems to work OK, but I really dislike this logic, and am beginning to think it's a perfect example of an imperative antipattern. So, let's see if we can do it more elegantly. Thinking about how I'd do it functionally, in Haskell, I'd use groupBy on each row, connecting the rows up until the next row has a non-blank product.
So let's try that in Perl! We'd want something like:
my @items = groupBy sub {
my (undef, $next_row) = @_;
! $next_row->has_product
},
map get_row_data->($_),
@rows;
my @products = map {
my $row_1 = $_->[0];
Product->new(
product => $row_1->product,
price => $row_1->price,
tags => [ map $_->tag, @$_ ],
);
}
@items;
That has less repetition, and less accidental complexity. First we group the lines. Then we turn the groups of related rows into new objects. Job done.
The groupBy function is interesting: notice how the callback to it takes 2 arguments ($this_item, $next_item), though as we don't care about the current row, only the next one, we're just doing (undef, $next_row).
groupBy is also problematic though, in that it doesn't exist in any of
the normal places I'd have expected (List::Util, List::MoreUtils etc.). So
let's create it. The ideal way might be to translate Haskell's definition from
the Prelude, but given that Perlish lists don't have lazy semantics, and we
then have to implement span etc., let's just write a noddy imperative
version for now:
sub groupBy {
my ($fn, @elems) = @_;
my @groups;
my $a = shift @elems;
my $b;
my @group = ($a);
while ($b = shift @elems) {
if ($fn->($a, $b)) {
push @group, $b;
} else {
push @groups, [@group];
@group = ($b);
}
$a = $b;
}
push @groups, [@group];
return @groups;
}
This of course has much of the unpleasantness I was complaining about before, but at least it's encapsulated, and allows us to use groupBy neatly. (And we can always come back and clean up the internals later).
(And yes, I know I could define groupBy (&@) so that I could omit the 'sub' around the block like with map/grep. But this is more annoying than useful, as I can't then simply call: groupBy \&function.)
How would you tackle this task? Is there even an elegant way to do it imperatively?
Of course, there is some silliness there too: you can bash Perl for many reasons (and if you've read my blog before, you might know that I do too), but there are some gems of forced interpretation:
C and Perl projects show a marked decline in activity over their first year. I suspect that the Perl result is due to the fact that it becomes harder and harder to contribute to a Perl codebase, the bigger it gets. The C result is more of a mystery.I'm not sure that the premise is true -- perhaps Perl projects are more limited in scope, for example. And Modern Perl is a quite different beast from the Matt's PERL script archive of yesteryear. But the punchline is priceless ;-)
Here's what I read with my biased interpretation of his results ;-)
Though I like the offbeat parallel reality more when it's low key (the series finales are more high-fantasy end-of-the-world stuff) the story arc is shaping up quite interestingly, as the whole premise of the scenario is put into question (hopefully this won't affect the surreal punning).
You can catch up on iPlayer — though, bizarrely, they haven't stacked previous episodes, so you'd only catch the series 1 ending now, bad BBC! And while I'm knocking Auntie, why don't the microsite or the iPlayer programme information give any details on the music used? The manic choral theme is fantastic, and the incidental music is also excellent. Any clues?
A couple of comments on the first post suggested that I look into the command-line bookkeeping application ledger, or indeed, its Haskell version hledger. They look very interesting, but rather hard to wrap my head around. So though I'm going to bear them in mind for later, I'll carry on doing these sketches till I understand the problem space better, at which point, perhaps I'll either a) steal some ideas from them, or b) realise they are undoubtedly the way forward and start using them.
I used Module::Starter to retrospectively turn this project into something I can release as a distribution to CPAN, with docs, tests, a Makefile, and so on.
module-starter --module=Beans --mi --author=osfameron --email=osfameron@cpan.org
This also creates some skeleton docs, so I've gone in to add a few actual notes (mainly just pointing at this blog), and to delete a few sections that module-starter puts in by default:
Then I added some tests in t/01-basic.t, for example:
my $item = Beans::Item->new(
name => 'Mortgage',
value => 500.00,
due_date => '2009-10-01',
comment => 'Home sweet home',
tags => [qw/ mortgage foo bar /],
);
ok $item, 'Object created successfully';
is $item->name, 'Mortgage', 'Name ok';
is $item->value, 500, 'Value ok';
is $item->due_date->month, 10, 'Date ok';
All very noddy stuff, but it helped me find a bug in the version I'd blogged earlier! I hadn't told the date fields to use the coercion I'd set, so the above code failed, complaining quite rightly that '2009-10-01' isn't a DateTime object!
So I amended the date fields like so:
has due_date => ( isa => DateTime,
is => 'rw',
required => 1,
coerce => 1,
);
and all was again well. Yay for failing tests! This brings me to a suggestion from John Napiorkowski to use MooseX::Types::DateTimeX to get my coercion for free. This does indeed work, and I've changed the code to use it, though it doesn't by default use DateTime::Format::Natural — we'll come back to this later.
While we're looking at types, let's fix the crufty implementation of tags. We've currently got an ArrayRef[NonEmptyStr], but really, we don't want an Array, because we want to be able to:
This changes our code to:
has tags => (
isa => 'Set::Object',
is => 'rw',
accessor => '_tags',
coerce => 1, # also accept array refs
handles => {
tags => 'members', # random order
add_tag => 'insert',
remove_tag => 'remove',
has_tag => 'member'
},
);
which we can test like so:
is_deeply [ sort $item->tags],
[qw/ bar foo mortgage /], 'Tags ok'
or diag Dumper($item->tags);
ok $item->has_tag('foo'), 'Has tag foo';
ok $item->has_tag('bar'), 'Has tag bar';
ok $item->has_tag('mortgage'), 'Has tag mortgage';
ok ! $item->has_tag('baz'), 'Nonexistant tag baz';
$item->add_tag('baz');
ok $item->has_tag('baz'), 'Now has baz';
$item->remove_tag('foo');
ok ! $item->has_tag('foo'), 'Now lost tag foo';
Set::Object's members function returns the contents in hashed order (i.e., effectively random), but given that we know our "objects" are actually strings, I'd prefer them in sorted order, which would simplify the is_deeply test above. We could do this as a method instead, or possibly use around to sort the results, but we'll come back to this soon!
lilac pointed out that I could create an instance of Random for Coins, I'll have a look at that soon.
import System.Random
import Control.Monad
import System.IO
import Data.Maybe
import Data.List
data Coin = Head | Tail
deriving (Eq, Ord, Show)
-- Derren Brown's coin game.
-- The second winner has chosen a combination that will win
-- significantly more often
main = do
g1 <- guess
let g2 = counter g1
putStrLn $ "Player 1 chooses " ++ (show g1)
putStrLn $ "Player 2 chooses " ++ (show g2)
coins <- coinFlips
let winner = take 1 . catMaybes .
map (win g1 g2) $
tails coins
putStrLn $ "Player " ++ (show winner) ++ " wins!"
guess = do f <- coinFlips
return $ take 3 f
counter [a,b,_] = [rev b, a, b]
where rev Head = Tail
rev Tail = Head
win g1 g2 l | g1 `isPrefixOf` l = Just 1
| g2 `isPrefixOf` l = Just 2
| otherwise = Nothing
-- modified from
-- http://www.haskell.org/pipermail/haskell-cafe/2005-April/009687.html
coinFlips :: IO [Coin]
coinFlips = do g <- newStdGen
let bools = randoms g
let coins = map bool2coin bools
return coins
where bool2coin True = Head
bool2coin False = TailI've recently been trying to improve my rather disorganized personal finances. While I previously "managed" by leaving the bulk of the money in my account and hoping that I'd calculated my expenses versus salary roughly correctly, this is suboptimal for answering various questions like "Do I have enough money to go on holiday?" or "Have I forgotten to pay the electricity bill for the last year so that I now have to pay it back?"
Clearly there is a better way. Checking off expected payments, knowing what's due in the next week/month/etc., and siphoning off the excess into a linked savings account for example. But this requires being alert and accurate... Failing that, I could always use a computer ;-)
It's particularly bad at being a working document. Moving an item (to reschedule the date, for example) involves:
There are also web based ones. My initial survey of these suggests that they are prettier and easier to use, but perhaps clunkier, or tied into specific bank systems.
Every project needs a good name! But this is just a learning exercise, so for now I'll call it "Beans". I'm hoping this will end up an ongoing series of posts, please feel free to follow along at the Beans git repo if you like, or to suggest improvements, for example:
Now we'll want to declare some fields for our class. For example, every
payment made or expected will have a value:
use MooseX::Declare;
class Beans::Item {
...
}
What's that? Perl has types?! Yes indeed, Moose gives us types, and
automatically validates against them. So when we try to create our object,
we'd find that:
has value => (
isa => Num,
is => 'rw',
required => 1,
);
I've cheekily missed out a few details though... Num isn't a Perl
builtin keyword, so we'll need to import it. In fact, while we're at it, let's
pull in Str too, so that we can add a couple of string fields at the
same time:
my $obj = Beans::Item->new( ); # fails, value is required!
my $obj = Beans::Item->new( value => "foo" ); # fails, "foo" is a string
my $obj = Beans::Item->new( value => 10.00 ); # OK! 10.00 is a number
Now we also want to be able to add the date of the expected payment:
use MooseX::Types::Moose qw/ Num Str /;
has name => ( isa => Str,
is => 'rw',
required => 1,
);
has comment => ( isa => Str,
is => 'rw',
);
This means we could do
use MooseX::Types::DateTime qw/ DateTime /;
has due_date => ( isa => DateTime,
is => 'rw',
required => 1,
);
And we can indeed make this prettier using coercions!
my $obj = Beans::Item->new(
value => 10.00,
due_date => DateTime->new(
year => 2010,
month => 6,
day => 30,
),
);
# but I'd prefer to be able to do
due_date => '2010-06-30',
While every line item will have an expected due_date, we'll also want
to keep track of what date the item was actually paid, if it has
been paid. As this is uncertain, we use the Haskell-inspired Maybe
type. (As far as I can see, this isn't actually as powerful as Haskell's
Maybe monad, it's more like a nullable type).
use DateTime::Format::Natural;
my $dt = DateTime::Format::Natural->new( prefer_future => 1 );
coerce 'DateTime'
=> from 'Str'
=> via { $dt->parse_datetime($_[0]) };
# We'll need to tweak the due_date declaration too...
# (coercions don't run unless you ask for them)
has due_date => ( isa => DateTime,
is => 'rw',
required => 1,
coerce => 1,
);
Once we've got this type, we can calculate a boolean field based on it.
The paid field will be true if paid_date contains anything,
and false otherwise. Let's try defining it like this:
use MooseX::Types::Moose qw/ Num Str Maybe /;
has paid_date => ( isa => Maybe[DateTime],
is => 'rw',
);
The default code block gets called the first time we ask for
the field. There are a couple of problems with this of course - once it's
set, if we change the paid_date, this field won't change. We'll come back to
this later, but here are some ideas:
use MooseX::Types::Moose qw/ Num Str Maybe Bool/;
has paid => ( isa => Bool,
is => 'rw',
lazy => 1,
default => sub {
my $self = shift;
defined $self->paid_date
},
);
use MooseX::Types -declare => [qw/ NonEmptyStr / ];
subtype NonEmptyStr
=> as Str
=> where { length $_ };
# so now we can modify this definition to:
has name => ( isa => NonEmptyStr,
is => 'rw',
required => 1,
);
>
Finally, let's add a list of tags. Just for now, we'll define these as an
array of non-empty strings:
use MooseX::Types::Moose qw/ Num Str Bool Maybe ArrayRef /;
has tags => ( isa => ArrayRef[NonEmptyStr],
is => 'rw',
default => sub { [] },
);
Notice how we're using default again to set the default value to
an empty array.
Joel++ booked a flat (via waytostay.com) for 5 of us. This was an excellent idea, reasonably priced, and a beautiful flat, great for socializing in. Highly recommended, especially for a group of friends/colleagues.
I've been apologizing for some time for not having actually sat down and released modules for all the techniques I'm playing with here. But James Laver finally pestered me enough, and we've created a new project: Foose (working title, in homage to Moose) which will aim to bring neatly packaged functional goodness to Perl. (See also irc:irc.perl.org/#foose)
If you're interested by anything you've read in this blog and would like to hire me, then please do get in touch ;-) I'm mainly interested in teleworking or contracts in Northwest UK, but happy to discuss!
While we were discussing how to promote the Italian Perl Workshop, and the planned training on Moose, I noted that there weren't any articles on Moose (Perl's modern OO implementation, inspired by CLOS, Smalltalk and Ruby) on perl.it. Lordarthas of course told me "well volunteered!"... oops.
I pointed out that I don't really know Moose, and we eventually agreed that I would "just" translate Jay Kuri's nice new Gentle Introduction.
Now, there is a reason why translators almost always translate into their native language. I can write in Italian reasonably well, but translating into it was a much harder task. While you're writing something yourself, you tend to route around phrases you don't know how to express, choose different words, simplify structures, etc. But translation implies some degree of fidelity to the source, and I found this incredibly hard going. I whined on #perl.it and, in true Open Source JFDI style, larsen asked "Huh? Why are you translating that?" and did it himself! Yay, larsen++!
So my volunteering ended up being limited to making a few corrections/suggestions, along with lordarthas, dree, and dada. Opensource translation and review (using wiki/email in this case, but a git repo or similar could work just as well) can have a fast turnaround, and pick up many errors/nuances that a lone translator would have to work really hard on to do by themselves.
An interesting problem with the technical translation was deciding which phrases to leave in English, and which to translate. Looks like "coercion" is staying in English (followed by an explanation) instead of using the Italian "coercizione". And the title is surprisingly hard to translate, as none of the words for "gentle" map well into Italian. Though it's less cute than the original, the least awful alternative seems to be "Una breve introduzione" (a brief introduction).
Organizing a conference is hard, let's go shopping! For the first time I'm officially helping, not just for the Italian Perl Workshop this year, but possibly for YAPC::EU::2010 too. I've been working with the perl.it guys on the proposal to host the European Perl Conference 2010 in Pisa.
We submitted the bid on Monday, and it's just been announced that the teams competing are: Pisa (us) and Kiev in Ukraine. Wow! YEF have actually published both our bids on that link, which is fantastic for transparency.
It also means that we can read their bid... and it's a good one! Looks like we've got some competition. Of course our Pisa bid is excellent too - in any case, the next couple of weeks till we find out who won are going to be a nailbiting time!
Osfameron's blog on Haskell, Perl programming, stuff.