file_sorter(3)

๐Ÿ“ฆ
version 29.0.6 ยท 3 versions
, in ubuntu stonking

๐ŸŽ€ NAME ๐ŸŽ€

file_sorter โ€” the file sorter module, bestie โœจ


๐Ÿ’– DESCRIPTION ๐Ÿ’–

okay so this module is literally your girl for sorting terms on files, merging files that are already sorted, and checking whether files are sorted. no cap. ๐Ÿซถ

Here's the vibe: chunks containing binary terms get read from a sequence of files, sorted internally in memory, written out onto temporary files, and then those temp files get merged so you end up with one sorted file as the output. ๐ŸŒˆ Merging is provided as an optimization โ€” it's faster when the files are already sorted โ€” but it always works to sort instead of merge, so don't stress. ๐Ÿ’…

On a file, a term is represented by a header and a binary. Two options decide the format of terms on files:

  • {header, HeaderLength} โ€” HeaderLength determines the number of bytes preceding each binary and containing the length of the binary in bytes. Defaults to 4. The order of the header bytes is defined like this: if B is a binary containing a header only, size Size of the binary is calculated as <<Size:HeaderLength/unit:8>> = B.
  • {format, Format} โ€” option Format determines the function that is applied to binaries to create the terms to be sorted. Defaults to binary_term, which is equivalent to fun binary_to_term/1. Value binary is equivalent to fun(X) -> X end, which means that the binaries are sorted as they are โ€” this is the fastest format ๐Ÿ”ฅ. If Format is term, io:read/2 is called to read terms. In that case, only the default value of option header is allowed. ๐Ÿ‘€

Option format also determines what is written to the sorted output file: if Format is term, then io:format/3 is called to write each term, otherwise the binary prefixed by a header is written. Notice that the binary written is the same binary that was read; the results of applying function Format are thrown away when the terms have been sorted. And fr fr, reading and writing terms using the io module is much slower than reading and writing binaries. ๐Ÿ˜ญ


๐Ÿ’… so, the other options (we love options)

  • {order, Order} โ€” the default is to sort terms in ascending order, but that can be changed by value descending or by specifying an ordering function Fun. An ordering function is antisymmetric, transitive, and total. Fun(A, B) is to return true if A comes before B in the ordering, otherwise false. An example of a typical ordering function is less than or equal to, =</2. Using an ordering function slows down the sort considerably ๐ŸŒ. Functions keysort, keymerge and keycheck do not accept ordering functions.
  • {unique, boolean()} โ€” when sorting or merging files, only the first of a sequence of terms that compare equal (==) is output if this option is set to true. Defaults to false, which implies that all terms that compare equal are output. When checking files for sortedness, a check that no pair of consecutive terms compares equal is done if this option is set to true. โœจ
  • {tmpdir, TempDirectory} โ€” the directory where temporary files are put can be chosen explicitly. The default, implied by value "", is to put temporary files in the same directory as the sorted output file. If output is a function (see below), the directory returned by file:get_cwd() is used instead. The names of temporary files are derived from the Erlang nodename (node/0), the process identifier of the current Erlang emulator (os:getpid()), and a unique integer (erlang:unique_integer([positive])). A typical name is fs_mynode@myhost_1763_4711.17, where 17 is a sequence number. Existing files are overwritten. Temporary files are deleted unless some uncaught EXIT signal occurs. ๐Ÿ’€
  • {compressed, boolean()} โ€” temporary files and the output file can be compressed. Defaults to false, which implies that written files are not compressed. Regardless of the value of option compressed, compressed files can always be read. Notice that reading and writing compressed files are significantly slower than reading and writing uncompressed files. ๐Ÿ˜ฎโ€๐Ÿ’จ
  • {size, Size} โ€” by default about 512*1024 bytes read from files are sorted internally. This option is rarely needed. ๐Ÿ™ˆ
  • {no_files, NoFiles} โ€” by default 16 files are merged at a time. This option is rarely needed. ๐Ÿ™ˆ

๐Ÿฆ‹ plot twist: functions instead of files

As an alternative to sorting files, a function of one argument can be specified as input. When called with argument read, the function is assumed to return either of the following:

  • end_of_input or {end_of_input, Value}} when there is no more input (Value is explained below). ๐ŸŒธ
  • {Objects, Fun}, where Objects is a list of binaries or terms depending on the format, and Fun is a new input function. โœจ

Any other value is immediately returned as the value of the current call to sort or keysort. Each input function is called exactly once. If an error occurs, the last function is called with argument close, the reply of which is ignored.

A function of one argument can also be specified as output. The results of sorting or merging the input is collected in a non-empty sequence of variable length lists of binaries or terms depending on the format. The output function is called with one list at a time, and is assumed to return a new output function. Any other return value is immediately returned as the value of the current call to the sort or merge function. Each output function is called exactly once. When some output function has been applied to all of the results, or an error occurs, the last function is called with argument close, and the reply is returned as the value of the current call to the sort or merge function. ๐Ÿ’…

If a function is specified as input and the last input function returns {end_of_input, Value}, the function specified as output is called with argument {value, Value}. This makes it easy to initiate the sequence of output functions with a value calculated by the input functions. iconic design tbh ๐Ÿงš


๐Ÿ“ example: sorting terms on a disk log

As an example, consider sorting the terms on a disk log file. A function that reads chunks from the disk log and returns a list of binaries is used as input. The results are collected in a list of terms.

sort(Log) ->
    {ok, _} = disk_log:open([{name,Log}, {mode,read_only}]),
    Input = input(Log, start),
    Output = output([]),
    Reply = file_sorter:sort(Input, Output, {format,term}),
    ok = disk_log:close(Log),
    Reply.

input(Log, Cont) ->
    fun(close) ->
            ok;
       (read) ->
            case disk_log:chunk(Log, Cont) of
                {error, Reason} ->
                    {error, Reason};
                {Cont2, Terms} ->
                    {Terms, input(Log, Cont2)};
                {Cont2, Terms, _Badbytes} ->
                    {Terms, input(Log, Cont2)};
                eof ->
                    end_of_input
            end
    end.

output(L) ->
    fun(close) ->
            lists:append(lists:reverse(L));
       (Terms) ->
            output([Terms | L])
    end.

For more examples of functions as input and output, see the end of the file_sorter module; the term format is implemented with functions. ๐Ÿซถ


๐Ÿ˜ญ the possible values of Reason when an error happens

  • bad_object, {bad_object, FileName} โ€” applying the format function failed for some binary, or the key(s) could not be extracted from some term.
  • {bad_term, FileName} โ€” io:read/2 failed to read some term.
  • {file_error, FileName, file:posix()} โ€” for an explanation of file:posix(), see file.
  • {premature_eof, FileName} โ€” end-of-file was encountered inside some binary term.

โœจ DATA TYPES โœจ

file_name() :: file:name().

file_names() :: [file:name()].

format() :: binary_term | term | binary | format_fun().

format_fun() :: fun((binary()) -> term()).

header_length() :: pos_integer().

i_command() :: read | close.

i_reply() ::
          end_of_input |
          {end_of_input, value()} |
          {[object()], infun()} |
          input_reply().

infun() :: fun((i_command()) -> i_reply()).

input() :: file_names() | infun().

input_reply() :: term().

key_pos() :: pos_integer() | [pos_integer()].

no_files() :: pos_integer().

o_command() :: {value, value()} | [object()] | close.

o_reply() :: outfun() | output_reply().

object() :: term() | binary().

option() ::
          {compressed, boolean()} |
          {header, header_length()} |
          {format, format()} |
          {no_files, no_files()} |
          {order, order()} |
          {size, size()} |
          {tmpdir, tmp_directory()} |
          {unique, boolean()}.

options() :: [option()] | option().

order() :: ascending | descending | order_fun().

order_fun() :: fun((term(), term()) -> boolean()).

outfun() :: fun((o_command()) -> o_reply()).

output() :: file_name() | outfun().

output_reply() :: term().

reason() ::
          bad_object |
          {bad_object, file_name()} |
          {bad_term, file_name()} |
          {file_error,
           file_name(),
           file:posix() | badarg | system_limit} |
          {premature_eof, file_name()}.

size() :: non_neg_integer().

tmp_directory() :: [] | file:name().

value() :: term().

and that's part 1 bestie!! ๐Ÿซถ part 2 is right there waiting, don't go anywhere โœจ

๐Ÿ’– FUNCTIONS ๐Ÿ’–

okay bestie, this is the part where we actually do stuff. every single one of these takes your files and either checks them, merges them, or full-on sorts them, and every single one of them has โœจopinionsโœจ about what happens if things go sideways. read the specs, they're not decorational.


๐ŸŒธ check(FileNames, Options) -> Reply ๐ŸŒธ

check(FileNames, Options) -> Reply
               when
                   FileNames :: file_names(),
                   Options :: options(),
                   Reply :: {ok, [Result]} | {error, reason()},
                   Result :: {FileName, TermPosition, term()},
                   FileName :: file_name(),
                   TermPosition :: pos_integer().

Checks files for sortedness. If a file is not sorted, the first out-of-order element gets handed back to you โ€” like, the moment something's off, it snitches. and heads up: the first term on a file has position 1, so we're counting from one, not zero, iykyk. ๐Ÿซถ


๐Ÿฆ‹ keycheck(KeyPos, FileNames, Options) -> Reply ๐Ÿฆ‹

keycheck(KeyPos, FileNames, Options) -> Reply
                   when
                       KeyPos :: key_pos(),
                       FileNames :: file_names(),
                       Options :: options(),
                       Reply :: {ok, [Result]} | {error, reason()},
                       Result :: {FileName, TermPosition, term()},
                       FileName :: file_name(),
                       TermPosition :: pos_integer().

Checks files for sortedness. Same energy as check, honestly โ€” if a file isn't sorted, you get the first out-of-order element back, and the first term on a file has position 1. the difference is you're passing a KeyPos so we're checking by key. ๐Ÿ’…


๐ŸŽ€ keymerge(KeyPos, FileNames, Output, Options) -> Reply ๐ŸŽ€

keymerge(KeyPos, FileNames, Output, Options) -> Reply
                   when
                       KeyPos :: key_pos(),
                       FileNames :: file_names(),
                       Output :: output(),
                       Options :: options(),
                       Reply :: ok | {error, reason()} | output_reply().

Merges tuples on files. Each input file is assumed to be sorted on key(s) โ€” so like, don't come crying to us if it's not, because that's the deal you made when you called this. ๐Ÿ™ˆ


๐Ÿ’œ keysort(KeyPos, FileName) -> Reply ๐Ÿ’œ

keysort(KeyPos, FileName) -> Reply
                  when
                      KeyPos :: key_pos(),
                      FileName :: file_name(),
                      Reply ::
                          ok |
                          {error, reason()} |
                          input_reply() |
                          output_reply().

Sorts tuples on files. that's the whole vibe. โœจ


๐ŸŒˆ keysort(KeyPos, Input, Output, Options) -> Reply ๐ŸŒˆ

keysort(KeyPos, Input, Output, Options) -> Reply
                  when
                      KeyPos :: key_pos(),
                      Input :: input(),
                      Output :: output(),
                      Options :: options(),
                      Reply ::
                          ok |
                          {error, reason()} |
                          input_reply() |
                          output_reply().

Sorts tuples on files. The sort happens on the element(s) you named in KeyPos. and here's the cute part: if two tuples compare equal (==) on one element, then the next element according to KeyPos gets compared โ€” it keeps walking down the list until it can make up its mind. and the sort is stable, periodt. ๐Ÿ‘‘


๐Ÿ’– merge(FileNames, Output, Options) -> Reply ๐Ÿ’–

merge(FileNames, Output, Options) -> Reply
                when
                    FileNames :: file_names(),
                    Output :: output(),
                    Options :: options(),
                    Reply :: ok | {error, reason()} | output_reply().

Merges terms on files. Each input file is assumed to be sorted. again: that's on you, bestie, we trust you and we do not double-check. ๐Ÿซถ


๐ŸŒธ sort(FileName) -> Reply ๐ŸŒธ

sort(FileName) -> Reply
               when
                   FileName :: file_name(),
                   Reply ::
                       ok |
                       {error, reason()} |
                       input_reply() |
                       output_reply().

Sorts terms on files. iconic behaviour, truly. โœจ


๐Ÿฆ‹ sort(Input, Output, Options) -> Reply ๐Ÿฆ‹

sort(Input, Output, Options) -> Reply
               when
                   Input :: input(),
                   Output :: output(),
                   Options :: options(),
                   Reply ::
                       ok |
                       {error, reason()} |
                       input_reply() |
                       output_reply().

Sorts terms on files. yes it's the same one-liner as above, and yes we're leaving it exactly as-is because the facts do not change just because the vibe does. ๐Ÿ’…


and that's a wrap on part two, bestie!! ๐ŸŽ‰ the whole thing came to you courtesy of Ericsson AB, running stdlib 8.0.4, and filed under file_sorter(3) โ€” go forth and sort responsibly, ily ๐Ÿซถ๐Ÿ’œ

๐Ÿ“ฆ where this page comes from