【问题标题】:Help me improve this Erlang?帮我改进这个 Erlang?
【发布时间】:2009-11-05 05:09:46
【问题描述】:

所以我对 Erlang 非常感兴趣。我找不到借口将它用于任何大事,但我会不时尝试将其用于玩具问题。

现在,我正在实现一个罗马数字翻译器。我现在只是在做“to”部分,我发现代码非常重复。它就像一个魅力,但是,看看它:

-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]).

我觉得有更好的方法来做到这一点。谁能提供一些见解?

【问题讨论】:

  • 老实说,在任何编程语言中,你都不会比现有的更好。
  • 您的代码中的一些 cmets:1) 您的代码错误。当你建立一个反向列表时,你的字符也应该被颠倒,例如为 4 添加“VI”而不是“IV”。 2)您可以一步连接,而不是构建深层列表然后展平。例如。而不是 ["IX" | Acc], "IX" ++ Acc.
  • 大概如果您将 lists:reverse(lists:flatten(Acc)) 更改为 lists:flatten(lists:reverse(Acc)) 那么您可以修复错误而无需反转所有两个字符数字。
  • 噢!是的,现场串联可能更明智,并且可以修复交换的两个字符数字。

标签: syntax erlang roman-numerals


【解决方案1】:

实际上,您的代码并没有那么重复。它看起来很像,因为文本形成了重复的模式。但是您的每个子句都处理一个特定情况,它们之间几乎没有逻辑重叠。您可以在 switch 语句中重新实现,但您会得到类似的重复。罗马数字翻译中的案例太多了,我认为您无法避免在做出每个单独的决定时会产生重复的感觉。

【讨论】:

  • 我猜不是,为了可读性和可维护性,这可能是我在编写生产罗马数字翻译代码的不太可能的情况下会这样做的方式。 (当然是修复已经指出的错误。)
【解决方案2】:

我之前已经将这个添加到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 倍),并且可以与直接方法的原生方法相媲美(大约慢 15%)。
  • 您甚至可以通过将尾部作为 digit/4 的额外参数包括在内,从而跳过附加来加快速度。我认为现在更清楚了。
  • @rvinrding:似乎它在 R13B02 中并没有做出可衡量的改进。看起来那个编译器神奇地为你做了。
  • 我认为尾递归不会让它更快(假设我们很少用罗马数字计算数十亿,这可能是一个相当大的假设)
【解决方案3】:

如果你不想重复,你可以通过我的Code Golf New Year Edition - Integer to Roman Numeralcontribution来启发。

-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%。它在 Intel(R) Core(TM)2 Duo CPU T7500 @ 2.20GHz 上以 1.2us 的字节码和 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] 对于速度来说不是必需的。生成的代码更具可读性,并且结果扁平化而没有性能损失。就个人而言,如果我对性能感兴趣,我会使用这个版本,它有点重复,但它是我所知道的最快的版本,并且在 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".

【讨论】:

  • 哇,这个解决方案看起来很眼熟... :)
【解决方案4】:

重复的部分是累加和函数调用。将它们移到一个地方,事情会看起来好多了。

%%% 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....

【讨论】:

    【解决方案5】:

    这个过程分为三个部分,一个规则列表,哪些符号代表哪些数字,搜索这些规则以找到下一个符号,以及将数字减少到零的迭代。每个部分都有一个功能,我们有:

    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, ""))).
    

    您可能可以使用 lists:foldl/3 进一步简化这一点。

    【讨论】:

    • 多哈。我当然可以将减法部分移出我的规则集,然后返回应该减去多少数字。然后我会返回常量。我当然支持我选择使用规则而不是查找表的函数。 :)
    【解决方案6】:

    这不是重复的,因为无论如何都必须实现“逻辑”。您可以做的一件事是使其非尾递归,因为无论如何您不会有超过 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(_) -> [].
    

    【讨论】:

      【解决方案7】:

      如果您支持罗马数字的所有变体,例如in Excel,它会变得更有趣 但基本上你的代码仍然是一系列大写/模式匹配的标题...

      【讨论】:

        【解决方案8】:

        使用查找表应该可以使其在任何语言中都更短、更快。

        【讨论】:

        • 这不是一对一的映射。他实际上必须比较大于或等于。这在查找表中要困难得多。我不认为在这种情况下它会更好。
        • 除非他为所有可能的数字生成一个包含 4000 个条目的查找表 :)
        • 你确定吗?我不知道。你能告诉我们这个例子的基准测试结果吗?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-12-28
        • 2023-03-19
        相关资源
        最近更新 更多