My Objection to Array#sum
github.com
github.com
Personally, I think the big mistake is to think objects should represent things in the real world instead of adapting to the needs of the program. For example, it's very common to need to split a class into FooLike (an interface), FooImpl, and FooRenderer to avoid coupling specific display logic to business objects you may want to reuse in other contexts. These have no analogs in the real world. They're there largely to make sense out of the different things your program needs to do with a Foo.
It would be cool but probably perlishly unadvisable if join,sum,any,all were just implemented as
"\n".join(lines) -> lines.join("\n")
sum(alist) -> alist.join(+)
any(alist) -> alist.join(or)
all(alist) -> alist.join(and) sum = foldl (+) 0
product = foldl (*) 1
and = foldl (&&) True
or = foldl (||) False
any p = or . map p
all p = and . map p
concat = foldl (++) []
unlines = concat . map (++ "\n")
Google calls it "reduce" as well - this is what MapReduce is based upon.(BTW, bad search query: [haskell unlines] gives me the Haskell prelude definition as the last result on the first page, even though that's the authoritative source. It gives me the Zvon and Informatik mirrors as #1 and #2, and then lots of useless discussion. Couldn't there be a way to boost "authoritative" sources up the rankings?)
To me, Python can't seem to make up its mind with these weird (IMHO) stand-alone methods like join(), sum(), any(), etc. That's the kind of thing I'd expect in a functional language, not in an OO language.
"list.join(str)" and "str.join(list)" are not the same. The list.join syntax only works for lists, but str.join takes any sequence, which is anything that implements the sequence protocol, including lists, dicts, sets, trees, heaps, iterators in general, and a limitless variety of user-defined sequences. Once you understand that it's not a matter of picking two spellings of the same functionality, but actually radically different functionality between the two spellings. (Putting this on string rather than it being a free-floating function is perhaps dubious, but at least the operation does have a sort of irreducible stringy-ness to it in a way that it does not have a listy-ness to it.)
If you make the "join" a method of list, then you have grotesquely cut its functionality. Now you are requiring people to implement their own join on every other object that implements the sequence protocol, which is just silly. (Of course if they have other needs they may still implement something else, but why not give them the option of the default?)
This is generic-programming through duck-types, not OO. Similarly for all the other examples you cite and quite a few more; putting them on "list" is not "the sensible thing to do", it would be a grave error.
Python 2.5.2 (r252:60911, May 7 2008, 15:19:09)
...
>>> ", ".join(str(x) for x in xrange(10))
'0, 1, 2, 3, 4, 5, 6, 7, 8, 9'
That's not a list in there.To me it seems unnatural to see join on string because string feels more 'basic' than a sequence, so I would expect the sequence to know about strings rather than the opposite. But the argument against join on string is not as strong as the argument against sum on array.
See http://docs.python.org/library/stdtypes.html#iterator-types and http://docs.python.org/library/stdtypes.html#http://docs.pyt...
You can't put join there. Because then you have to put add, any, none, all, reduce, filter, and so on and so forth for at least another ten or twenty base methods (ignoring everything else that applies just to the sequence protocol written by users), which every single implementation would then have to provide. No matter how abstractly beautiful it may be that this implements object-orientation dogma, in practice it's an idea too stupid to even begin to consider.
BTW, there are other cases where Python has gone the other way. Multiline lambdas, for instance. The case against multiline lambdas is essentially that they look ugly - they let you embed an indentation-sensitive block in what might be a parenthesized expression, which means you need some dangling delimiters. But they're certainly useful, as Scheme/Lisp/Ocaml/etc. have shown.
It's similar to the compare-constants-from-the-left idiom in languages where assignment is a valid expression (notably C). If you always write "if (NULL == foo)" instead of "if (foo == NULL)", you eliminate a whole class of bugs. But I've seen very few programmers do this, because it "feels" wrong to a lot of people.
I shouldn't have said that the join argument was "purely aesthetic", I should have said that it was "purely aesthetic and trivial", because you're right, aesthetics are important, and (imo) are reason enough to disallow multiline lambdas. How do you cross the line from trivial to not? I don't know exactly, but I do know that swapping the position of the involved nouns and taking up the same number of characters for a good technical reason doesn't.
I would support making the C compiler just disallow "if (x = y)".
$.each(my_array, function(val) {
// Do something
// Do something else
});
That's putting the most complicated object last, and feels much more natural than: $.each(function(val) {
// Do something
// Do something else
}, my_array);
I suspect it's more that English has trained us to read "noun verb object" sentences. In an if statement, `foo` feels like the subject, and then "== NULL" is the predicate. In an array map, the array is the subject, and the function is the predicate. In the Python join example, the array feels like the subject, "join" is the verb, and "," is the object, which is what makes it seem so awkward. Ruby's array.sum feels more natural because the array is the subject and sum is an intransitive verb.In this case, their objection is that it violates their intuition, but the placement of join is a natural result of how Python's types are defined. Defining join on all sequences would have real (annoying) implications.
How does that not fit this situation? Because there's actually a good technical reason to do it the way it's currently done? Wouldn't that imply that there should be even less discussion of it?
Bike-shedding is when the arguments have relatively the same merit, and the choice makes little difference in the end. I don't think either applies.
An example of bike-shedding in this context would be renaming join to fuse.
Bitching that '\n'.join(alist) should be alist.join('\n') is a favorite pastime of people who want to demonstrate that they've heard of OO programming. It's a perfect example of bike-shedding.
http://www.freebsd.org/cgi/getmsg.cgi?fetch=506636+517178+/u...
http://en.wikipedia.org/wiki/Color_of_the_bikeshed
http://catb.org/jargon/html/B/bikeshedding.html
http://www.urbandictionary.com/define.php?term=bikeshedding
All of these definitions require the issue to be of marginal benefit to the overall problem. The observation that people feel the need to contribute their opinion on matters they feel they know about is why bikeshedding happens, but it's not bikeshedding itself.
The more important point was that you can't call someone wrong when they're using the actual, original definition correctly, which I was (the link I included was how the term entered the geek lexicon).
But. All of this is a digression. The entire reason I said I don't think this is bikeshedding is because I think there's value in discussing why join is a member of strings and not sequences in Python. Understanding this point helps in understanding Python's design.
If you write your own class that is iterable but doesn't inherit from list, it would not have a join() method unless you wrote one. But it works as an argument to '\n'.join() for free since that can operate on anything iterable.
The other problem with sticking methods like join(), sum() and any() onto collections is that whenever a new method is added (for example, any() was added in python 2.5), it would cause confusion for anyone who has a list-like class that happens to have a method with that name that does something different.
In Ruby, this is accomplished through Array#inject, which allows you to specify the procedure that gets applied as you're joining. This makes it easy to do summing, string concatenation, etc, etc.
I don't buy into this idea of interface as binary - that if you respond_to? the message, it's always appropriate, and if you don't, it's not. Interfaces are something you look at when you're writing the program, and so they are interpreted by humans and can be necessarily fuzzy: "#sum will give you the total of the elements in the array, unless they aren't homogenous, in which case you'll likely get an error". Fine. If you don't know enough about the array in question, don't send #sum.
Similarly, if you don't know that this is a chequing account, it would be odd to ask it to write a cheque.
But at runtime, you send the message, and you maybe get an error. Whether this is TypeError or NoMethodError seems entirely irrelevant.
Incidentally, my objection to Array#sum is that you don't know what to use as the "0" element (what if the elements implement +, but aren't numbers), though I'd be fine with it being called #numeric_sum or the like.
I can't help thinking a cleaner solution is the more traditional way: Create a subclass called NumberArray that can only hold numbers and has specific methods for operating on them. But I guess that might not be the "Ruby-way".
I can't see anything wrong with this for some cases. Another way to get there would be to define a NumericCollection module that can be used to extend any Array.
This lets you skip subclassing.
This is how I felt when I first saw Array#sum. But then I took a few hi-
But then I tried it a few times. And now I can quit anytime I want.
I think String#titlecase is correct though. Not all arrays can be summed, but all strings can be titlecased, including part codes.
Of course your :) might have meant you were being funny, then I just totally missed your point and explained something you already know.
I checked irb and "ß".upcase is "ß", so I guess strings in ruby are of type "german legal document."
I also just learned that the reason there's no uppercase ß is that no words start with ß, so titlecase would never touch it.
That being said, I suspect that this turns out to be a cultural debate more than anything else. Remember, Ruby is a freedom language. Ruby could eliminate many of the issues you raise by being strictly typed, but of course that'd defeat the purpose.
The whole thing is a question of trust. Do you trust clients enough to give them the syntactic sugar of Array#sum even if their arrays may not all be strictly summable?
The answer to that question if you're writing Java is probably no. If you're a Rubyist, however, the answer is a resounding maybe. For something like ActiveSupport, it's probably OK. For other more hostile environments, maybe not.
As to whether it's idiomatic Ruby to provide Array#sum, I'd argue no, even though it's culturally acceptable.
I think saying "Ruby is a freedom language" is a cop-out for foregoing good design. It excuses us from having to think through if the design makes sense, because we can always throw up our hands and say "Ruby is a freedom language." But I also think that's not an entirely true statement: Ruby doesn't let programmers jump to any random line of code. (I did find a neat module that implemented gotos and labels, but I think it only works at proc granularity, not line granularity.) It doesn't let programmers muck with the the stack frames or manage their own memory.
The reason for these non-freedoms is that past experience has shown us that in some domains, some language features are high risk, low yield. I see the freedom of Ruby as a chance to experiment with many different ideas for language design, and the practical value of that is the languages we design in the future will have those lessons baked in. But by defaulting to the idea that it's a "freedom language," we resist learning.
I say this as a non-Rubyist. I wanted to learn a dynamically typed language, and I chose Python. I don't have the time now to delve deeply into two languages. So I'm commenting on this as an observer, but I'm an observer who sees the value in Ruby, and potential implications to future languages.
I think some people could use that designation as a cop-out, but I don't think I'm doing that above. The point I was trying to make is that Ruby, by design, is not strictly typed and allows you to do things like metaprogramming easily. I'm certainly not trying to be an apologist for poor design.
I agree that Ruby should embrace Freedom, but that does not mean that all choices programers make freely are good ones. This is not about whether programmers accidentally call #sum on arrays that cannot be summed. It is about whether, when I look at the methods implemented by Array, it describes what arrays do or what arrays sometimes do or may do once in a while.
I actually think that Array#sum is the least objectionable in a single project where summing the elements may be common, maybe objectionable in ActiveSupport where every Rails project gets it whether they sum arrays or not, and absolutely wrong in Ruby's core libraries where every Ruby programmer will get it.
Free choices should be scrutinized most carefully when they has the broadest impact.
Sure. I think it's in Ruby's culture for people to write ultra-cute convenience methods and DSLs for this sort of thing, even if it makes it possible for someone to write code that doesn't make semantic sense.
I actually think that Array#sum is the least objectionable in a single project where summing the elements may be common, maybe objectionable in ActiveSupport where every Rails project gets it whether they sum arrays or not, and absolutely wrong in Ruby's core libraries where every Ruby programmer will get it.
Agree 100%. Arrays in Ruby core shouldn't be summing, they should be injecting.
And what would you get if you mapped over a Numbers object? An Array or another Numbers object? Looks like you'll have to specify the return type for the block. Even then, is an array of numbers really always a Numbers object?
I suppose that in this case, you could use a Numbers module and (somehow) make every array of numbers magically inherit this. But a generic, extensible way of doing this would be really, really complex and hard to do efficiently. Does every newly created collection have to be checked for Numbers-ness?
Really, you can go as wild as you want to, but I'd rather just be able to map, transpose and flatten around without all of this bureaucracy. Besides that, I'm fairly certain this is only a problem in single-dispatch languages where methods are owned by classes.
I wonder if the solution might be to have all arrays support sum (in the responds_to() sense) only if they can can actually perform the operation.
Thus
[1, 2, 3].respond_to?(:sum) => true
[1, [2, 3]].respond_to?(:sum) => false
I can come up with an O(1) way to do this where we assume that an array does respond to sum, and degrades to not supporting sum when a non numeric is inserted ... but getting the array to support sum again if/when the non-numeric is removed... I'd have to think for a bit to come up with something better than O(n)...
[1, [2, 3]].respond_to?(:sum) => false
I elided a discussion about this case from the post. For some arrays, a recursive sum is semantically valid. For other arrays a recursive sum is not semantically valid, and it is not a simple case of knowing whether all of the leaf elements respond to :+. So again, trying to implement this in the Array class doesn't work because containers are implementation rather than interface.If you are going to use an Array class, I think the array's client is the one responsible for knowing whether its elements can be summed and if so, whether recursive summing is valid. The other approach is to subclass or otherwise create special-case arrays that know about their semantically valid operations like sum.
His answer is similar: just because you know the structure of something doesn't mean you know anything about its semantics.
Because of this, I'm tending to believe that adding more strong-typing to languages is a losing battle, though I admit this raises more questions than answers.
I see this at the opposite.
Just because an object says that it responds to a method doesn't guarantee that invoking said method will succeed. I'm not sure why the original poster is so surprised by this finding.
Since this observation is valid in both statically and dynamically typed languages, I prefer a statically typed one since at least, I don't need to verify that the object does respond to that method before calling it.
The article is correct .sum shouldn't be on an array, but sum shouldn't be global either. This is why generics exist.
This is done in Smalltalk with detectSum.
people detectSum: [ :person | person age ].
items detectSum: [ :item | item price ]. class Array
def detectSum(&block)
self.map(&block).sum
end
end
>> [1, 2, 3].detectSum {|i| i**2}
=> 14
Most Ruby people though would just call #map and #sum in succession on an ad hoc basis. detectSum: aBlock
"Evaluate aBlock with each of the receiver's elements as the argument.
Return the sum of the answers."
| sum |
sum := 0.
self do: [:each |
sum := (aBlock value: each) + sum].
^ sum
Anyway, my point still stands. Better to accept a block than to use a sum method that assumes the Array consists of numbersWhat this comes down to for me is: what makes the best interface?
Specifically:
myArray.sum()
is much easier to read and use. In a world where we have to maintain our code much longer then we write it this simplicity is very valuable.Secondly, it is important to note that since the array uses generics it is not necessary to check that the items are numbers, as any attempt to insert a non-number would have thrown an error.
So in summery I agree with you completely that array<variant>.sum should _not_ exist. But I hope you see my point that array<number>.sum _should_ exist. (If the code enforces the numbers to exist in the array there is no reason not to include the sum method!)