raganwald / ick

The Invocation Construction Kit: an ad hoc, informally-specified, bug-ridden, slow implementation of half of Monads.

nizze (author)
Tue Mar 24 05:26:25 -0700 2009
raganwald (committer)
Mon Apr 13 07:49:18 -0700 2009
ick / website / inside.txt
100755 250 lines (169 sloc) 13.268 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
h1. Inside the Invocation Construction Kit
 
h2. More about the four block structures
 
"Ick":index.html provides #let, a method for block-structuring Ruby code. As already shown, if you want someone's phone number only if they are a friend: @let(Person.find(:first, ...)) { |person| person.phone_number if person.friend? }@
 
This code makes it clear that you only need the @person@ variable inside the block. If you want to refactor this code, you know that the entire expression can move without breaking another piece of code.
 
h3. #let, #returning, #my, and #inside
 
As shown above, #let does the obvious thing: it passes a value to a block, it evaluates the block in the caller's environment, and it returns the result of the block. Hmmm...
 
There are two binary decisions to be made about every block: First, do you want to evaluate the block in the calling environment (which is how almost every block is evaluated in Ruby), or do you want to evaluate the block in the value's context. In other words, does _self_ stay the same, or does it become the value in the block?
 
If you want something that behaves like #let but evaluates in the value's environment just like #please, you can use #my:
 
<pre syntax="ruby">
my(Person.find(:first, ...)) do
first_name = 'Charles'
last_name = 'Babbage'
friends << 'Ada Lovelace'
end
</pre>
 
This will return Charles Babbage's friends. On the surface, _evaluates_in_value_environment_ is about the syntactic sugar of dropping an instance variable. But with a little thought, you can come up with some really cool way to (mis)use this capability.
 
So #let and #my both pass an expression to a block and return the result. Given that they both 'declare' _returns_result_, this is not surprising. But there is another choice: _returns_value_ instead of _returns_result_. Ruby on Rails includes the popular #returning method, and it works the same in Ick:
 
<pre syntax="ruby">
returning(Person.find(:first, ...)) do |p|
p.first_name = 'Charles'
p.last_name = 'Babbage'
p.friends << 'Ada Lovelace'
end
</pre>
 
This returns the person record, not the list of friends. The block is evaluated strictly for side effects. And what happens if we want to return the value and also evaluate in the value's environment?
 
<pre syntax="ruby">
inside(Person.find(:first, ...)) do
first_name = 'Charles'
last_name = 'Babbage'
friends << 'Ada Lovelace'
end
</pre>
 
The method #inside returns the value and evaluates the block in the value's environment.
 
(The four methods were inspired by "Michiel de Mare's":http://blog.rubyenrails.nl/articles/2008/02/18/our-daily-method-10-object-r-rs-ds-s post on the same subject, although Ick’s nomenclature is not compatible with Michiel’s. Michiel’s #rsss, #rrss, #rsds, and #rrds are called #returning, #let, #inside, and #my in Ick.)
 
h3. lets
 
Ick version 0.3 includes an experiemental form of #let, #lets, that allows you to bind multiple variables:
 
<pre syntax="ruby">
lets(
    :person => Person.find(:first, ...),
    :place => City.select { ... },
    :thing => %w(ever loving blue eyed)) {
  "#{person.name} lives in #{place} where he is known as the '#{thing} thing.'"
}
</pre>
 
You must have the "ruby2ruby gem":http://seattlerb.rubyforge.org/ruby2ruby/ installed to use #lets. You know the drill:
 
<pre>sudo gem install ruby2ruby</pre>
 
Like #let, #lets _evaluates_in_value_environment_ and _returns_result_. You can roll your own variations if you want something else. If you're familiar with Lisp, #lets is a lot like Lisp's simplest let macro. But not like letrec or let*. If you don't know the difference, then #lets will not suprise you. Basically, the binding of values to names works a lot like you would expect a hash to behave.
 
For example, if I wrote:
 
<pre syntax="ruby">
a = nil
a = { :foo => call_something_else(), :bash => a[:foo] }
</pre>
 
You would consider this bad code, wouldn't you? There's no expectation that the espressions are evaluated in any particular order. It's the same with #lets. This is probably a bad idea:
 
<pre syntax="ruby">
foo = nil
lets(:foo => call_something_else(), :bash => foo) { ... }
</pre>
 
@bash@ is not going to be bound to the return value of @call_something_else@, it's going to be bound to nil. The variables you bind are only visible inside the block.
 
h3. What about #try and #maybe?
 
The methods #try and #maybe are both implemented as _evaluates_in_calling_environment_, because that is least surprising. But when you're rolling your own, you might want to change that to make things more sugary. For example, here is a different version of #try:
 
<pre syntax="ruby">
class Please < Ick::Guard
  guard_with { |value, sym| value.respond_to?(sym) == true }
  evaluates_in_value_environment and returns_result
  belongs_to Object
end
 
please(...) { may.i.have.some.more }
</pre>
 
The method #please executes in the value's environment, and thus it can call methods directly. Here is the key point about Ick as compared to rolling your own methods directly: you can combine and recombine the parts to make new kinds of methods. As you just saw, we can make #try and #please out of the same building blocks. This is why Ick is a "contruction kit," not just a collection of handy syntactic short cuts.
 
h2. Wrap Music
 
The four methods #let, #returning, #my, and #inside are fairly simple. There's some finagling with what they return and their evaluation environment, but evaluation within the environment is 100% standard Ruby. But things get really interesting when we want to actually change Ruby's evaluation behaviour. Specifically, when we want to change what it means to call a method on the value we provide.
 
That's what #maybe, #please, and #try all do: when a method is called on the value, they intercept it and decide whether to call the method or prematurely return _nil_. Ick can't actually change the behaviour of the Ruby interpreter, so what it does is as close to evil metaprogramming as possible. Ick doesn't do any method redefinition or method chain manipulation, instead it performs its juju by wrapping the value in a delegator. Ick's delegators are called _Wrappers_.
 
Ick's built-in methods that use wrappers inherit from @Ick::Wrap@. When the result is returned from the block, it is unwrapped if it is wrapped.
 
h3. Hand Rolling
 
You can use Ick::Wrap along with your own wrappers by calling #invoke_wrapped:
 
<pre syntax="Ruby">
class MyWrapper < Ick::Wrapper
  def initialize(value)
    # ...
  end
  # ...
end
 
Ick::Wrap.instance.invoke_wrapped(Person.find(:first, ...), MyWrapper)
</pre>
 
If you need additional parameters for your wrapper, you can declare them in the initializer and pass them to #invoke_wrapped:
 
<pre syntax="Ruby">
class MyWrapper < Ick::Wrapper
  def initialize(value, extra1, extra2)
    # ...
  end
  # ...
end
 
Ick::Wrap.instance.invoke_wrapped(Person.find(:first, ...), MyWrapper, :foo, :bar)
</pre>
 
The methods #maybe, #please, and #try are all implemented as subclasses of Ick:Guard. Have a look at guard.rb to see how to hide the extra wrapping syntax that #invoke_wrapped requires.
 
h3. ArrayWrapper
 
Ick::ArrayWrapper wraps a collection of receivers. Any message sent to the wrapper is dispatched to each of the receivers in the collection. Here's a simplified implementation:
 
<pre syntax="ruby">
class ArrayWrapper < Wrapper
  
  def __invoke__(sym, *args, &block)
    @value.map { |_| _.__send__(sym, *args, &block) }
  end
  
  is_contagious
  
end
</pre>
 
What on Earth is it for? Specifically, what's wrong with just using Enumerable#map if we want to send the same message(s) to each receiver in a collection?
 
The answer is that when we use Enumerable#map, it executes the entire block once for each receiver. So if there are side effects in the block, they are executed once for each receiver as well. Whereas ArrayWrapper dispatches the messages to each receiver without executing side effects more than once.
 
Ick::Tee provides a method for using an ArrayWrapper with _return_value_ semantics. When you use #tee, it evaluates the block once, but dispatches methods to each of the values you pass in. It returns the first value you pass in. This can be handy for things like sending logging information to more than one log.
 
Here's one of the test cases you can find in the gem:
 
<pre syntax="ruby">
def test_tee_semantics
  array_logger_one = []
  array_logger_two = []
  line_number = 0
  [array_logger_one, array_logger_two].map do |log|
    line_number += 1
    log << "A#{line_number}"
    line_number += 1
    log << "B#{line_number}"
  end
  assert_equal(%w(A1 B2), array_logger_one)
  assert_equal(%w(A3 B4), array_logger_two)
  array_logger_one = []
  array_logger_two = []
  line_number = 0
  tee(array_logger_one, array_logger_two) do |log|
    line_number += 1
    log << "A#{line_number}"
    line_number += 1
    log << "B#{line_number}"
  end
  assert_equal(%w(A1 B2), array_logger_one)
  assert_equal(%w(A1 B2), array_logger_two)
end
</pre>
 
When we used Enumerable#map, the side effect @line_number += 1@ is evaluated four times, twice for each receiver. However when we use #tee, the side effect @line_number += 1@ is only evaluated twice, even though we have two receivers. Now that you know about the four execution possibilities encapsulated by #let, #returning, #my, and #inside, you can probably guess what #fork does when I tell you that its implementation _returns_result_.
 
h3. Beware!
 
Because Ick::Wrap is passing a wrapper into the block instead of the original value, you can get unexpected results under certain circumstances. For example, if you assign the value something else within the block such as an instance variable, when the block exits the value will still have whatever behaviour the wrapper creates.
 
Therefore, wrappers are _unidirectional_: they only work when you are calling methods on your value, not when you are passing the wrapped value as a parameter to any other method. This is hideous: why should there be any difference between @{ |value| value + 1 }@ and @{ |value| 1 + value }@?
 
Also, wrappers are _external_, they only affect messages sent to the wrapped value in your block. For example, if you are using #try to avoid NoMethodErrors, any messages you send to the wrapped value will handled by the wrapper. But what happens if the wrapped value sends itself a message that it doesn't handle? Aha, that will raise a NoMethodError!
 
Wrappers are a very leaky abstraction, which is why I try to name things a little closer to what they actually do than what they purport to do but don't really do. For example, we write @try { something.or.other }@ to emphasize that we are only trying the things you see in front of you.
 
h2. Why Ick?
 
As just mentioned, Ick is a construction kit. By all means install the gem and go wild with #let, #returning, #my, #inside, #try, and #maybe. But have a look under the hood. Ick uses classes and template methods to replicate what can be done in a few lines of explicit code. For example, Object#returning is implemented in Rails as:
 
<pre syntax="ruby">
class Object
  def returning(value)
    yield(value)
    value
  end
end
</pre>
 
So why bother with Ick? Well, if you want to make a method just like Object#returning, only _X_ (for some value of X), you can't do that without copying, pasting, and modifying. Ick's classes are included specifically so you can subclass things and make your own new kinds of methods that are variations of the existing methods.
 
Thus, the extra abstraction is appropriate if you want to use the built-in methods as a starting point for your own exploratory programming. And if you don't care, you just want the methods, by all means install the gem and just use them. Don't worry about the implementation unless you identify it as a performance problem.
 
h3. Where do you want to go today?
 
The point behind abstracting invocation and evaluation is that you can _separate concerns_. For example, which methods to chain is one concern. How to handle nil or an object that does not respond to a method is a separate concern. Should you raise and handle and exception? Return nil? log an error? Why should error handling and logging be intermingled with your code?
 
With Ick, you can separate the two issues. You can even make the handling pluggable. For example, if instead of calling #let you call your own method, you could sometimes invoke @Ick::Let@ with a block and other times invoke your own handler, perhaps one that logs every method called.
 
Have fun!
 
h2. Administrivia
 
h3. Home Sweet Home
 
"ick.rubyforge.org":http://ick.rubyforge.org
 
h3. How to submit patches
 
Read the "8 steps for fixing other people's code":http://drnicwilliams.com/2007/06/01/8-steps-for-fixing-other-peoples-code/.
 
The trunk repository is @svn://rubyforge.org/var/svn/ick/trunk@ for anonymous access.
 
h3. License
 
This code is free to use under the terms of the "MIT license":http://en.wikipedia.org/wiki/MIT_License.
 
h3. Shout Out
 
"Mobile Commons":http://mcommons.com/. Still Huge After All These Years.
 
h3. Contact
 
Comments are welcome. Send an email to "Reginald Braithwaite":mailto:raganwald+rubyforge@gmail.com. And you can always visit "weblog.raganwald.com":http://weblog.raganwald.com/ to see what's cooking.