Like many, I started out with procedural programming. Of course, when learning a functional language, old habits may die hard, so I wrote a fairly trivial little thing which takes an integer and returns a list of the english-language representation.
A few days later, I re-wrote it to try to take advantage of tail recursion in the main order-of-magnitude function; I freely admit that no attempt has been made to add tail-recursion to the calculation of numbers less than 1000, but since those have a fixed maximum depth of 3 (and a maximum of 5 calls in any case) I opted not to worry about it for now.
Any notes and criticisms, please lob them at me, that I may learn to do things in a less procedural way.
-module(titoa).
-export([itoa/1]).
itoa(0) -> "zero";
itoa(N) when is_float(N) -> no_float_support;
itoa(N) when N < 0 -> "negative " ++ itoa(abs(N));
itoa(N) when is_integer(N) -> itoa_render(N);
itoa(_) -> severe_error.
itoa_render(N) when N >= 1100, N < 10000, N rem 100 == 0, N rem 1000 /= 0 ->
itoa_render(N, ["", "hundred"], 100, []);
itoa_render(N) when N >= 1000 ->
itoa_render(N,["",
"thousand","million","billion","trillion","quadrillion","quintillion",
"sextillion","septillion","octillion","nontillion","dectillion"
],1000, []
);
itoa_render(N) when N >= 100, N rem 100 == 0 ->
itoa_render(N div 100) ++ " hundred";
itoa_render(N) when N >= 100 ->
Hun_diff = N rem 100,
itoa_render(N - Hun_diff) ++ [32 | itoa_render(Hun_diff)];
itoa_render(N) when N > 19, N rem 10 == 0 ->
lists:nth(N div 10 - 1, [
"twenty","thirty","forty","fifty","sixty","seventy","eighty","ninety"
]);
itoa_render(N) when N > 19 ->
Ten_diff = N rem 10,
itoa_render(N - Ten_diff) ++ "-" ++ itoa_render(Ten_diff);
itoa_render(N) when N > 0 ->
lists:nth(N, [
"one","two","three","four","five","six","seven","eight","nine",
"ten","eleven","twelve","thirteen","fourteen","fifteen","sixteen",
"seventeen","eighteen","nineteen"
]);
itoa_render(_) -> []. % 0
itoa_render(0, _, _, After) -> After;
itoa_render(_, [], _, _) -> overflow;
itoa_render(N, [Magnitude | Remaining_Magnitudes], Factor, After) ->
This_OOM = itoa_render(N rem Factor),
This_Rep = if
This_OOM == [] -> [];
Magnitude == [] -> This_OOM;
true -> This_OOM ++ [32 | Magnitude] ++ if
After == [] -> [];
true -> [32]
end
end,
itoa_render(N div Factor, Remaining_Magnitudes, Factor, This_Rep ++ After).
2 Answers 2
At first glance, you're trying to be a bit cute here and it's making your code more complex than necessary. For example, why not handle 11 and 12 as part of the teens and why not just type out the teen and decade names in full rather than having extra logic to append a not-quite common suffix ("teen" and "ty")?
For n < 1000
you have the following rules (let <n>
mean "n
in words", %
mean modulo, and //
mean integer division):
if 100 <= n then <n // 100> " hundred" ++
(if n % 100 == 0 then "" else " and " ++ <n % 100>)
if 20 <= n then {"", "twenty", ..., "ninety"}[n // 10] ++
(if n % 10 == 0 then "" else " " ++ <n % 10>)
otherwise {"zero", "one", "two", ..., "ten", "eleven", ..., "nineteen"}[n]
Now you only need to deal with the orders of magnitude ("thousand", "million", etc.).
-
1\$\begingroup\$ I'll grant you the bit about the splitting of 12-19, and the "teen" and "ty". However, could you please help me understand why doing it with if clauses is more efficient than using Erlang's guard syntax, and furthermore, what about the tail recursion -- did I implement it correctly or will it not work? \$\endgroup\$Dereleased– Dereleased2011年10月03日 06:18:08 +00:00Commented Oct 3, 2011 at 6:18
-
\$\begingroup\$ Just three quick points. First, there should be no efficiency difference between using if-then-elses or guards -- they're the same pig wearing different dresses. Second, where you write
itoa_render(N - Hun_diff) ++ [32 | itoa_render(Hun_diff)];
, you're clearly not being tail recursive since you have a recursive call in a constructor argument in a function argument. Third, you probably shouldn't worry about efficiency too much here since this algorithm is O(log n) anyway! My point was more about clarity. \$\endgroup\$Rafe– Rafe2011年10月03日 22:40:22 +00:00Commented Oct 3, 2011 at 22:40 -
\$\begingroup\$ And a valid point it is, clarity. I know the 0-999 calculators aren't tail recursive ("I freely admit that no attempt has been made to add tail-recursion to the calculation of numbers less than 1000"), but the concept of tail recursion is fairly new to me, so largely I just wanted to be sure that I had managed to get it going in that one place, or if I failed to grasp it and thus failed to implement it. I am going to rewrite it using explicit ifs and see if that increases clarity. \$\endgroup\$Dereleased– Dereleased2011年10月03日 23:27:58 +00:00Commented Oct 3, 2011 at 23:27
-
\$\begingroup\$ Tail recursion typically looks something like this:
f(x) = if isBaseCase(x) then a else f(g(x))
. Anything else, such asf(x) = if isBaseCase(x) then a else h(f(g(x)))
is not tail recursive. \$\endgroup\$Rafe– Rafe2011年10月04日 01:19:12 +00:00Commented Oct 4, 2011 at 1:19 -
\$\begingroup\$ I was going to post it, but I'm electing not to now; I must've done something wrong, because it seems larger and more convoluted. I suppose I have a fair bit more learning to do. \$\endgroup\$Dereleased– Dereleased2011年10月04日 01:56:21 +00:00Commented Oct 4, 2011 at 1:56
- Defensive programming is not recommended in Erlang, which means you could/should remove the
no_float_support
andsevere_error
clause. Using lists:nth is generally bad practice since it has a
O(n)
complexity and this means using your list like an array. For fixed-size lists, use tuples instead. Here's an example:element(N div 10 - 1, {twenty","thirty","forty","fifty","sixty","seventy","eighty","ninety"} );