Haskell‘s notorious learning curve isn’t just down to the apparent need to learn advanced Category Theory before you can cogently browse the documentation. It’s also that you’re hit with a syntax that’s really unfamiliar – at least if you’re coming from the more "mainstream" languages like I am (Python, C++, Coffeescript). In particular, you’re hit with a lot of operators that appear to do similar things. The differences are subtle to me, night and day to people who actually know what they’re doing.
In particular, the difference between
($) wasn’t immediately obvious to me. They both seem to do a kind of function application.
Of course, it’s all in the name.
(.) is composition, and
($) is application. Moreover,
($) has a very different precedence ranking to
(.) It’s much lower on the food chain – essentially always last to go.
It’s easy to see where we newcomers get confused. After all, isn’t function application just passing an argument to a function? And isn’t function composition achieved by the same method? Aren’t these two things essentially equivalent, really?
And really, yes they are.
Nevertheless, you do run across situations where one is more appropriate than the other.
Consider a situation I was in today. I’m learning the Time package, and one of the things I was playing around with was adding to a localized time. Annoyingly,
Data.Time doesn’t really give you an easy way to do this. As far as I can tell, you have to:
- convert the local time to UTC
- add the difference in seconds
- convert it back to your local time, which requires passing in the timezone
The last of those is the rub. Without that, we’d have an easy oneliner:
addToZonedTime offset = utcToZonedTime . addUTCTime offset . zonedTimeToUTC
addToZonedTime does just what I said: it takes an
offset (in seconds) and returns a composed function that will take a
Data.Time jargon for a localized time representation object), convert it to UTC, apply the adjustment, then turn it back into its local time representation. But of course this doesn’t work – it doesn’t even compile – because
utcToZonedTime is of type
TimeZone -> UTCTime -> ZonedTime – which is to say, it takes a timezone as its first argument. Damn.
So the way we have to write it is:
addToZonedTime tz offset = utcToZonedTime tz . addUTCTime offset . zonedTimeToUTC
It looks like a minor change, but it’s a really annoying one, because to pass in a
TimeZone representation I have to first extract it from by
ZonedTime object external to the function. That just feels wrong. Passing in the
offset feels natural: that changes on each call. Passing in the local time (
ZonedTime) feels natural: that’s the thing I’m seeking to change, after all. But passing in the
timezone is something I shouldn’t have to do – because the
ZonedTime object already has that information. Passing it in separately is redundant.
I mean, how ugly is this?
addToZonedTime (zonedTimeZone localtime) (60*60*25) localtime
You can see right in the call that
localtime is in there twice!
And of course, this is what
where statements are for. So, how about this instead?
addToZonedTime offset localtime = utcToZonedTime tz . addUTCTime offset . zonedTimeToUTC localtime where tz = zonedTimeZone localtime
Much better! One problem: it doesn’t run. The error it throws says that
zonedTimeToUTC is applied to too many arguments – and that’s because the way this function is defined is as nothing but composed functions. The definition says that
addToZoneTime plus an
offset plus a
localtime is the composition of:
tzis the timezone portion of
Put simply, we’ve composed all these functions together, but nothing actually triggers it to run! They have to be applied to something. Hence –
This definition works:
addToZonedTime offset localtime = utcToZonedTime tz . addUTCTime offset . zonedTimeToUTC $ localtime where tz = zonedTimeZone localtime
So, that’s a situation where there’s a real difference. Because the precedence of
$ is so low, the compiler knows that I don’t mean to compose
zonedTimeToUTC applied to
localtime with everything else. Now it says what I actually meant, which is to compose all that other stuff together and then apply it to
localtime. The and then is made possible by a grant from the fact that
$ always gets its say last. First do whatever’s on the left and right sides of
$, then apply the left to the right.
And that, in a compact example, is why Haskell gives us both
($), even though they’re the same in many prominent ways. The little differences do matter.