Created
July 27, 2012 19:06
-
-
Save ferd/3189862 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-module(lazy). | |
-compile(export_all). | |
-include_lib("eunit/include/eunit.hrl"). | |
%% A lazy list is build from cells containing the value of their current | |
%% computation and a function that will return the next one. | |
%% Once a lazy list is over, the atom 'done' is returned. | |
-define(lcell(Term, Fun), {Term, Fun}). | |
-type lcell() :: {term(), fun(() -> tuple() | 'done' )} | |
| 'done'. | |
%% eval takes a lazy list and returns a normal list. | |
%% Using an infinite list means it goes into an infinite loop. | |
-spec eval(lcell()) -> list(). | |
eval(done) -> []; | |
eval(Cell) -> lists:reverse(eval(Cell, [])). | |
-spec eval(lcell(), list()) -> list(). | |
eval(done, Acc) -> Acc; | |
eval(?lcell(Term, Fn), Acc) -> eval(Fn(),[Term|Acc]). | |
%% convert/1 takes a list and returns a lazy one to be evaluated with | |
%% functions from this module. | |
-spec convert(list()) -> lcell(). | |
convert([]) -> done; | |
convert([H|T]) -> ?lcell(H,fun() -> convert(T) end). | |
conversion_test() -> | |
?assertEqual([1,2,3], eval(convert([1,2,3]))). | |
%%% Functions that do a final evaluation (ie.: return a single element). | |
%%% They do not require the use of 'eval' in order to work, but can't be | |
%%% coupled with other functions from this module. | |
%% all takes a function Pred and applies it to a lazy list. If the Pred | |
%% returns 'false' once, all/2 also returns false. Otherwise, it returns | |
%% 'true'. | |
-spec all(fun((any()) -> boolean()), lcell()) -> boolean(). | |
all(Pred, done) when is_function(Pred) -> | |
true; | |
all(Pred, ?lcell(Term,Fn)) when is_function(Pred) -> | |
case Pred(Term) of | |
true -> all(Pred, Fn()); | |
false -> false | |
end. | |
all_test_() -> | |
Pred = fun(X) -> X > 10 end, | |
[?_assertEqual(true, all(Pred,seq(11,20))), | |
?_assertEqual(false, all(Pred,seq(9,20))), | |
?_assertEqual(lists:all(Pred,[]), all(Pred, convert([])))]. | |
%% any takes a function Pred and applies it to a lazy list. If the Pred | |
%% returns 'true' once, any/2 also returns true. Otherwise (all false), | |
%% it returns 'false'. | |
%% -spec any(fun(any()) -> boolean()), lcell()) -> boolean(). | |
%% ^ that spec won't work because the function name clashes with the | |
%% dialyzer default 'any()' type. | |
any(Pred, done) when is_function(Pred) -> | |
false; | |
any(Pred, ?lcell(Term,Fn)) when is_function(Pred) -> | |
case Pred(Term) of | |
true -> true; | |
false -> any(Pred, Fn()) | |
end. | |
any_test_() -> | |
Pred = fun(X) -> X > 10 end, | |
[?_assertEqual(true, any(Pred,seq(11,20))), | |
?_assertEqual(true, any(Pred,seq(9,20))), | |
?_assertEqual(false, any(Pred,seq(1,3))), | |
?_assertEqual(lists:any(Pred,[]), any(Pred, convert([])))]. | |
%% fold will reduce a list from left to right like lists:foldl/3 would. | |
-spec fold(fun((any(),any()) -> any()), any(), lcell()) -> any(). | |
fold(_, Start, done) -> Start; | |
fold(F, Start, ?lcell(Term,Fn)) when is_function(F) -> | |
fold(F, F(Term,Start), Fn()). | |
fold_test_() -> | |
F = fun(X,Y) -> X*Y end, | |
[?_assertEqual(lists:foldl(F,1,lists:seq(1,100)), | |
fold(F,1,seq(1,100))), | |
?_assertEqual(lists:foldl(F,0,[1,2,3,4]), fold(F,0,seq(1,4))), | |
?_assertEqual(24, fold(F,1,seq(1,4)))]. | |
%% member returns 'true' if the element Elem is part of the lazy list and | |
%% 'false' if it's not. lists:member uses strict comparison (=:=) and so | |
%% does this one. | |
-spec member(any(),lcell()) -> boolean(). | |
member(_,done) -> | |
false; | |
member(Elem, ?lcell(Term,Fn)) -> | |
if Elem =:= Term -> true; | |
true -> member(Elem, Fn()) | |
end. | |
member_test_() -> | |
[?_assertEqual(true, member(1, seq(1,3))), | |
?_assertEqual(false, member(1, seq(2,3))), | |
?_assertEqual(lists:member(2.0,[1,2,3]), member(2.0,seq(1,3))), | |
?_assertEqual(lists:member(2.0,[1.0,2.0,3.0]), | |
member(2.0,convert([1.0,2.0,3.0])))]. | |
%% nth takes a lazy cell and will return the nth result from there. | |
%% the function is 1-indexed in order to be coherent with the rest of | |
%% Erlang. | |
-spec nth(non_neg_integer(), lcell()) -> any(). | |
nth(_,done) -> []; | |
nth(1,?lcell(Term,_)) -> Term; | |
nth(N,?lcell(_,Fn)) -> nth(N-1, Fn()). | |
nth_test_() -> | |
[?_assertEqual(2, nth(4,convert([3,7,9,2,6,3]))), | |
?_assertEqual(2, nth(4,convert([3,7,9,2]))), | |
?_assertEqual([], nth(4,convert([])))]. | |
%%% These functions can be combined together. | |
%% seq is the equivalent to lists:seq, with lazy cells | |
-spec seq(integer(), integer()) -> lcell(). | |
seq(From, To) -> seq(From,To,1). | |
%% same as seq/2, with a step mentioned | |
-spec seq(integer(), integer(), integer()) -> lcell(). | |
seq(From, To, Step) when is_integer(From), is_integer(To), is_integer(Step), | |
From < To, Step > 0 -> | |
?lcell(From, fun() -> seq(From+Step, To, Step) end); | |
seq(From, To, Step) when is_integer(From), is_integer(To), is_integer(Step), | |
To < From, Step < 0 -> | |
?lcell(From, fun() -> seq(From+Step, To, Step) end); | |
seq(From, To, Step) when is_integer(From), is_integer(To), is_integer(Step), | |
To == From -> | |
?lcell(From, fun() -> done end); | |
seq(From, To, Step) when is_integer(From), is_integer(To), is_integer(Step), | |
Step < 0, From =< To -> | |
done; | |
seq(From, To, Step) when is_integer(From), is_integer(To), is_integer(Step), | |
Step > 0, From >= To -> | |
done. | |
seq_test_() -> | |
[?_assertEqual([1,2,3,4], eval(seq(1,4))), | |
?_assertEqual([4,3,2,1], eval(seq(4,1,-1))), | |
?_assertEqual("ABCD", eval(seq($A,$D))), | |
?_assertEqual(lists:seq(40,230,12), eval(seq(40,230,12))), | |
?_assertEqual(lists:seq(1,10,2), eval(seq(1,10,2)))]. | |
%% sublist is exactly similar to lists:sublist, except in a lazy version | |
%% which needs to be evaluated by eval/1. Note that in the case of | |
%% sublist/2 and sublist/3, the List actually comes as the first argument: | |
%% this is done to be consistent with the lists module function. | |
-spec sublist(lcell(), non_neg_integer()) -> lcell(). | |
sublist(Cell, Len) -> sublist(Cell, 1, Len). | |
-spec sublist(lcell(), non_neg_integer(), non_neg_integer()) -> lcell(). | |
sublist(done, Start, Len) when is_integer(Start), is_integer(Len) -> | |
done; | |
sublist(?lcell(Term,_Fn), 1, 1) -> | |
?lcell(Term, fun() -> done end); | |
sublist(?lcell(Term, Fn), 1, Len) when is_integer(Len) -> | |
?lcell(Term, fun() -> sublist(Fn(), 1, Len-1) end); | |
sublist(?lcell(_, Fn), Start, Len) when is_integer(Start), is_integer(Len) -> | |
sublist(Fn(), Start-1, Len). | |
sublist_test_() -> | |
[?_assertEqual(lists:sublist([1,2,3,4],3), eval(sublist(seq(1,4),3))), | |
?_assertEqual(lists:sublist([1,2,3],4), eval(sublist(seq(1,3),4))), | |
?_assertEqual(lists:sublist([1,2,3,4,5],3,4), | |
eval(sublist(seq(1,5),3,4))), | |
?_assertEqual(lists:sublist(lists:seq(1,3),9), | |
eval(sublist(seq(1,3),9))), | |
?_assertEqual(lists:sublist([1,2,3,4,5,6],3,2), | |
eval(sublist(seq(1,6),3,2)))]. | |
%% map applies a function to each element of a list, returning a list. | |
-spec map(fun(), lcell()) -> lcell(). | |
map(F,done) when is_function(F) -> | |
done; | |
map(F,?lcell(Term,Fn)) when is_function(F) -> | |
?lcell(F(Term), fun() -> map(F, Fn()) end). | |
map_test_() -> | |
F = fun(X) -> X*2 end, | |
[?_assertEqual(lists:map(F,[1,2,3,4,5]), eval(map(F,seq(1,5)))), | |
?_assertEqual(lists:map(F,[]), eval(map(F,convert([]))))]. | |
%% filter returns only elements of the lazy list which return true when | |
%% passed to Pred | |
-spec filter(fun((any()) -> boolean()), lcell()) -> lcell(). | |
filter(_,done) -> done; | |
filter(F,?lcell(Term, Fn)) when is_function(F) -> | |
case F(Term) of | |
true -> ?lcell(Term, fun() -> filter(F, Fn()) end); | |
false -> filter(F,Fn()) | |
end. | |
filter_test_() -> | |
F = fun(X) -> X rem 2 =:= 0 end, | |
[?_assertEqual(lists:filter(F,lists:seq(1,100)), | |
eval(filter(F,seq(1,100)))), | |
?_assertEqual(lists:filter(F,[]), eval(filter(F,convert([]))))]. | |
%% append/2 takes two lists and appends the second one to the first one. | |
-spec append(lcell(),lcell()) -> lcell(). | |
append(done,done) -> done; | |
append(done,?lcell(Term,Fn)) -> | |
?lcell(Term, fun() -> append(done, Fn()) end); | |
append(?lcell(Term,Fn),Cell) -> | |
?lcell(Term, fun() -> append(Fn(),Cell) end). | |
append_test_() -> | |
[?_assertEqual(lists:append([1,2,3],[4,5,6]), | |
eval(append(seq(1,3),seq(4,6)))), | |
?_assertEqual(lists:append([],[]), | |
eval(append(convert([]),convert([])))), | |
?_assertEqual(lists:append([],[1,2,3]), | |
eval(append(convert([]),seq(1,3)))), | |
?_assertEqual(lists:append([1,2,3],[]), | |
eval(append(seq(1,3),convert([]))))]. | |
%% takewhile keeps building the list until the predicate returns false. | |
-spec takewhile(fun((any()) -> boolean()), lcell()) -> lcell(). | |
takewhile(Pred, done) when is_function(Pred) -> done; | |
takewhile(Pred, ?lcell(Term, Fn)) when is_function(Pred) -> | |
case Pred(Term) of | |
true -> ?lcell(Term, fun() -> takewhile(Pred,Fn()) end); | |
false -> done | |
end. | |
takewhile_test_() -> | |
Pred = fun(X) -> X < 5 end, | |
[?_assertEqual(lists:takewhile(Pred, lists:seq(1,10)), | |
eval(takewhile(Pred, seq(1,10)))), | |
?_assertEqual(lists:takewhile(Pred, lists:seq(10,15)), | |
eval(takewhile(Pred, seq(10,15))))]. | |
%% dropwhile keeps ignoring elements of a list until the predicate returns | |
%% true. It's technically not lazy and will do all the computations at once | |
%% but the list returned after will be. | |
-spec dropwhile(fun((any()) -> boolean()), lcell()) -> lcell(). | |
dropwhile(Pred, done) when is_function(Pred) -> done; | |
dropwhile(Pred, ?lcell(Term, Fn)) when is_function(Pred) -> | |
case Pred(Term) of | |
true -> dropwhile(Pred, Fn()); | |
false -> ?lcell(Term, fun() -> dropwhile(Pred,Fn()) end) | |
end. | |
dropwhile_test_() -> | |
Pred = fun(X) -> X < 5 end, | |
[?_assertEqual(lists:dropwhile(Pred, lists:seq(1,10)), | |
eval(dropwhile(Pred, seq(1,10)))), | |
?_assertEqual(lists:dropwhile(Pred, lists:seq(1,3)), | |
eval(dropwhile(Pred, seq(1,3))))]. | |
%% zip/2 takes two lists Xs and Ys and makes a list of tuples of the form | |
%% [{Xn,Yn}]. The implementation differs from the standard lists one as | |
%% the lists one fails if the count of list elements is differnet in the | |
%% first and second one. This one just stops whenever one of the lists is | |
%% over. | |
-spec zip(lcell(), lcell()) -> lcell(). | |
zip(done,_) -> done; | |
zip(_,done) -> done; | |
zip(?lcell(TermX,FnX), ?lcell(TermY,FnY)) -> | |
?lcell({TermX,TermY}, fun() -> zip(FnX(),FnY()) end). | |
zip_test_() -> | |
[?_assertEqual([{1,5},{2,6},{3,7},{4,8}], eval(zip(seq(1,4),seq(5,8)))), | |
?_assertEqual([{1,10},{2,11}], eval(zip(seq(1,2),seq(10,1000)))), | |
?_assertEqual([], eval(zip(convert([]),seq(1,1000))))]. | |
%% zipwith/3 takes a function and two lists Xs and Ys and combines them | |
%% using the function. | |
%% As an example, if the combining function is fun(X,Y) -> {X,Y} end, the | |
%% zipwith function behaves exactly as zip/2. A different behavior could be | |
%% demonstrated with the combining function fun(X,Y) -> X+Y end, which, | |
%% coupled with the lists [1,2,3] and [4,5,6,7] would give [5,7,9]. | |
-spec zipwith(fun( (any(),any()) -> any() ), lcell(), lcell()) -> lcell(). | |
zipwith(F,done,_) when is_function(F) -> done; | |
zipwith(F,_,done) when is_function(F) -> done; | |
zipwith(F, ?lcell(TermX, FnX), ?lcell(TermY, FnY)) when is_function(F) -> | |
?lcell(F(TermX, TermY), fun() -> zipwith(F, FnX(), FnY()) end). | |
zipwith_test_() -> | |
Fn = fun(X,Y) -> {X, Y} end, | |
[?_assertEqual(eval(zip(seq(1,4), seq(5,8))), | |
eval(zipwith(Fn, seq(1,4), seq(5,8)))), | |
?_assertEqual(eval(zip(seq(1,3), seq(1,1000))), | |
eval(zipwith(Fn, seq(1,3), seq(1,1000)))), | |
?_assertEqual([2,4,6,8], | |
eval(zipwith(fun(X,Y) -> X+Y end, seq(1,4), seq(1,10))))]. | |
%% builder lets you use a function to produce the next element your lazy | |
%% list will contain, may it be from a stream or even raw input from the | |
%% shell. | |
%% | |
%% It will however be slower than other functions because of its need to | |
%% do checks through a function rather than pattern match. | |
-spec builder(any(), fun((any()) -> boolean()), fun()) -> lcell(). | |
builder(Term, Stop, Step) -> | |
case Stop(Term) of | |
true -> done; | |
false -> ?lcell(Term, fun() -> builder(Step(Term), Stop, Step) end) | |
end. | |
builder_test_() -> | |
[?_assertEqual(lists:seq(1,100,4), | |
eval(builder(1,fun(X)-> X>=100 end, fun(X) -> X+4 end)))]. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment