Вопрос
Архаел предложил в эта почта что написание новой процедуры форматирования для обработки именованных параметров может быть хорошим учебным упражнением.Итак, ради изучения языка я написал процедуру форматирования, которая обрабатывает именованные параметры.
Пример:
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 мкс.
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, смогут дать мне советы о том, как улучшить мой код.
Ура, Майк
Решение
Без комментариев ни по алгоритму, ни по использованию соответствующих библиотечных функций...
Я ожидал увидеть более широкое использование сопоставления с образцом и рекурсии;например, 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, [], [], "");
Другие советы
В дополнение к предложению Дуга, я бы не стал использовать 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 или что-то подобное, я бы очень хотел добавить его в код своего веб-фреймворка. Эджанго.
Если вы не знаете, сильно ли накладные расходы циклов влияют на ваш код, вам следует их измерить.Это просто.
-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.
На моем ноутбуке это около 50 нс.Я думаю, это не должно сильно влиять на ваш текущий код.
Другой способ измерения — напрямую использовать статистику (wall_lock) или статистику (время выполнения), которая возвращает время в мс.Преимущество в том, что вам не нужен экспорт измеренной функции.Это всего лишь косметическое улучшение.