所以我对Erlang真的很感兴趣。我找不到借口把它用于任何大的东西,但我不时尝试用它来解决玩具问题。
现在,我正在实施罗马数字翻译。我现在只是做“到”部分,我发现代码非常重复。它就像一个魅力,但是,好吧,看看它:
-module(roman).
-compile([export_all]).
toRoman(N) ->
toRoman(N, []).
toRoman(0,Acc) ->
lists:reverse(lists:flatten(Acc));
toRoman(N, Acc) when N >= 1000 ->
toRoman(N-1000,["M"|Acc]);
toRoman(N,Acc) when N >= 900 ->
toRoman(N-900,["CM" | Acc]);
toRoman(N,Acc) when N >= 500 ->
toRoman(N-500,["D" | Acc]);
toRoman(N,Acc) when N >= 400 ->
toRoman(N-400, ["CD" | Acc]);
toRoman(N,Acc) when N >= 100 ->
toRoman(N-100, ["C" | Acc]);
toRoman(N,Acc) when N >= 90 ->
toRoman(N-90, ["XC" | Acc]);
toRoman(N,Acc) when N >= 50 ->
toRoman(N-50, ["L" | Acc]);
toRoman(N, Acc) when N >= 40 ->
toRoman(N-40, ["XL" | Acc]);
toRoman(N, Acc) when N >= 10 ->
toRoman(N-10, ["X" | Acc]);
toRoman(N, Acc) when N >= 9 ->
toRoman(N-9, ["IX" | Acc]);
toRoman(N, Acc) when N >= 5 ->
toRoman(N-5, ["V" | Acc]);
toRoman(N, Acc) when N >= 4 ->
toRoman(N-4, ["IV" | Acc]);
toRoman(N, Acc) ->
toRoman(N-1, ["I" | Acc]).
test() ->
Test = fun(X) -> io:format("~p -> ~p~n", [X, toRoman(X)]) end,
lists:map(Test, [0,1,3,6,23,43,75,87,13,23, 3999, 3998, 2531, 140]).
我觉得有更好的方法来做到这一点。任何人都可以提供一些见解吗?
答案 0 :(得分:6)
实际上你的代码并不重复。它只是看起来像因为文本形成一个重复的模式。但是你的每个子句都处理一个特定的情况,它们之间几乎没有逻辑重叠。您可以在switch语句中重新实现,但是您会得到类似的重复。罗马数字翻译的案例太多了,我认为你不能避免重复的感觉,使每一个决定都成为原因。
答案 1 :(得分:5)
之前我已经将这一个添加到rosettacode.org,并在此处重新发布。 我发现解决方案非常优雅。
-module(roman).
-export([to_roman/1]).
to_roman(0) -> [];
to_roman(X) when X >= 1000 -> "M" ++ to_roman(X-1000);
to_roman(X) when X >= 100 -> digit(X div 100, $C,$D,$M) ++ to_roman(X rem 100);
to_roman(X) when X >= 10 -> digit(X div 10, $X,$L,$C) ++ to_roman(X rem 10);
to_roman(X) when X >= 1 -> digit(X, $I,$V,$X).
digit(1,X,_,_) -> [X];
digit(2,X,_,_) -> [X,X];
digit(3,X,_,_) -> [X,X,X];
digit(4,X,Y,_) -> [X,Y];
digit(5,_,Y,_) -> [Y];
digit(6,X,Y,_) -> [Y,X];
digit(7,X,Y,_) -> [Y,X,X];
digit(8,X,Y,_) -> [Y,X,X,X];
digit(9,X,_,Z) -> [X,Z].
答案 2 :(得分:4)
如果你不想重复,你可以通过我的Code Golf New Year Edition - Integer to Roman Numeral contribution激励你。
-module(n2).
-export([y/1]).
-define(D(V,S),n(N)when N>=V->[??S|n(N-V)];).
y(N)->io:format(n(N)).
?D(1000,M)?D(900,CM)?D(500,D)?D(400,CD)?D(100,C)?D(90,XC)?D(50,L)?D(40,XL)?D(10,X)?D(9,IX)?D(5,V)?D(4,IV)?D(1,I)n(0)->[10].
在erlang中编写代码并不是一个好的推荐方法。宏很糟糕。如果可以,请避免它。它很难调试,它引入了不受热代码交换跟踪的模块间依赖性,因此也是如此。如果你喜欢更多功能性方法,那么“代码就是数据,数据就是代码”,请看这个例子:
-module(roman).
-compile([export_all]).
toRoman(N) when is_integer(N), N >= 0 ->
toRoman(N,
[{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"},
{100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"},
{10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}]).
toRoman(0, _) -> [];
toRoman(N, [{X, V} | _] = S) when N >= X ->
[V | toRoman(N - X, S)];
toRoman(N, [_ | S]) -> toRoman(N, S).
test() ->
F = fun (X) -> lists:flatten(toRoman(X)) end,
"" = F(0),
"I" = F(1),
"III" = F(3),
"VI" = F(6),
"XXIII" = F(23),
"XLIII" = F(43),
"LXXV" = F(75),
"LXXXVII" = F(87),
"XIII" = F(13),
"XXIII" = F(23),
"MMMCMXCIX" = F(3999),
"MMMCMXCVIII" = F(3998),
"MMDXXXI" = F(2531),
"CXL" = F(140),
ok.
只是出于好奇,你的代码在字节码中的速度提高了5%,在本机代码的速度比我的低5%。它在字节码中以1.2us执行一次转换,在Intel(R)Core(TM)2 Duo CPU T7500 @ 2.20GHz上以本机执行370ns。
编辑:我没有使用尾递归版本,因为递归深度非常小。所以我很好奇是否有任何性能损失或收益。我无法测量字节码中的任何算法甚至本机,但在原始代码中会发生有趣的事情。如果我以直接的方式编写原始算法(未针对尾部调用进行优化),它在本机代码中比我的快40%(一次转换大约250ns)。字节代码没有可测量的差异。这是一个有趣的例子,尾部调用优化不值得做。
toRoman(N) when N >= 1000 -> "M" ++ toRoman(N - 1000);
toRoman(N) when N >= 900 -> "CM" ++ toRoman(N - 900);
toRoman(N) when N >= 500 -> "D" ++ toRoman(N - 500);
toRoman(N) when N >= 400 -> "CD" ++ toRoman(N - 400);
toRoman(N) when N >= 100 -> "C" ++ toRoman(N - 100);
toRoman(N) when N >= 90 -> "XC" ++ toRoman(N - 90);
toRoman(N) when N >= 50 -> "L" ++ toRoman(N - 50);
toRoman(N) when N >= 40 -> "XL" ++ toRoman(N - 40);
toRoman(N) when N >= 10 -> "X" ++ toRoman(N - 10);
toRoman(N) when N >= 9 -> "IX" ++ toRoman(N - 9);
toRoman(N) when N >= 5 -> "V" ++ toRoman(N - 5);
toRoman(N) when N >= 4 -> "IV" ++ toRoman(N - 4);
toRoman(N) when N >= 1 -> "I" ++ toRoman(N - 1);
toRoman(0) -> [].
P.S。:展平列表不是Erlang代码的常见行为。上述示例中的返回结构众所周知为io_list
,并且通常在erlang io系统中被接受。您可以将其直接发送到套接字,端口等。如果您想要编写它,可以使用io:put_chars(IOList)
或io:format("Result: '~s'~n", [IOList])
。
EDIT2 :如果++
运算符的左操作数有常量列表,那么erlang编译器将优化列表连接,因此["string" | L]
对速度不是必需的。生成的代码更具可读性,结果扁平化而不会降低性能。 Personaly,如果我对性能感兴趣,我会使用这个有点重复的版本但是我知道的最快,并且在字节代码中以310ns执行一次转换,在本机中执行210ns。
toRoman(N) when N >= 1000 -> "M" ++ toRoman(N - 1000);
toRoman(N) -> toRomanC(N div 100, N rem 100).
toRomanC(0, N) -> toRomanX(N);
toRomanC(1, N) -> "C" ++ toRomanX(N);
toRomanC(2, N) -> "CC" ++ toRomanX(N);
toRomanC(3, N) -> "CCC" ++ toRomanX(N);
toRomanC(4, N) -> "CD" ++ toRomanX(N);
toRomanC(5, N) -> "D" ++ toRomanX(N);
toRomanC(6, N) -> "DC" ++ toRomanX(N);
toRomanC(7, N) -> "DCC" ++ toRomanX(N);
toRomanC(8, N) -> "DCCC" ++ toRomanX(N);
toRomanC(9, N) -> "CM" ++ toRomanX(N).
toRomanX(N) -> toRomanX(N div 10, N rem 10).
toRomanX(0, N) -> toRomanI(N);
toRomanX(1, N) -> "X" ++ toRomanI(N);
toRomanX(2, N) -> "XX" ++ toRomanI(N);
toRomanX(3, N) -> "XXX" ++ toRomanI(N);
toRomanX(4, N) -> "XL" ++ toRomanI(N);
toRomanX(5, N) -> "L" ++ toRomanI(N);
toRomanX(6, N) -> "LX" ++ toRomanI(N);
toRomanX(7, N) -> "LXX" ++ toRomanI(N);
toRomanX(8, N) -> "LXXX" ++ toRomanI(N);
toRomanX(9, N) -> "XC" ++ toRomanI(N).
toRomanI(0) -> [];
toRomanI(1) -> "I";
toRomanI(2) -> "II";
toRomanI(3) -> "III";
toRomanI(4) -> "IV";
toRomanI(5) -> "V";
toRomanI(6) -> "VI";
toRomanI(7) -> "VII";
toRomanI(8) -> "VIII";
toRomanI(9) -> "IX".
答案 3 :(得分:2)
重复部分是累积和函数调用。将它们移到一个地方,事情会好得多。
%%% Roman numerals ruleset
r(N) when N >= 1000 -> {N-1000, "M"};
r(N) when N >= 900 -> {N-900, "CM"};
r(N) when N >= 500 -> {N-500, "D"};
r(N) when N >= 400 -> {N-400, "CD"};
r(N) when N >= 100 -> {N-100, "C"};
r(N) when N >= 90 -> {N-90, "XC"};
r(N) when N >= 50 -> {N-50, "L"};
r(N) when N >= 40 -> {N-40, "XL"};
r(N) when N >= 10 -> {N-10, "X"};
r(N) when N >= 9 -> {N-9, "IX"};
r(N) when N >= 5 -> {N-5, "V"};
r(N) when N >= 4 -> {N-4, "IV"};
r(N) when N > 0 -> {N-1, "I"}.
roman(N, Acc) ->
case r(N) of
{0, R} ->
[R | Acc];
{N2, R} ->
roman(N2, [R | Acc])
end.
roman(N) ->
list_to_binary(lists:reverse(roman(N, ""))).
顺便说一句,你得到4和6的相同结果:
8> [roman:toRoman(N) || N <- lists:seq(1,10)].
["I","II","III","VI","V","VI","VII","VIII","XI","X"]
同样的错误让你9和11相等,40和60,90和110 ......
答案 4 :(得分:2)
此过程分为三个部分:符号代表哪些数字的规则列表,搜索这些规则以查找下一个符号,以及将数字减少为零的迭代。每个部分都有一个功能,我们有:
ruleset() -> [
{1000, "M"},
{900, "CM"},
{500, "D"},
{400, "CD"},
{100, "C"},
{90, "XC"},
{50, "L"},
{40, "XL"},
{10, "X"},
{9, "IX"},
{5, "V"},
{4, "IV"},
{1, "I"}].
find_next(N) -> find_next(ruleset(), N).
find_next([{V, Symbol}|_], N) when N >= V -> {N - V, Symbol};
find_next([_|T], N) -> find_next(T, N).
roman(N, Acc) ->
case find_next(N) of
{0, R} -> [R | Acc];
{N2, R} -> roman(N2, [R | Acc])
end.
roman(N) ->
lists:append(lists:reverse(roman(N, ""))).
你可以使用list:foldl / 3来进一步简化这一点。
答案 5 :(得分:1)
这不重复,因为无论如何必须实施“逻辑”。你可以做的一件事就是让它非尾递归,因为你不会有超过20-30次递归......
-module(roman).
-compile([export_all]).
to_roman(N) when N >= 1000 -> "M" ++ to_roman(N-1000);
to_roman(N) when N >= 900 -> "CM" ++ to_roman(N- 900);
...
to_roman(N) when N >= 4 -> "IV" ++ to_roman(N- 4);
to_roman(N) when N >= 1 -> "I" ++ to_roman(N- 1);
to_roman(_) -> [].
您可以通过定义宏来进一步保存一些字符。我讨厌宏,但你可能会喜欢它们:)。
-module(roman).
-compile([export_all]).
-define( TO_ROMAN(L, C) , to_roman(N) when N >= L -> C ++ to_roman(N-L) ).
?TO_ROMAN(1000, "M");
?TO_ROMAN( 900, "CM");
...
?TO_ROMAN( 4, "IV");
?TO_ROMAN( 1, "I");
to_roman(_) -> [].
答案 6 :(得分:1)
如果您支持in Excel之类的罗马数字的所有变体,那会更有趣 但基本上你的代码仍然是一系列大案例/模式匹配标题......
答案 7 :(得分:-1)
使用查找表可以使其在任何语言中更短更快。