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
虽然我怀疑这种开销大部分是由于循环造成的,因为调用带有一个循环的函数会产生<为1us。
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中有更好的基准测试方法,请告诉我。
守则:
(根据道格的建议进行了修订)
-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更有经验的人可以给我提示如何改进我的代码。
干杯, 麦克
答案 0 :(得分:2)
没有评论算法,或使用适当的库函数......
我原本希望看到更多使用模式匹配和递归;例如,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, [], [], "");
答案 1 :(得分:2)
除了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。
答案 2 :(得分:1)
如果你不知道循环开销是否会影响你的代码,你应该测量它。这很简单。
-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)或统计数据(运行时),以ms为单位返回时间。好处是您不需要导出测量功能。这只是化妆品的改进。