However, I want it all to be lazy. That is, nothing should happen until foldl starts pulling items, and then the work should happen just in time. None of the intermediary lists nor the final list should ever be materialised.
How would you change the process predicate to make this sort of lazy processing possible? I am aware of lazy lists but it is not obvious to me how to convert process to use them.
Please assume process outputs items in the same order as the input (although that is not shown).
Last thing I need is to be able to chain things which do not return 1-for-1 lists. For example, say at the top I insert [1,2,3] and the next process creates [4,5,6,7,8,9] from that input. I dont see an obvious way to do that with lazy lists or otherwise.
A lazy list is free to do what ever it wants to
as long as it adheres to the protocol. The protocol
I showed is a little weak, since it cannot detect
the end of a list. It is only call(Lazy, Head, TailLazy),
basically assuming infinite lists. But you can already do
funny stuff with it, like duplicate each element in place:
So the lazy list can have state besides mapping elements:
?- take(6, dup(lazy([a,b,c])), X).
X = [a, a, b, b, c, c] .
Chaining f1 |> f2 is written as f2(f1). So the last processor
is the outer functor of the argument to take/3, and the first
processor is the inner functor of the argument.
I’ve got what i need working with lazy_list except its useless for the purpose since its about 20x slower than a fully materialized version. I had the same experience when using lazy_findall/3.
This way you show is slightly unusual. It is impossible to tell without a concrete example, of course, so I am only guessing; still, the usual way would be more like:
You should play around and see that reversing the order of the clauses makes no difference; and that in the specific case of char_code/2, removing the cut makes not difference. For a non-deterministic predicate “do_work” it will of course make a difference, you will get all possible combinations. Here is a silly example just to demonstrate:
About lazy evaluation. The Prolog alternative to lazy evaluation is backtracking. You can get very far with it. The huge advantage is that it is very fast and needs far less memory; you never need to materialize any lists. The huge disadvantage is that you lose solutions on backtracking. If you can provide a concrete example, it might be easier to judge if you need lists or lazy lists.
I assume you know that SWI Prolog lazy lists are implemented as constraints using attributed variables, and that unification of attributed variables is going to be orders of magnitude slower that unification that doesn’t involve attributed variables.
To make it concrete, the attr_unify_hook that gets invoked whenever a variable with a lazy_list attribute is unified:
Note that several of the predicates involving lazy lists have a chunk size argument or option. That reduces the number of wakeups. Still, there is quite a bit of work involved in extending the lazy list.
Lazy Lists via Attributed Variables aka Constraints as in the
corresponding lazy list library from SWI-Prolog are different from the
direct lazy lists I provided. They do two things, namely a) lazy evaluation
(call-by-name) and b) memoization, and not only one thing lazy evaluation.
Which means they are more memory hungry and require a smarter garbage
collection, and end-user Prolog code that has no choice points that blocks garbage
collection. This means procedural Prolog code with cuts and less declarative Prolog
code, in case one is really up to minimize the memory foot print. Also I don’t know
whether the library is really developed and tested that it doesn’t leave garbage.
The difference is seen here.
/* direct lazy lists, no memoization */
?- Lazy = random, take(3, random, X), take(3, random, Y).
Lazy = random,
X = [51, 76, 72],
Y = [73, 26, 21] .
/* SWI-Prolog lazy lists, memoization */
?- lazy_list(random2, Lazy), take2(3, X, Lazy, _), take2(3, Y, Lazy, _).
Lazy = [15, 54, 30|_A],
X = Y, Y = [15, 54, 30],
lazy_list(user:random2, _A) .
Haskell does the same for its delayed expressions, namely lazy evaluation
and memoization, the later to avoid repeated and hence costly re-evaluation
of the delayed expression. The memoization is garbage collected when the
The lazy_list abstraction is practical. It’d be great if you could write something using a similar contract which can be used to pipe lists into processes and processes into each other via lists.
I’m not really sure how memoisation fits into all this, but I think its strange that a lazy list ends up exploding the memory due to overhead.