How many HOFs in Phobos?
Andrei Alexandrescu
SeeWebsiteForEmail at erdani.org
Mon Jan 31 15:08:42 PST 2011
On 1/31/11 3:37 PM, bearophile wrote:
> This is a high-level implementation of Levenshtein distance in Haskell:
>
>
> levenshtein s1 s2 = last $ foldl transform [0 .. length s1] s2
> where transform ns@(n:ns') c = scanl calc (n+1) $ zip3 s1 ns ns'
> where calc z (c', x, y) = minimum [y+1, z+1, x + fromEnum (c' /= c)]
>
> main = print (levenshtein "kitten" "sitting")
>
>
> (I don't explain that Haskell code because below I give a kind of D translation that is probably enough to understand.)
> It prints 3, the code comes from the rosettacode.org site:
> http://rosettacode.org/wiki/Levenshtein_distance#Haskell
>
> Currently in std.algorithm there are higher order functions (HOFs) like "map", "filter", "reduce" etc, reduce is the right fold.
>
> That Haskell code uses a foldl (fold left), and scanl (like foldl, but keeps and returns all intermediate values too). This is a brutal translation to D2:
>
>
> import std.stdio, std.range, std.array, std.algorithm, std.typecons;
>
> /// foldl f z [x1, x2, ..., xn] == (...((z `f` x1) `f` x2) `f`...) `f` xn
> T1 foldl(F, T1, Range)(F f, T1 z, Range xs) {
> foreach (x; xs)
> z = f(z, x);
> return z;
> }
>
> /// scanl f z [x1, x2, ...] == [z, z `f` x1, (z `f` x1) `f` x2, ...]
> T1[] scanl(F, T1, Range)(F f, T1 z, Range xs) {
> auto result = [z];
> foreach (x; xs) {
> z = f(z, x);
> result ~= z;
> }
> return result;
> }
>
> auto levenshtein(string s1, string s2) {
> int[] transform(int[] ns, char c) {
> auto n = ns[0];
> auto ns2 = ns[1..$];
> int compute(int z, Tuple!(dchar, int, int) t) {
> return min(t[2]+1, z+1, t[1] + (t[0] != c));
> }
> return scanl(&compute, n+1, zip(s1, ns, ns2));
> }
> return foldl(&transform, array(iota(cast(int)s1.length+1)), s2)[$-1];
> }
>
> void main() {
> string add(string x, string y) { return x ~ y; }
> writeln(foldl(&add, "", ["ab", "cd", "ef"])); // "abcdef"
> writeln(scanl(&add, "", ["ab", "cd", "ef"])); // ["", "ab", "abcd", "abcdef"]
>
> writeln(levenshtein("kitten", "sitting")); // 3
> }
>
>
> Haskell has tons of built-in HOFs (monadic ones too, like foldM), but D is less functional than Haskell, and reading highly functional-style code is not easy for many programmers (and D syntax limits make such code noisy). What's the right number of HOFs to put in std.algorithm+std.range? Are (better implementations of) scanl and foldl fit for Phobos? (I am not suggesting to rename "reduce" to "foldr". You may add an "alias reduce foldr;" if you want, but that's all).
>
> Hours ago I have added this suggestion to bugzilla:
> http://d.puremagic.com/issues/show_bug.cgi?id=5510
>
> Bye,
> bearophile
Thanks for this work.
The Haskell implementation doesn't scale. My testbed:
levenshtein s1 s2 = last $ foldl transform [0 .. length s1] s2
where transform ns@(n:ns') c = scanl calc (n+1) $ zip3 s1 ns ns'
where calc z (c', x, y) = minimum [y+1, z+1, x + fromEnum (c'
/= c)]
n = 4
s = concat $ replicate n "kitten"
t = concat $ replicate n "sitting"
main = print (levenshtein s t)
I assigned n doubling values: 4 8 16 32 64 ... and measured the running
times gave me:
4 -> 3ms
8 -> 4ms
16 -> 8ms
32 -> 32ms
64 -> 137ms
128 -> 607ms
256 -> 2.6s
512 -> 15.8s
1024 -> 78s
2048 -> doesn't finish with -O, crashes without -O
I used ghc -O on a top-of-the-line server (8 cores, 64 GB RAM).
Then I ran your implementation on a MacBook Pro with dmd -O -release
-inline:
4 -> 4ms
8 -> 4ms
16 -> 6ms
32 -> 11ms
64 -> 36ms
128 -> 108ms
256 -> 402ms
512 -> 1.59s
1024 -> 6.36s
2048 -> 25.43s
4096 -> 100.2s
Finally, on the same laptop and with the same options I ran the
std-provided levenshteinDistance, obtaining:
4 -> 5ms
8 -> 5ms
16 -> 5ms
32 -> 6ms
64 -> 11ms
128 -> 33ms
256 -> 112ms
512 -> 431ms
1024 -> 1.8s
2048 -> 7.9s
4096 -> 37.9s
Note that levenshteinDistance is more general (works with any forward
ranges, automatically performs correct UTF decoding, accepts custom
comparison).
The trend of the two D implementations is quadratic: time required is 4x
upon doubling the size of the input. The trend of the Haskell
implementation is super-quadratic.
Note that reduce is akin to foldl, not foldr. To answer your question,
adding functional primitives to Phobos is good but should be done in
moderation; a staunch functional approach is not always the most
recommendable.
Andrei
More information about the Digitalmars-d
mailing list