【问题标题】:Printing named parameters打印命名参数
【发布时间】:2010-09-29 10:55:03
【问题描述】:

Archaelus 在this post 中建议编写一个新的格式例程来处理命名参数可能是一个很好的学习练习。因此,本着学习语言的精神,我编写了一个处理命名参数的格式化例程。



一个例子:

1> fout:format("hello ~s{name}, ~p{one}, ~p{two}, ~p{three}~n",[{one,1},{three,3},{name,"Mike"},{two,2}]).
hello Mike, 1, 2, 3
ok



基准测试:

1> timer:tc(fout,benchmark_format_overhead,["hello ~s{name}, ~p{one}, ~p{two}, ~p{three}~n",[{one,1},{name,"Mike"},{three,3},{two,2}],100000]).
{421000,true}
= 4.21us per call

虽然我怀疑大部分开销是由于循环造成的,但使用一个循环调用函数会在

1> timer:tc(fout,benchmark_format_overhead,["hello ~s{name}, ~p{one}, ~p{two}, ~p{three}~n",[{one,1},{name,"Mike"},{three,3},{two,2}],1]).
{1,true}

如果有更好的 erlang 基准测试方法,请告诉我。



守则: (已根据Doug的建议进行了修改)

-module(fout).

-export([format/2,benchmark_format_overhead/3]).

benchmark_format_overhead(_,_,0)->
    true;
benchmark_format_overhead(OString,OList,Loops) ->
    {FString,FNames}=parse_string(OString,ONames),
    benchmark_format_overhead(OString,OList,Loops-1).

format(OString,ONames) ->
    {FString,FNames}=parse_string(OString,ONames),
    io:format(FString,FNames).

parse_string(FormatString,Names) ->
    {F,N}=parse_format(FormatString),
    {F,substitute_names(N,Names)}.

parse_format(FS) ->
    parse_format(FS,"",[],"").

parse_format("",FormatString,ParamList,"")->
    {lists:reverse(FormatString),lists:reverse(ParamList)};
parse_format([${|FS],FormatString,ParamList,"")->
    parse_name(FS,FormatString,ParamList,"");
parse_format([$}|_FS],FormatString,_,_) ->
    throw({'unmatched } found',lists:reverse(FormatString)});
parse_format([C|FS],FormatString,ParamList,"") ->
    parse_format(FS,[C|FormatString],ParamList,"").

parse_name([$}|FS],FormatString,ParamList,ParamName) ->
    parse_format(FS,FormatString,[list_to_atom(lists:reverse(ParamName))|ParamList],"");
parse_name([${|_FS],FormatString,_,_) ->
    throw({'additional { found',lists:reverse(FormatString)});
parse_name([C|FS],FormatString,ParamList,ParamName) ->
    parse_name(FS,FormatString,ParamList,[C|ParamName]).

substitute_names(Positioned,Values) ->
    lists:map(fun(CN)->
                        case lists:keysearch(CN,1,Values) of
                            false ->
                                throw({'named parameter not found',CN,Values});
                            {_,{_,V}} ->
                                V
                        end end,
              Positioned).

由于这是一个学习练习,我希望那些对 erlang 更有经验的人可以给我一些关于如何改进我的代码的提示。

干杯, 迈克

【问题讨论】:

  • 为了计时,如果太快无法测量,应该循环运行,取平均值作为时间
  • 请在标题中重新表述您的问题,如果我在搜索结果中看到它,我不知道您在问什么。

标签: erlang


【解决方案1】:

没有评论算法,或者使用适当的库函数......

我本来希望看到更多使用模式匹配和递归;例如 parse_character (不再折叠)可能会被替换为:

parse_in_format ([], FmtStr, ParmStrs, ParmName) -> {FmtStr, ParmStrs};
parse_in_format ([${ | Vr], FmtStr, ParmStrs, ParmName) -> parse_in_name (Vr, FmtStr, ParmStrs, ParmName);
parse_in_format ([$} | Vr], FmtStr, ParmStrs, ParmName) -> throw() % etc.
parse_in_format ([V | Vr], FmtStr, ParmStrs, ParmName) -> parse_in_format (Vr, [V | FmtStr], ParmStrs, ParmName).

parse_in_name ([], FmtStr, ParmStrs, ParmName) -> throw() % etc.
parse_in_name ([$} | Vr], FmtStr, ParmStrs, ParmName) -> parse_in_format (Vr, FmtStr, [list_to_atom(lists:reverse(ParmName))|ParmStrs], "");
parse_in_name ([${ | Vr], FmtStr, ParmStrs, ParmName) -> throw() % etc.
parse_in_name ([V | Vr], FmtStr, ParmStrs, ParmName) -> parse_in_name (Vr, FmtStr, ParmStrs, [V | ParmName]).

开始
parse_in_format (FormatStr,  [], [], "");

【讨论】:

  • 谢谢 Doug,我同意这种方法更好,我现在正在使用更少的库函数重写它。 Erlang 是我的第一个函数式语言,所以我仍在过渡到函数式思维。感谢您的评论!
【解决方案2】:

如果您不知道循环开销是否对您的代码有很大影响,您应该对其进行测量。很简单。

-define(COLOOPS, 1000000).

-export([call_overhead/1,measure_call_overhead/0, measure_call_overhead/1]).

% returns overhead in us 
measure_call_overhead() -> measure_call_overhead(?COLOOPS).
measure_call_overhead(N) -> element(1, timer:tc(?MODULE, call_overhead, [N]))/N.

call_overhead(0)->ok;
call_overhead(N)->
    ok=nop(),
    call_overhead(N-1).

nop()->ok.

在我的笔记本电脑上大约是 50ns。我认为这应该不会对您当前的代码产生太大影响。

另一种测量方法是直接使用统计信息(wall_clock)或统计信息(运行时间),它以毫秒为单位返回时间。好处是您不需要导出测量功能。这只是化妆品的改进。

【讨论】:

    【解决方案3】:

    除了 doug 的建议之外,我会避免在此处使用 atom_to_list/1 - 替代名称代码不需要它们,并且在运行时生成原子几乎总是一个坏主意。字符串可以很好地工作。

    parse_name([$}|FS],FormatString,ParamList,ParamName) ->
        parse_format(FS,FormatString,[lists:reverse(ParamName)|ParamList],"");
    parse_name([${|_FS],FormatString,_,_) ->
        throw({'additional { found',lists:reverse(FormatString)});
    parse_name([C|FS],FormatString,ParamList,ParamName) ->
        parse_name(FS,FormatString,ParamList,[C|ParamName]).
    

    我也会使用 proplists:get_value 而不是 lists:keysearch/3 - 当你有两个元素元组的列表 {Name, Value} 就像我们在这里所做的那样,使用 proplists 代码是要走的路 - 它仍然有点混乱因为我们需要 case 语句来检查缺失值,以便我们可以因更好的错误而崩溃。

    substitute_names(Positioned,Values) ->
        [ case proplists:get_value(Name, Values) of
              undefined -> erlang:exit({missing_parameter, Name});
              V -> V
          end
          || Name <- Positioned ].
    

    由于这是一个库,它应该是 io_lib 的替代品,而不是 io。这样我们就不必提供io 提供的所有备选方案(可选的IoDevice 参数等)。

    format(OString,ONames) ->
        {FString,FNames}=parse_string(OString,ONames),
        io_lib:format(FString,FNames).
    

    总而言之,可靠的代码。如果你愿意在 BSD 或类似的环境下授权它,我很想将它添加到我的 web 框架代码Ejango

    【讨论】:

    • 抱歉,回复延迟,过去两周我一直在度假。感谢您的详细建议和解释,它们非常有帮助。我很高兴在 BSD 下许可代码,因此请随意使用它(以及您希望的任何修改)。干杯,迈克
    猜你喜欢
    • 1970-01-01
    • 2021-03-12
    • 2020-05-05
    • 1970-01-01
    • 2023-04-03
    • 2023-04-03
    • 2016-03-20
    • 2016-05-01
    • 2020-01-06
    相关资源
    最近更新 更多