Excellent write up, Stephane!
I will add that over the years, there have been many times (countless!) when developing/debugging involved a complex block and that turned out to be significant nuisance.
I can elaborate on the details, but once a block gets passed around, revising it on the fly breaks debug and continue.
Having a block comprise a single message send allows one to revise that method as and when needed.
From: vwnc-request@lists.cs.illinois.edu vwnc-request@lists.cs.illinois.edu On Behalf Of stephane.ducasse@free.fr
Sent: April 10, 2023 07:03
To: Any question about pharo is welcome pharo-users@lists.pharo.org
Cc: vwnc@lists.cs.illinois.edu
Subject: Re: [vwnc] [Pharo-users] Block evaluation with n+1 arguments
BTW to me when a block needs too many arguments it feels like that an object has to be born :)
With an object I can just sent or not a given extra argument.
Now I do not know enough your specific context but what I learned is that complex blocks are difficult to follow, manipulate…
so I keep block as simple as possible and else I create little objects.
This is a little lectures from a super cool forthcoming mooc
https://rmod-files.lille.inria.fr/DesignCoffeeClub/ForLearningLab/7-Lang-04-BlocksVsObjects.pdf <https://urldefense.com/v3/__https:/rmod-files.lille.inria.fr/DesignCoffeeClub/ForLearningLab/7-Lang-04-BlocksVsObjects.pdf__;!!DZ3fjg!7bRnSLpPY1OiW_EoNtvSDBVMfYN5owvLQjTs_FdNbynPLMw7hmdZCNifkawlO9Hz-NbVkGnnh37u3XIaEJEAHjFBHHppJxSeBgY$>
On 6 Apr 2023, at 15:28, Steffen Märcker <merkste@web.de mailto:merkste@web.de > wrote:
Hi!
I want to evaluate a block an argument 'arg1' and additional n arguments
given in an array 'args'. The following code does the trick:
block valueWithArguments: (Array with: arg1) , args.
Is there a way to do this without the overhead of creating a new Array?
(How) Can I add additional #value:value:[...] methods to BlockClosure that
evaluate the block with n arguments directly without falling back to
#valueWithArguments: ? If yes, what's the maximum?
Cheers!
Steffen
From the slides:
"What is the difference between a block and a simple object understanding
#value?"
The slides give the wrong answer. The right answer is that
The slides also say
" imagine passing a block around and want to accumulate information
◦ you can't!"
which is of course not really so. Consider
|b s|
b := [:state :argument | state nextPut: argument].
s := Summary new.
#(3 1 4 1 5 9) do: (b bindFirst: s).
s printOn: Transcript. Transcript cr.
That's actual working code and the output is
Summary(count: 6 min: 1 max: 9 mean: 3.833333333333333 sd:
2.994439290863428 skew: 1.044066600910494)
Far from "you can't", it's dead simple.
My library includes
#bindFirst:[bindSecond:[bindThird:]] #bindSecond:[bindThird:] #bindThird:
for binding any subset of the first three arguments of a block.
So far that's been enough.
The slides further say
"Adding behavior (i.e., offering another message) is impossible"
which is sort of true but misleading: you cannot add additional behaviour
to an individual block, but you CAN add additional behaviour to the class
it is an instance of. Which is how come I have #value:valueWithArguments:
#bindFirst: and its relatives, and a host of other combinators. It really
really
helps that I have a separate class for each block arity. For example, I
have
about 20 non-ANSI methods on TriadicBlock but about 50 on DyadicBlock.
The additional 30 on DyadicBlock would make no sense for TriadicBlock,
and smushing everything up into a single Block class would not work well.
That whole set of slides reads like a hymn of praise to Java nested classes,
which can do things blocks can't but on the other hand can't do things that
blocks can.
The issue we are discussing in this thread, of course, is not blocks as
such.
It isn't even blocks with many arguments. In
aBlock value: firstArgument valueWithArguments: restArguments
we have been given no reason to suppose that restArguments is long.
I still have a couple of stickers that Brian Marick gave me, reading
AN EXAMPLE WOULD BE GOOD ABOUT NOW.
I'd still like to know what the original poster wanted to DO with
value: firstArgument value: restArguments
^self valueWithArguments: {firstArgument} , restArguments
On Tue, 11 Apr 2023 at 04:25, Richard Sargent rsargent@5x5.on.ca wrote:
Excellent write up, Stephane!
I will add that over the years, there have been many times (countless!)
when developing/debugging involved a complex block and that turned out to
be significant nuisance.
I can elaborate on the details, but once a block gets passed around,
revising it on the fly breaks debug and continue.
Having a block comprise a single message send allows one to revise that
method as and when needed.
From: vwnc-request@lists.cs.illinois.edu <
vwnc-request@lists.cs.illinois.edu> *On Behalf Of *
stephane.ducasse@free.fr
Sent: April 10, 2023 07:03
To: Any question about pharo is welcome pharo-users@lists.pharo.org
Cc: vwnc@lists.cs.illinois.edu
Subject: Re: [vwnc] [Pharo-users] Block evaluation with n+1 arguments
BTW to me when a block needs too many arguments it feels like that an
object has to be born :)
With an object I can just sent or not a given extra argument.
Now I do not know enough your specific context but what I learned is that
complex blocks are difficult to follow, manipulate…
so I keep block as simple as possible and else I create little objects.
This is a little lectures from a super cool forthcoming mooc
https://rmod-files.lille.inria.fr/DesignCoffeeClub/ForLearningLab/7-Lang-04-BlocksVsObjects.pdf
https://urldefense.com/v3/__https:/rmod-files.lille.inria.fr/DesignCoffeeClub/ForLearningLab/7-Lang-04-BlocksVsObjects.pdf__;!!DZ3fjg!7bRnSLpPY1OiW_EoNtvSDBVMfYN5owvLQjTs_FdNbynPLMw7hmdZCNifkawlO9Hz-NbVkGnnh37u3XIaEJEAHjFBHHppJxSeBgY$
On 6 Apr 2023, at 15:28, Steffen Märcker merkste@web.de wrote:
Hi!
I want to evaluate a block an argument 'arg1' and additional n arguments
given in an array 'args'. The following code does the trick:
block valueWithArguments: (Array with: arg1) , args.
Is there a way to do this without the overhead of creating a new Array?
(How) Can I add additional #value:value:[...] methods to BlockClosure that
evaluate the block with n arguments directly without falling back to
#valueWithArguments: ? If yes, what's the maximum?
Cheers!
Steffen
Hi!
First, thanks for your engaging answers Richard, Stephane and the others!
The objective is to avoid unnecessary object creation in a tight loop that
interfaces between a value source and a block that processes the values.
The source object returns multiple values as a tuple (for good reasons).
The block processes theses values but needs another argument (at the
first place).
We do not know the number of values at compile time but know that they
match the arity of the block. Something like this (though more involved in
practice):
(1 to: 1000) do: [:i | | args |
args := source compute: i.
block valueWithArguments: {i} , args ]
Since prepending the tuple with the first argument and then sending
#valueWithArguments: creates an intermediate Array, I wonder whether we can
avoid (some of) that overhead in the loop without changing this structure.
Note, "{i}, args" is only for illustration and creates an additional third
array as Steve already pointed out.
To sum up the discussion so far:
Did I miss something?
Kind regards,
Steffen
Steffen,
I think the trouble you are seeing comes from trying to use a block in a way that is inappropriate.
A Block provides an anonymous function, but why use a Block that makes it so much more complicated?
Perhaps, you should reconsider an approach like:
(1 to: 1000) do: [:i | | data |
data:= source compute: i.
MyProcessor handleSourceData: data index: i ]
That allows you to factor out the block code into clean and intention revealing code, and eliminates the need to compose the arguments into a form compatible with Block invocation.
-----Original Message-----
From: Steffen Märcker merkste@web.de
Sent: April 11, 2023 09:44
To: Any question about pharo is welcome pharo-users@lists.pharo.org
Cc: vwnc@lists.cs.illinois.edu
Subject: [Pharo-users] Re: [vwnc] Block evaluation with n+1 arguments
Hi!
First, thanks for your engaging answers Richard, Stephane and the others!
The objective is to avoid unnecessary object creation in a tight loop that interfaces between a value source and a block that processes the values.
The source object returns multiple values as a tuple (for good reasons).
The block processes theses values but needs another argument (at the first place).
We do not know the number of values at compile time but know that they match the arity of the block. Something like this (though more involved in
practice):
(1 to: 1000) do: [:i | | args |
args := source compute: i.
block valueWithArguments: {i} , args ]
Since prepending the tuple with the first argument and then sending
#valueWithArguments: creates an intermediate Array, I wonder whether we can avoid (some of) that overhead in the loop without changing this structure.
Note, "{i}, args" is only for illustration and creates an additional third array as Steve already pointed out.
To sum up the discussion so far:
Did I miss something?
Kind regards,
Steffen
You say
<quote>
On the first point, we have to take your word for it.
It's not clear why you could not pass an n+1-element array to
the source method and have it fill in elements after the first
rather than having it allocate a new array. If you are concerned
about object allocation in a tight loop this would be a good
place to start.
argArray := Array new: block argumentCount.
...
source compute: i into: argArray.
argArray at: 1 put: i.
block valueWithArguments: argArray.
If for some reason it is utterly impossible to modify
'source compute: i' in this way, we can still use the
technique of allocating an array once for the whole loop.
argArray := Array new: block argumentCount.
...
argArray at: 1 put: i;
replaceFrom: 2 to: argArray size with: (source compute: i).
block valueWithArguments: argArray.
This seems like the smallest possible change to your code.
You still have the overhead of copying from one array to another
-- which is why I prefer modifying #compute: -- but you do not
have the overhead of allocating an array per iteration.
On the second point, right there you have the assumption that is
limiting your vision.
You are viewing the problem as "pass an extra first argument to
the block" when you should frame it as "ensure that the block
knows the value of i SOMEHOW". Presumably these blocks are
generated by code written by you.
So let's start with
Someclass
methods for: 'generating blocks'
blockFor: aSituation
^[:x0 :x1 ... :xn | ....]
block := Someclass blockFor: theSourceSituation.
(1 to: 1000) do: [:i | | args |
args := source compute: i.
block valueWithArguments: {i} , args
So now we change it to
Someclass
methods for: 'generating blocks'
blockFor: aSituation sharing: stateObject
^[:x1 ... :xn | |x0|
x0 := stateObject contents.
.....]
ref := Ref with: 0.
block := Someclass blockFor: theSourceSituation sharing: ref.
1 to: 1000 do: [:i | |args|
ref contents: i.
args := source compute: i.
block valueWithArguments: args].
Ref is an actual class in my library modelled on the Pop-2 and SML
types of the same name. It's not important. What is important is
that information can be supplied to a block through a shared object
as well as through a parameter.
I am a little bit twitchy about the 'coincidence' of the size of
#compute:'s result and the argument count of the block. Why not
pass the block to #compute: so that there never is any array in
the first place?
compute: index
... ^{e1. ... en} ...
=>
compute: index thenDo: aBlock
... ^aBlock value: e1 ... value: en ...
ref := Ref with: 0.
block := Someclass blockFor: theSourceSituation sharing: ref.
1 to: 1000 do: [:i | |args|
ref contents: i.
source compute: i thenDo: block].
Now there are even fewer arrays being allocated and no use of
#valueWithArguments: in any guise.
The problem is NOT, as some commentators apparently think, that
you are using a block. The problem is that having thought of
one way to wire things up -- not an unreasonable way, in fact --
you concentrated on making that way faster instead of looking
for other ways to do it.
We have several idioms here:
Reuse Object (convert an allocation per iteration to an allocation
per loop by reinitialising a object instead of allocating a new one)
Communicate Through Shared Microstate (communicate information between
a method and a block or object through a 'microstate' object created
by the method and passed when the block or object is created)
Multiple Values by Callback (instead of 'returning' multiple values in
a data structure, pass a block to receive those values as parameters).
On Wed, 12 Apr 2023 at 04:44, Steffen Märcker merkste@web.de wrote:
Hi!
First, thanks for your engaging answers Richard, Stephane and the others!
The objective is to avoid unnecessary object creation in a tight loop that
interfaces between a value source and a block that processes the values.
The source object returns multiple values as a tuple (for good reasons).
The block processes theses values but needs another argument (at the
first place).
We do not know the number of values at compile time but know that they
match the arity of the block. Something like this (though more involved in
practice):
(1 to: 1000) do: [:i | | args |
args := source compute: i.
block valueWithArguments: {i} , args ]
Since prepending the tuple with the first argument and then sending
#valueWithArguments: creates an intermediate Array, I wonder whether we can
avoid (some of) that overhead in the loop without changing this structure.
Note, "{i}, args" is only for illustration and creates an additional third
array as Steve already pointed out.
To sum up the discussion so far:
Did I miss something?
Kind regards,
Steffen
Dear Richard,
thanks for elaborating on your ideas. As I am still figuring out what works
best, I'll give them a try. Especially the approach to let the source deal
with passing the arguments to the block - though this requires more
changes.
The problem is NOT, as some commentators apparently think, that
you are using a block.
Indeed. A block provided (by the user) is actual the natural way in my
case.
The problem is that having thought of
one way to wire things up -- not an unreasonable way, in fact --
you concentrated on making that way faster instead of looking
for other ways to do it.
You're right. I first wanted to see how far I can get with this "direct"
approach before trying other techniques. :-D
All the best!
Steffen