by Yitzchak Schaffer (@yitznewton)
My main production language is PHP. I started dabbling in Haskell a couple of years ago, out of curiosity. I’ve found a few great resources out there and have gone through the novice lessons a few times. I still haven’t got to the point where I have graduated to the point of building Real Things in the language. I noticed, however, that exposure to the very different approach and style of Haskell had an immediate broadening effect on my PHP work. I encountered a great example of that yesterday.
Iterating with exceptional cases
The problem: we need to collapse a list of strings by interposing a delimiter, the classic sort of implode()
operation. But not all cases get the delimiter. The actual application we are building allows for different rules, but let’s treat the “non-Oxford comma” case for the purposes of this blog post.
class DelimiterRendererTest extends \PHPUnit_Framework_TestCase
{
public function data()
{
return [
[
[
],
'',
],
[
[
'jim',
],
'jim',
],
[
[
'jim',
'bob',
],
'jim and bob',
],
[
[
'jim',
'bob',
'joe',
],
'jim, bob and joe',
],
[
[
'jim',
'bob',
'joe',
'pete',
],
'jim, bob, joe and pete',
],
];
}
/**
* @dataProvider data
* @param string[] $strings
* @param string $expected
*/
public function testRender(array $strings, $expected)
{
$renderer = new DelimiterRenderer();
$this->assertEquals($expected, $renderer->render($strings));
}
}
If we were to use a loop for this, each iteration would have to know the context of the looping in order to know whether the delimiter should be added. To me, that is both a potential breeding-ground for bugs, and ugly; I should be able to declare the correct rendering for a given segment or combinations of segments without reference to segments that I’m not working on.
Pattern matching and recursion
In Haskell, pattern matching is one of the main ways to choose between alternate implementations, given your input. This problem could be solved in Haskell as follows. The function is defined with a type signature first, and then the actual function definition.
renderDelimited :: [String] -> String
renderDelimited [] = ""
renderDelimited (x:[]) = x
renderDelimited (x:y:[]) = x ++ " and " ++ y
renderDelimited (x:xs) = x ++ ", " ++ renderDelimited xs
The renderDelimited
function takes an array of String
s, and returns a String
. There are four cases: the empty array, a one-member array ((x:[])
means one member merged with an empty array), a two-member array, and “everything else.”
I’ve applied recursion to build the full string. Where there are three or more members, we render the first member, the delimiter, and then the output of rendering the rest of the list with the same function. That’s what makes it recursive.
*Main> renderDelimited []
""
*Main> renderDelimited ["bob"]
"bob"
*Main> renderDelimited ["bob", "pete"]
"bob and pete"
*Main> renderDelimited ["bob", "pete", "joe"]
"bob, pete and joe"
*Main> renderDelimited ["bob", "pete", "joe", "larry"]
"bob, pete, joe and larry"
PHP obviously doesn’t support pattern matching in this way, but we can do the same thing with explicit conditionals. Use of the implode()
function allows us to merge the 1
and 2
cases.
class DelimiterRenderer
{
/**
* @param array $strings
* @return string
*/
public function render(array $strings)
{
$count = count($strings);
if ($count === 0) {
return '';
}
if ($count < 3) {
return implode(' and ', $strings);
}
return $strings[0] . ', ' . $this->render(array_slice($strings, 1));
}
}
Boom! The tests pass.
By using this technique drawn from bread-and-butter Haskell, we’ve avoided bringing the surrounding context explicitly into the iterations of a loop, and instead we are evaluating each step on its own.
more
{ 0 comments... » Learn You a Haskell for Great Good in PHP, Ruby, ... read them below or add one }
Post a Comment