io的意外行为:在Erlang中的fread

时间:2009-01-23 15:30:42

标签: erlang

这是一个Erlang问题。

我遇到了一些意想不到的行为:io:fread。

我想知道是否有人可以检查我使用io的方式是否有问题:fread或者io中是否有错误:fread。

我有一个文本文件,其中包含“数字三角形”,如下所示:

59
73 41
52 40 09
26 53 06 34
10 51 87 86 81
61 95 66 57 25 68
90 81 80 38 92 67 73
30 28 51 76 81 18 75 44
...

每对数字之间只有一个空格,每一行以回车符换行结束。

我使用以下Erlang程序将此文件读入列表。

-module(euler67).
-author('Cayle Spandon').

-export([solve/0]).

solve() ->
    {ok, File} = file:open("triangle.txt", [read]),
    Data = read_file(File),
    ok = file:close(File),
    Data.

read_file(File) ->
    read_file(File, []).

read_file(File, Data) ->
    case io:fread(File, "", "~d") of
        {ok, [N]} -> 
            read_file(File, [N | Data]);
        eof ->
            lists:reverse(Data)
    end.

该程序的输出是:

(erlide@cayle-spandons-computer.local)30> euler67:solve().
[59,73,41,52,40,9,26,53,6,3410,51,87,86,8161,95,66,57,25,
 6890,81,80,38,92,67,7330,28,51,76,81|...]

注意如何将第四行(34)的最后一个数字和第五行(10)的第一个数字合并为一个数字3410。

当我使用“od”转储文本文件时,这些行没有什么特别之处;他们就像任何其他行一样以cr-nl结尾:

> od -t a triangle.txt
0000000    5   9  cr  nl   7   3  sp   4   1  cr  nl   5   2  sp   4   0
0000020   sp   0   9  cr  nl   2   6  sp   5   3  sp   0   6  sp   3   4
0000040   cr  nl   1   0  sp   5   1  sp   8   7  sp   8   6  sp   8   1
0000060   cr  nl   6   1  sp   9   5  sp   6   6  sp   5   7  sp   2   5
0000100   sp   6   8  cr  nl   9   0  sp   8   1  sp   8   0  sp   3   8
0000120   sp   9   2  sp   6   7  sp   7   3  cr  nl   3   0  sp   2   8
0000140   sp   5   1  sp   7   6  sp   8   1  sp   1   8  sp   7   5  sp
0000160    4   4  cr  nl   8   4  sp   1   4  sp   9   5  sp   8   7  sp

一个有趣的观察结果是,问题发生的一些数字恰好出现在文本文件中的16字节边界上(但不是全部,例如6890)。

3 个答案:

答案 0 :(得分:9)

我将继续使用Erlang中的一个bug,这也是一个奇怪的错误。将格式字符串更改为“~2s”会产生同样奇怪的结果:

["59","73","4","15","2","40","0","92","6","53","0","6","34",
 "10","5","1","87","8","6","81","61","9","5","66","5","7",
 "25","6",
 [...]|...]

因此,为了计算,它似乎将换行符计为常规字符,但在生成输出时则不然。懒散无比。

一周的Erlang编程,我已经深入研究了源代码。这对我来说可能是一个新纪录......

修改

我进一步调查证实这是一个错误。调用fread中使用的内部方法之一:

> io_lib_fread:fread([], "12 13\n14 15 16\n17 18 19 20\n", "~d").           
{done,{ok,"\f"}," 1314 15 16\n17 18 19 20\n"}

基本上,如果要读取多个值,那么换行符,第一个换行符将在字符串的“仍待读取”部分中被吃掉。其他测试表明,如果你在前面添加一个空格就可以了,如果你用一个换行符引导该字符串,它会要求更多。

我要深究这一点,gosh-darn-it ...(笑)没有那么多代码可以通过,而且没有太多代码专门处理换行符,所以它不应该需要很长时间才能缩小范围并修复它。

修改^ 2

哈哈!得到了一点点。

这是你想要的stdlib的补丁(记得重新编译并将新的beam文件放在旧的顶部):

--- ../erlang/erlang-12.b.3-dfsg/lib/stdlib/src/io_lib_fread.erl
+++ ./io_lib_fread.erl
@@ -35,9 +35,9 @@
     fread_collect(MoreChars, [], Rest, RestFormat, N, Inputs).

 fread_collect([$\r|More], Stack, Rest, RestFormat, N, Inputs) ->
-    fread(RestFormat, Rest ++ reverse(Stack), N, Inputs, More);
+    fread(RestFormat, Rest ++ reverse(Stack), N, Inputs, [$\r|More]);
 fread_collect([$\n|More], Stack, Rest, RestFormat, N, Inputs) ->
-    fread(RestFormat, Rest ++ reverse(Stack), N, Inputs, More);
+    fread(RestFormat, Rest ++ reverse(Stack), N, Inputs, [$\n|More]);
 fread_collect([C|More], Stack, Rest, RestFormat, N, Inputs) ->
     fread_collect(More, [C|Stack], Rest, RestFormat, N, Inputs);
 fread_collect([], Stack, Rest, RestFormat, N, Inputs) ->
@@ -55,8 +55,8 @@
                eof ->
                    fread(RestFormat,eof,N,Inputs,eof);
                _ ->
-                   %% Don't forget to count the newline.
-                   {more,{More,RestFormat,N+1,Inputs}}
+                   %% Don't forget to strip and count the newline.
+                   {more,{tl(More),RestFormat,N+1,Inputs}}
            end;
        Other ->                                %An error has occurred
            {done,Other,More}

现在将我的补丁提交给erlang-patches,并获得由此产生的名声和荣耀......

答案 1 :(得分:1)

除了它似乎是其中一个erlang库中的错误之外,我认为你可以(非常)轻松地避开这个问题。

鉴于您的文件是面向行的,我认为最佳做法是您逐行处理它。

考虑以下结构。它适用于未修补的erlang,因为它使用延迟评估,它可以处理任意长度的文件,而不必先将所有文件读入内存。该模块包含一个应用于每一行的函数示例 - 将一行整数的文本表示形式转换为整数列表。


-module(liner).
-author("Harro Verkouter").
-export([liner/2, integerize/0, lazyfile/1]).

% Applies a function to all lines of the file
% before reducing (foldl).
liner(File, Fun) ->
    lists:foldl(fun(X, Acc) -> Acc++Fun(X) end, [], lazyfile(File)).

% Reads the lines of a file in a lazy fashion
lazyfile(File) ->
    {ok, Fd} = file:open(File, [read]),
    lazylines(Fd).
% Actually, this one does the lazy read ;)
lazylines(Fd) ->
    case io:get_line(Fd, "") of
        eof -> file:close(Fd), [];
        {error, Reason} ->
            file:close(Fd), exit(Reason);
        L ->
            [L|lazylines(Fd)]
    end.

% Take a line of space separated integers (string) and transform
% them into a list of integers
integerize() ->
    fun(X) ->
        lists:map(fun(Y) -> list_to_integer(Y) end,
                string:tokens(X, " \n")) end.


Example usage:
Eshell V5.6.5  (abort with ^G)
1> c(liner).
{ok,liner}
2> liner:liner("triangle.txt", liner:integerize()).
[59,73,41,52,40,9,26,53,6,34,10,51,87,86,81,61,95,66,57,25,
 68,90,81,80,38,92,67,73,30|...]

And as a bonus, you can easily fold over the lines of any (lineoriented) file w/o running out of memory :)

6> lists:foldl( fun(X, Acc) -> 
6>                  io:format("~.2w: ~s", [Acc,X]), Acc+1
6>                  end,
6>              1,  
6>              liner:lazyfile("triangle.txt")).                                        
 1: 59
 2: 73 41
 3: 52 40 09
 4: 26 53 06 34
 5: 10 51 87 86 81
 6: 61 95 66 57 25 68
 7: 90 81 80 38 92 67 73
 8: 30 28 51 76 81 18 75 44

干杯, 小时。

答案 2 :(得分:0)

我注意到有多个实例合并了两个数字,并且它似乎位于从第四行开始的每一行的行边界处。

我发现如果你在第五行开始的每一行的开头添加一个空白字符,那就是:

59
73 41
52 40 09
26 53 06 34
 10 51 87 86 81
 61 95 66 57 25 68
 90 81 80 38 92 67 73
 30 28 51 76 81 18 75 44
...

数字得到正确解析:

39> euler67:solve().
[59,73,41,52,40,9,26,53,6,34,10,51,87,86,81,61,95,66,57,25,
 68,90,81,80,38,92,67,73,30|...]

如果您将空白添加到前四行的开头也是有效的。

这更像是一种解决方法,而不是一种实际解决方案,但它确实有效。我想弄清楚如何为io:fread设置格式字符串,以便我们不必这样做。

<强>更新 这是一种不会强制您更改文件的解决方法。这假定所有数字都是两个字符(&lt; 100):

read_file(File, Data) ->
case io:fread(File, "", "~d") of
    {ok, [N] } -> 
        if
            N > 100 ->
                First = N div 100,
                Second = N - (First * 100),
                read_file(File, [First , Second | Data]);

            true ->
                read_file(File, [N | Data])
        end;
    eof ->
        lists:reverse(Data)
end.

基本上,代码会捕获换行符中两个串联的任何数字,并将它们分成两行。

同样,这是一个暗示io中可能存在错误的kludge:fread,但是应该这样做。

再次更新上述内容仅适用于两位数输入,但由于该示例将所有数字(甚至是<10)数字打包成两位数格式,因此该示例适用于此示例