Chaining keyword messages
Asked Answered
N

3

10

Let's suppose that I have an object, x, which can accept any one of the selectors, s1, s2, ..., sn. Let's further suppose that the result of any one of these selectors operating on the object is an updated object of the same type. I could then "chain" these selectors (as long as they are unary messages) as I wish, such as:

x s1 s2 ... sn

This will take the result of x s1 and apply selector s2, then apply selector s3 to that result, and so on. I would want to apply one or more of these selectors in some order for various results:

x s8 s2

In Smalltalk I can do this if the selectors are unary messages. However, if my selectors are keyword messages, I can no longer do this. If x individually accepts selectors, s1:, s2:, ..., sn:, then the following doesn't work:

x s1: a1 s2: a2 ... sn: an

There is the ; operator:

x s1: a1 ; s2: a2 ; ... ; sn: an

But using the cascading: each stage modifies the original x along the way, and I do not wish to modify x in this case.

To chain keyword messages, I think I'm left using the following syntax with parentheses:

(...(((x s1: a1) s2: a2) ... sn: an)

Which makes me feel like I'm programming in LISP if I have 3 or more keywrod messages. A specific example of this could be a multidimensional array. If foo is a 3 dimensional array, and you wanted to access an object at location 2,3,5 in the array, I think it would look like:

(((foo at: 2) at: 3) at: 5) some_object_selectors

That's a trivial example, of course, but illustrates the case. One might have other kinds of embedded objects, or other chain of consecutive object operations where you are interested in the end result.

Is there a more syntactically appealing way to do this in Smalltalk? I'm supposing there isn't another operator, perhaps a cousin to the ; operator (say we use & for example), which would chain them, such as:

x s1: a1 & s2: a2 & ... & sn: an

Since I would like to apply the selectors in any, or nearly any, desired order (for possibly different results), the selector form, s1:s2:s3:... is too confining. Also, this gives a facility which already exists in other languages, such as Ruby, where it would be equivalently expressed as:

x.s1(a1).s2(a2)...sn(an)

Lacking a special operator, an alternative could be to pass an array of selector-argument pairs, or perhaps a lookup table of selector-argument pairs. The lookup table requires setup for passing literals (it has to be created and populated), which makes me lean toward an array since I can simply write it as:

x { {s1. a1}. {s2. a2}. ... {sn. an} }

This is still a bit clunky, and I'm not so sure this is any more elegant than just using all the parentheses. I'm afraid my question may be at least partly subjective, but I am interested knowing what the best practice might be, and whether an operator exists that I'm not aware of which may help, or whether one is being entertained by a Smalltalk standards body.

Naiad answered 8/8, 2015 at 19:8 Comment(3)
Good question, also sometimes expressed as message pipe or pipeline - your not the first one to miss such feature, it's regularly discussed in mailing list. I recommend the exellent blog blog.3plus4.org/2007/08/30/message-chainsPhonography
@Phonography yeah, it looks like they beat me to the question 8 years ago. :)Naiad
FWIW I found your question pretty clear and am struck that Smalltalk doesn't have such a language feature. When it's available for unary and binary messages but not keyword messages, it seems like a language issue.Staley
I
1

Normally we give this object x a subject to operate on, and then ask for this changed subject when we're finished with it. Copying is rather inefficient.

Object subclass: #Foo
    instanceVariableNames: 'subject'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Try'

order: y
    self subject order: y

select: z
    self subject select: z

subject
    ^subject ifNil: [ subject := FooSubject new ]

where: x
    self subject where: x
Italicize answered 8/8, 2015 at 20:38 Comment(15)
I understand. I was looking for a way to get the same effect as x.s1(a1).s2(a2)...sn(an) as is done in Ruby, for example, without having to resort to all the parentheses. The subject x is something I might want to apply different operations on at different times with different sets of selectors. So I don't wish to change it. If it changes, then I have to make copies of it anyway in that case so I have it in its original form again. If I want to save an object that has some number of operations done on it, I'd do it by, newX := x s1: a1 | s3: a3. for example.Naiad
What are you trying to achieve? I am not sure how to write understandable code with that construct.Italicize
This sort of construct is used pervasively in Ruby on Rails. When constructing queries with ActiveRecord, one can do things like, Foo.where(...).order(...).select(...) but Foo isn't changed. The semantics of the code are quite clear. Maybe in this case, in the Smalltalk paradyme,I do need to just make a copy of Foo each time and let the operators modify it. I'll have to give that some thought.Naiad
You don't need it, just use the subject as in my answerItalicize
This gives me something to think about. Is FooSubject a kind of Foo in this case?Naiad
Probably a FooQuery. And it might have been created as FooSubject on: aFoo instead of just new. If the interface gets very wide, we'd be more likely to directly ask for the subject and work with that, or use a DNU handler to delegateItalicize
Thanks Stephen. I need to cogitate on this a bit... :)Naiad
I've just been learning Smalltalk, and I'm still learning that the object management is different than, say, Ruby, which I've done quite a bit. And this impacts how to do certain tasks. Of course my first inclination is to see if what I want to do in Smalltalk maps to how I'm used to doing it in another language. I think in Ruby, the construct of x.s1(a1).s2(a2)...sn(an) does not create intermediate object copies for each dotted method call, although each subchain could represent an object on its own if so-called (e.g., x.s1(a1).s2(a2) is an object of the same type).Naiad
In the example I gave where selectors don't have arguments, e.g., x s1 s2 ... sn would that be doing a copy between each selector and, therefore, also be inefficient?Naiad
There is either the object itself that is not copied, a copy, or an immutable data structure (that has overhead itself)Italicize
Imho, having the cascade operator do cascading instead of chaining is a design mistake of the language. I would much rather have a cascade method implemented on object that returns a wrapper which overrides doesnotunderstand by forwarding the method call to the wrapped object and returning itself. With easy unwrapping with an end method on the wrapper. Chaining is a much more useful operation than cascade, in a much wider range of situations.Laurellaurella
@Laurellaurella why would I need a chaining operator? That is the default behavior (returning self). There are all kinds of proxy and wrapper implementations in smalltalk, with different properties. I have not yet found a language design decision in Ruby that I like better than those of smalltalk, but please explainItalicize
The fact that returning self is the default behaviour is exactly what makes the cascade operator so useless compared to chaining! What I can't do in smalltalk is something like foo.filter(stuff).bar(x,y).map(stuff).collect().baz(z,w) . The smalltalk equivalent would be (((foo filter: stuff) bar1: x bar2: y) map: stuff) collect baz1: z baz2: w. which is basically unreadable. Self handles this much better by forcing a case convention on keyword messages so that you can chain them: (foo filter: stuff bar1: x Bar2: y map: stuff) collect baz1: z Baz2: wLaurellaurella
In short, I really dislike how modern Smalltalk implementations are so damned conservative. Smalltalk's syntax is interesting, but it is hardly a god-given jewel or even a local optimum, and there are a ton of ways in which it can be made more usable.Laurellaurella
Well, we tend to solve problems at the framework side, not the language. That is essential for our ability to continue innovating, as it reduces the costs of building tooling.There are several implementations of lazy collections (and of immutable ones) which are simple enough. I don't have enough experience with self to decide if train wreck style programming works well enough for me there. In more traditional languages it doesn't.Italicize
C
7

There seems to be some confusion here.

  1. Cascading is done using ;, where each message is sent to the same argument.
  2. Your x s1 s2... example (which you called "cascading") is called message chaining, where each message is sent to the result of the previous message (due to the beautiful and elegant nature of Smalltalk's syntax, which is simply Object message and always returns a result). And no, you can't always put them in any order. Imagine s1 is multiplyByTwo and s2 is addTwo, with x having an instance variable modified by these methods that starts as 1. Order may matter. (Please note that I'm very reluctant to use such awfully short names as examples - it's not very Smalltalk-like. ;-)
  3. If your n is so large that using parentheses makes it look awkward, then, uhm... sorry, but you're doing it wrong.
    • Consider creating a method on your object to accept multiple arguments.
    • Consider creating a wrapper object to carry your parameters.
    • Consider using a design pattern to decouple your object from the operations you want to perform on it. (Which pattern you need will depend on what you want to achieve; here's a good starting point.)

Rather than looking for an operator to bend Smalltalk to your will (of how you may be used to doing things in other languages), I'd recommend bending to Smalltalk's will instead (explicit naming, not being afraid to create more objects and methods, etc.). You'll get more out of it in the long run, and I dare say you'll get to the point where you wish other language had Smalltalk's simplicity and power.

Collimate answered 9/8, 2015 at 2:30 Comment(6)
Thanks for the input. I apologize I had the wrong terminology. I have recently started learning ST, and had already forgotten the difference of terms. As far as how large n is, how large is too large? I think if it's 4 or 5 it looks ugly. But that is subjective. Certainly I am trying to understand the language philosophy better, and have read a lot about it. I am not necessarily "trying to bend it to my will" but seeking understanding. To me (a recent convert to ST), the ability to chain only selectors that don't have arguments seemed inconsistent to me.Naiad
I realize I can't always put messages in the order that I want. I was talking about a problem in which, in my case, I did want to do so. Therefore, I couldn't treat it as s1:s2:s3:... which forces order. Thanks for providing the link, I'll read it.Naiad
No need to apologise, just thought I'd clarify, because I got confused when first reading through the question. And of course you're correct about it being subjective. I just find that people coming to Smalltalk from other languages tend to be more reluctant to create new (small) objects and methods. Fair enough about the message order, thanks for explaining. Sounds like you're probably after a pattern, then. Kudos for trying to get Smalltalk! :-)Collimate
No worries. I am actually not shy about creating the objects and methods (and aside: should I call them messages? selectors? methods? or do they refer to the same thing?). I was trying to describe my problem generically without getting into a lot of additional detail, but I seem to have muddled it a bit in the process. But between you and Stephen, you have given me more food for thought. So, thank you!Naiad
Glad to hear it. As for your aside, the three are sort of interchangeably used, but strictly speaking, the selector is just the name of the method (e.g. from:to:do:), a message you send would include the selector and any arguments (e.g. you'd send from: 1 to: 10 do: aBlock to a collection), and the method is the implementation of the whole thing on the collection class, including the method body. Hope that makes sense.Collimate
Makes perfect sense. Thank you.Naiad
I
1

Normally we give this object x a subject to operate on, and then ask for this changed subject when we're finished with it. Copying is rather inefficient.

Object subclass: #Foo
    instanceVariableNames: 'subject'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Try'

order: y
    self subject order: y

select: z
    self subject select: z

subject
    ^subject ifNil: [ subject := FooSubject new ]

where: x
    self subject where: x
Italicize answered 8/8, 2015 at 20:38 Comment(15)
I understand. I was looking for a way to get the same effect as x.s1(a1).s2(a2)...sn(an) as is done in Ruby, for example, without having to resort to all the parentheses. The subject x is something I might want to apply different operations on at different times with different sets of selectors. So I don't wish to change it. If it changes, then I have to make copies of it anyway in that case so I have it in its original form again. If I want to save an object that has some number of operations done on it, I'd do it by, newX := x s1: a1 | s3: a3. for example.Naiad
What are you trying to achieve? I am not sure how to write understandable code with that construct.Italicize
This sort of construct is used pervasively in Ruby on Rails. When constructing queries with ActiveRecord, one can do things like, Foo.where(...).order(...).select(...) but Foo isn't changed. The semantics of the code are quite clear. Maybe in this case, in the Smalltalk paradyme,I do need to just make a copy of Foo each time and let the operators modify it. I'll have to give that some thought.Naiad
You don't need it, just use the subject as in my answerItalicize
This gives me something to think about. Is FooSubject a kind of Foo in this case?Naiad
Probably a FooQuery. And it might have been created as FooSubject on: aFoo instead of just new. If the interface gets very wide, we'd be more likely to directly ask for the subject and work with that, or use a DNU handler to delegateItalicize
Thanks Stephen. I need to cogitate on this a bit... :)Naiad
I've just been learning Smalltalk, and I'm still learning that the object management is different than, say, Ruby, which I've done quite a bit. And this impacts how to do certain tasks. Of course my first inclination is to see if what I want to do in Smalltalk maps to how I'm used to doing it in another language. I think in Ruby, the construct of x.s1(a1).s2(a2)...sn(an) does not create intermediate object copies for each dotted method call, although each subchain could represent an object on its own if so-called (e.g., x.s1(a1).s2(a2) is an object of the same type).Naiad
In the example I gave where selectors don't have arguments, e.g., x s1 s2 ... sn would that be doing a copy between each selector and, therefore, also be inefficient?Naiad
There is either the object itself that is not copied, a copy, or an immutable data structure (that has overhead itself)Italicize
Imho, having the cascade operator do cascading instead of chaining is a design mistake of the language. I would much rather have a cascade method implemented on object that returns a wrapper which overrides doesnotunderstand by forwarding the method call to the wrapped object and returning itself. With easy unwrapping with an end method on the wrapper. Chaining is a much more useful operation than cascade, in a much wider range of situations.Laurellaurella
@Laurellaurella why would I need a chaining operator? That is the default behavior (returning self). There are all kinds of proxy and wrapper implementations in smalltalk, with different properties. I have not yet found a language design decision in Ruby that I like better than those of smalltalk, but please explainItalicize
The fact that returning self is the default behaviour is exactly what makes the cascade operator so useless compared to chaining! What I can't do in smalltalk is something like foo.filter(stuff).bar(x,y).map(stuff).collect().baz(z,w) . The smalltalk equivalent would be (((foo filter: stuff) bar1: x bar2: y) map: stuff) collect baz1: z baz2: w. which is basically unreadable. Self handles this much better by forcing a case convention on keyword messages so that you can chain them: (foo filter: stuff bar1: x Bar2: y map: stuff) collect baz1: z Baz2: wLaurellaurella
In short, I really dislike how modern Smalltalk implementations are so damned conservative. Smalltalk's syntax is interesting, but it is hardly a god-given jewel or even a local optimum, and there are a ton of ways in which it can be made more usable.Laurellaurella
Well, we tend to solve problems at the framework side, not the language. That is essential for our ability to continue innovating, as it reduces the costs of building tooling.There are several implementations of lazy collections (and of immutable ones) which are simple enough. I don't have enough experience with self to decide if train wreck style programming works well enough for me there. In more traditional languages it doesn't.Italicize
S
0

All the above is fine, if you read and grok all the comments. But to make things a bit more clear, Stephan's answer above does provide the way to construct a solution, but it doesn't explain how to use it.

If one is " looking for a way to get the same effect as x.s1(a1).s2(a2)...sn(an) " you can use his formulation:

>>order: y
self subject order: y

>>select: z
self subject select: z

>>subject
^subject ifNil: [ subject := FooSubject new ]

>>subject: anObject
subject := anObject

>>where: x
self subject where: x

by going back to using a 'cascade' of messages, like this:

result := x select: y; where: z; order: anOrder; subject

or like this:

x subject: theTarget; select: y; where: z; order: anOrder.
result := theTarget.

wherein the cascade is now affecting the captured "subject". So you end up with the desired effect, in this spacific case, but all the messages must operated on the one "captured" subject. You can't really get this effect when the intent is to 'chain', wherein the result of each message becomes the receiver for the next message. A small, but sometimes irritating design flaw that really should get fixed.

Syllabify answered 19/5, 2023 at 22:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.