unwords2 :: [String] -> String
unwords2 [] = []
unwords2 (x:xs) = x ++ (concatMap (\s -> ' ':s) xs)
Testing on the Godbolt compiler explorer the Godbolt compiler explorer shows what code you actually get with both versions, and allows you to see the intermediate representation of both. The original version gets transformed into:
test2_go1
= \ ds_a14Zds1_a151 ->
case ds_a14Zds1_a151 of {
[] -> [];
: y_a152y_a154 ys_a153ys_a155 -> ++_$s++ lvl_r15Yds_r160 y_a152y_a154 (test2_go1 ys_a153ys_a155)
}
end Rec }
unwords2
= \ ds_dIOds1_dIN ->
case ds_dIOds1_dIN of {
[] -> [];
: x_aHm xs_aHn -> ++ x_aHm (test2_go1 xs_aHn)
}
unwords2 :: [String] -> String
unwords2 [] = []
unwords2 (x:xs) = x ++ (concatMap (\s -> ' ':s) xs)
Testing on the Godbolt compiler explorer shows what code you actually get with both versions, and allows you to see the intermediate representation of both. The original version gets transformed into:
test2_go1
= \ ds_a14Z ->
case ds_a14Z of {
[] -> [];
: y_a152 ys_a153 -> ++_$s++ lvl_r15Y y_a152 (test2_go1 ys_a153)
}
end Rec }
unwords2
= \ ds_dIO ->
case ds_dIO of {
[] -> [];
: x_aHm xs_aHn -> ++ x_aHm (test2_go1 xs_aHn)
}
unwords2 :: [String] -> String
unwords2 [] = []
unwords2 (x:xs) = x ++ (concatMap (' ':) xs)
Testing on the Godbolt compiler explorer shows what code you actually get with both versions, and allows you to see the intermediate representation of both. The original version gets transformed into:
test2_go1
= \ ds1_a151 ->
case ds1_a151 of {
[] -> [];
: y_a154 ys_a155 -> ++_$s++ ds_r160 y_a154 (test2_go1 ys_a155)
}
end Rec }
unwords2
= \ ds1_dIN ->
case ds1_dIN of {
[] -> [];
: x_aHm xs_aHn -> ++ x_aHm (test2_go1 xs_aHn)
}
That should be "well-behaved" enough for the compiler, in some cases, to optimize away deep copies of the words you pass in. (Testing
Testing on the Godbolt compiler explorer the Godbolt compiler explorer shows what code you actually get with both versions, and GHC actually does a pretty good joballows you to see the intermediate representation of both. The original version gets transformed into:
unwords'
= \ ds_dJs ->
case ds_dJs of {
[] -> [];
: x_agY xs_agZ ->
case xs_agZ of wild1_a13X {
[] -> x_agY;
: ds1_a13Y ds2_a13Z ->
++ x_agY (unpackAppendCString# lvl1_r15Z (unwords' wild1_a13X))
}
}
Which as you see is making redundant deep copies with yoursunpackAppendCString
of each nested recursive call.) The second version becomes:
test2_go1
= \ ds_a14Z ->
case ds_a14Z of {
[] -> [];
: y_a152 ys_a153 -> ++_$s++ lvl_r15Y y_a152 (test2_go1 ys_a153)
}
end Rec }
unwords2
= \ ds_dIO ->
case ds_dIO of {
[] -> [];
: x_aHm xs_aHn -> ++ x_aHm (test2_go1 xs_aHn)
}
where the concatMap
call becomes a helper function that reduces to a builtin. Introducing calls to these functions as top-level variables shows that GHC is capable of partially inlining the second version but not the first.
That should be "well-behaved" enough for the compiler, in some cases, to optimize away deep copies of the words you pass in. (Testing on the Godbolt compiler explorer shows what code you actually get with both versions, and GHC actually does a pretty good job with yours.)
That should be "well-behaved" enough for the compiler, in some cases, to optimize away deep copies of the words you pass in.
Testing on the Godbolt compiler explorer shows what code you actually get with both versions, and allows you to see the intermediate representation of both. The original version gets transformed into:
unwords'
= \ ds_dJs ->
case ds_dJs of {
[] -> [];
: x_agY xs_agZ ->
case xs_agZ of wild1_a13X {
[] -> x_agY;
: ds1_a13Y ds2_a13Z ->
++ x_agY (unpackAppendCString# lvl1_r15Z (unwords' wild1_a13X))
}
}
Which as you see is making redundant deep copies with unpackAppendCString
of each nested recursive call. The second version becomes:
test2_go1
= \ ds_a14Z ->
case ds_a14Z of {
[] -> [];
: y_a152 ys_a153 -> ++_$s++ lvl_r15Y y_a152 (test2_go1 ys_a153)
}
end Rec }
unwords2
= \ ds_dIO ->
case ds_dIO of {
[] -> [];
: x_aHm xs_aHn -> ++ x_aHm (test2_go1 xs_aHn)
}
where the concatMap
call becomes a helper function that reduces to a builtin. Introducing calls to these functions as top-level variables shows that GHC is capable of partially inlining the second version but not the first.
The original implementation isn’t ideal, because it’s recursive without being tail-recursive or tail-recursive-modulo-cons (two special cases that GHC can optimize). Other answers have already discussed how to fix up the foldr
implementation, which would be more efficient. (In practice, testing with the Godbolt compiler explorer shows that GHC does a decent job with it.)
This code could also be cleaned up with pattern-matching:
unwords' [] = []
unwords' (x:xs)
| null xs = x
| otherwise = x ++ " " ++ unwords' xs
could become:
unwords' [] = []
unwords' [x] = x
unwords' (x:xs) = x ++ " " ++ unwords' xs
Another efficient technique in functional programming is a map-reduction. In this case, you can represent adding a single character in front of each word after the first as a :
operation, then concatenate them, which lets you write this as:
unwords2 :: [String] -> String
unwords2 [] = []
unwords2 (x:xs) = x ++ (concatMap (\s -> ' ':s) xs)
That should be "well-behaved" enough for the compiler, in some cases, to optimize away deep copies of the words you pass in. (Testing with ghc -O
on Godboltthe Godbolt compiler explorer shows that this versionwhat code you actually get with both versions, and GHC actually does not need to call ghc-prim_GHC.CString_unpackAppendCString
a pretty good job with yours.)
Although, in the real world, if you care about optimized string-processing, you would use a Text
or a ByteString
, not a linear linked list of UCS-4 [Char]
.
The original implementation isn’t ideal, because it’s recursive without being tail-recursive or tail-recursive-modulo-cons (two special cases that GHC can optimize). Other answers have already discussed how to fix up the foldr
implementation, which would be more efficient. (In practice, testing with the Godbolt compiler explorer shows that GHC does a decent job with it.)
This code could also be cleaned up with pattern-matching:
unwords' [] = []
unwords' (x:xs)
| null xs = x
| otherwise = x ++ " " ++ unwords' xs
could become:
unwords' [] = []
unwords' [x] = x
unwords' (x:xs) = x ++ " " ++ unwords' xs
Another efficient technique in functional programming is a map-reduction. In this case, you can represent adding a single character in front of each word after the first as a :
operation, then concatenate them, which lets you write this as:
unwords2 :: [String] -> String
unwords2 [] = []
unwords2 (x:xs) = x ++ (concatMap (\s -> ' ':s) xs)
That should be "well-behaved" enough for the compiler, in some cases, to optimize away deep copies of the words you pass in. (Testing with ghc -O
on Godbolt shows that this version does not need to call ghc-prim_GHC.CString_unpackAppendCString
.)
Although, in the real world, if you care about optimized string-processing, you would use a Text
or a ByteString
, not a linear linked list of UCS-4 [Char]
.
The original implementation isn’t ideal, because it’s recursive without being tail-recursive or tail-recursive-modulo-cons (two special cases that GHC can optimize). Other answers have already discussed how to fix up the foldr
implementation, which would be more efficient.
This code could also be cleaned up with pattern-matching:
unwords' [] = []
unwords' (x:xs)
| null xs = x
| otherwise = x ++ " " ++ unwords' xs
could become:
unwords' [] = []
unwords' [x] = x
unwords' (x:xs) = x ++ " " ++ unwords' xs
Another efficient technique in functional programming is a map-reduction. In this case, you can represent adding a single character in front of each word after the first as a :
operation, then concatenate them, which lets you write this as:
unwords2 :: [String] -> String
unwords2 [] = []
unwords2 (x:xs) = x ++ (concatMap (\s -> ' ':s) xs)
That should be "well-behaved" enough for the compiler, in some cases, to optimize away deep copies of the words you pass in. (Testing on the Godbolt compiler explorer shows what code you actually get with both versions, and GHC actually does a pretty good job with yours.)
Although, in the real world, if you care about optimized string-processing, you would use a Text
or a ByteString
, not a linear linked list of UCS-4 [Char]
.