Differing performance characteristics should not break the semantics of an abstraction. That's an abstraction leak which prevents writing generalized abstractions.
But I guess in terms of strings let's take a moment and remember the one truly good design decision that Python devs made about strings (which isn't usual for an imperative language). Strings are immutable by default.
Erm, Haskell people write code that can do this all the time - you write stuff in terms of generic monoid and then it just does. I can even think of a concrete case where I used those two examples: I had essentially a report that looped over a bunch of rows and accumulated something as a secondary effect, and in one client's case that was a string explaining what had happened, and in another client's case that was a count of how many things had been processed.
Batman!
It's not possible to write the generic code since there are basically no constraints. Generic code is all about constructs with certain invariants and use those invariants to create something new. Javascript addition basically has no invariants.
I would argue that that's not true.
JS is a "mono-typed" language and there is therefore only one type of things you can operate on and return as a result.
>It's not possible to write the generic code since there are basically no constraints. Generic code is all about constructs with certain invariants and use those invariants to create something new. Javascript addition basically has no invariants.
That's backwards to my point. I tried to point out that having generic code which can operate on (almost) arbitrary data structures mixing for example natural numbers or lists of arbitrary things will lead in the end to the same puzzling results as the "classical JS-Batman" example.
Don't get me wrong, generics are a great tool! But imho it's easy to "overstretch" their usage until a point where things become "so generic" that your functions can operate on "almost everything" but the results become as surprising as the alleged "JS insanities".
Something like the Monoid abstraction is incredibly powerful (most languages can't express such an abstraction in a sensible way even). Using such a powerful tool to abstract over some very concrete and banal things (that would be handled better separately as they're "not the same", no matter there may be a deep mathematical connection) is not a good architecture imo. If your code handling "concrete things" is written in a very generic and abstract way you won't have any "normal" tools left on your belt in case you would need to abstract over that stuff you've just written (at this point you could enter type-level programming or something that will achieve something similar but you will than end up with laughable complex code to handle in the end banal business logic).
Well, in that case programmers should not be surprised when they subtract an integer from a string and get NaN. Apparently they are are surprised though. I'd prefer my language not let me subtract an integer from a string, since if I ever attempt to subtract an integer from a string that's almost certainly an error.
> That's backwards to my point. I tried to point out that having generic code which can operate on (almost) arbitrary data structures mixing for example natural numbers or lists of arbitrary things will lead in the end to the same puzzling results as the "classical JS-Batman" example.
But that's not true. Nothing you do with a monoid will create such a puzzling result, because summing a list via a monoid is - by definition - equivalent to summing it by calling + pairwise. (Of course, if you have a type for which + behaves in a confusing way, then this might be confusing - but it's not the monoid that's being confusing there).
If you want to claim monoid-based code can be confusing, try using some actual examples of monoid-based code, rather than code in a different language that isn't using monoids and wouldn't compile if it did.
> If your code handling "concrete things" is written in a very generic and abstract way you won't have any "normal" tools left on your belt in case you would need to abstract over that stuff you've just written (at this point you could enter type-level programming or something that will achieve something similar but you will than end up with laughable complex code to handle in the end banal business logic).
This is completely wrong. If you write your code generically it becomes easier to abstract over, not harder. For example, if you write a sort function that operates generically from lists and separate that from your comparison function instead of writing separate functions for sorting lists of integers and lists of strings, it's much easier to then make further abstractions over your sorting.
> https://en.wikipedia.org/wiki/Rule_of_least_power
Again you're getting it backwards. Code written in terms of monoids is less powerful than code written in terms of a specific datatype, which means there are far fewer ways to get it wrong.
Javascript has a type system that is enforced at run-time.
> That's backwards to my point. I tried to point out that having generic code which can operate on (almost) arbitrary data structures mixing for example natural numbers or lists of arbitrary things will lead in the end to the same puzzling results as the "classical JS-Batman" example.
I don't agree. The entire point of the link you posted was the insanity of the semantic of the addition operations in Javascript. Monoids are very simple and well defined. The javascript insanity would need pages upon pages of explanations because basically everything is a special case. In contrast monoids can be fully explained in a two sentences.
>Javascript has a type system that is enforced at run-time.
Nobody questions that JS has "runtime type-checks". But that's irrelevant to my point. I've referenced of course this here:
https://existentialtype.wordpress.com/2011/03/19/dynamic-lan...
>Monoids are very simple and well defined.
Sure. Just define an instance for a type that models a JS runtime-object, let's see what happens… ;-)
But I see, my point is still not explained well: What I'm trying to say is that using abstractions that are so powerful that the resulting code can operate on (almost) arbitrary "unrelated" things using that code will lead to the same kind of puzzling outcomes as "JS Watman".
(And like I said, a deep mathematical relation doesn't make things "related" in a practical sense automatically. For example when it comes to "correct" domain modeling of business cases where you explicitly want to treat different parts of your system separately; as this results in a less "entangled" architecture. Gluing together parts that are unrelated through clever code on the other hands side creates maintenance hell where everything depends on everything, but you can't even see what depends on what as everything is generic; that's the opposite of good software engineering, imho. To throw a few phrases: "Divide and conquer!" "Use the right tool for the job!" "Use the least powerful tool that gets the job done well!").
>>> import numpy as np
>>> xs = np.arange(9).reshape((3,3))
>>> xs
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
>>> sum(xs) # builtin sum, not numpy sum
array([ 9, 12, 15])
>>> sum([[3, 11], [5]], [])
[3, 11, 5]
>>> sum([(3, 11), (5,)], ())
(3, 11, 5)
However, that doesn't work for strings; there's a special-case check for that.The problem with Python is that it doesn't have typing that can express "I can sum".
# Python 3.8
from typing import Protocol, TypeVar
T = TypeVar('T')
class Summable(Protocol[T]):
def __add__(self, other: T) -> T: ...
In Python 3.4-3.7 you'd need to install typing-extensions library: from typing_extensions import ProtocolC++ added such feature, this above looks like a Python version, Rust has it, Scala had them as one of the first "OOP" languages.
It's nice to see that Haskell pioneered a very useful and now spreading language feature!
Computing the number of bytes used by a data type seems perfectly reasonable use of sum in that context. This is also achievable in many languages. All 'sum' needs is a map from a value to a number, which can have many sensible meanings even for arbitrary data types.
sum(map(mappingFunction, iterable))
works fine as long as mappingFunction maps the elements of an iterable to numbers.This works
from functools import reduce
nums = [1,2,3]
def add(p, q): return p + q
print(reduce(add, nums))
strs = ["how", "are", "you"]
print(reduce(add, strs))
so why not that?This is true for lists in Python. `a = a + b` creates a new list, but `a += b` works in-place:
>>> a = [1, 2]
>>> b = a
>>> a = a + [3]
>>> print(a, b)
[1, 2, 3] [1, 2]
>>> a = [1, 2]
>>> b = a
>>> a += [3]
>>> print(a, b)
[1, 2, 3] [1, 2, 3]
There are other issues with the `+=`-style operators as well - IMO it was a mistake to have `a += b` be anything other than a shorthand for `a = a + b`.Concatenation is not addition in my opinion
I've often wanted something like bits++bits concatenation to work in the same way for numbers as for strings
For python, syntactically it is. In a general sense, an operation is what you define it to be. Just my opinion.
x+=y is equivalent to x.append(y)
x = x + y creates a new list, then assign it to x (highly inefficient in a loop)
Perhaps not stupid, but ignorant, and not just of the special casing in sum which exist because of a footgun.
sum is basically a readability alternative for functools.reduce for adding numbers, and it's special cased for strings because though it would superficially work for strings otherwise, the performance of repeated string concatenations is pathological in python (as in many garbage-collected languages where it forces many allocations), so a special cases was added which points people to the method to produce exactly the same result without the pathological performance issue.
If you are doing something more complex than numbers with heterogenous data that includes strings in a reduction by the addition operator so that the join you'd use with homogeneous collections of strings isn't what you want (I'm not sure that hypothetical is ever going to happen), use functools.reduce, which for readability you probably should be using in preference to sum() if you aren't doing a mathematical sum anyway.
No, you don't, because: (1) if your idea of a sum is just reducing with “+”, you use functools.reduce; and (2) if your idea of sum is a mathematical sum, you use the very slight shortcut of the sum() built-in function, which is (barring things like the runtime type checking) equivalent in result to:
def sum(iterable, start=0):
return functools.reduce(lambda x,y: x+y, iterable, start) In [17]: class Start(object):
...: def __add__(self, other):
...: return other
...:
In [18]: sum(["a", "b", "c"], start=Start())
Out[18]: 'abc'I'm not saying that's necessarily the correct design approach, but it's the one that Python has explicitly established as their guiding principle! A different language will take a different approach: Ruby, for example, likes to build multiple alternative ways of accomplishing the same thing, which leads to multiple aliases for built-in methods or various alternative flags that allow a method to behave in a number of ways. It shouldn't be a surprise that Ruby is fine with the equivalent operation:
irb(main):001:0> ["a", "b"].sum(identity="")
=> "ab"
But that's because Ruby's design sensibility is different from Python's. You might like one better than another, but they both come from a consistent vision of how to design programming languages.This philosophy falls apart immediately once you start using Python though. I have yet to see it actually be used for anything but not implementing features that someone wants.
mylist = [x*2 for x in range(1,10)]
but also do mylist = []
for x in range(1,10):
mylist.append(x*2)
That's two ways to do the same thing, and I bet you would find a lot of the latter in beginner's python code. I think a better choice would to just have a linter which enforced the correct usage of sum() and join(), and not the implementation itself. mylist = map(lambda x: x * 2, range(1, 10))The point stands.
a + b
f"{a}{b}"
"".join([a, b])
"{}{}".format(a, b)
If a == b, then you can also, which itself is a bit ridiculous: a * 2
Is mentioning StringIO considered cheating?I would feel the Zen if Python would have a separate concatenation operator, it could be even the same for strings and lists. Lack of the operator in popular languages is my pet peeve.
"%s%s" % (a, b)
(Still works at least as of python 3.6) ["a", "b"].sum("")As a reader, you don't know what an abstract operation does, just that it should obey certain rules. Writing code abstractly when abstraction is unnecessary removes information, which has a cost in readability.
Now if you want to write abstract code then it will be frustrating, but some languages are optimized for writing concrete code.
At least, in this particular case. I'm not sure how consistent Python is about this.
If 3 + 2 is allowed but you don't think "a" + "b" should work, why should 8.1 + 1.9 work? If your argument is that it's because they're numbers, what about a Complex library, or numeric vectors, or GMP? If I can do 3 + 2 and 8.1 + 1.9, I should be allowed to make Complex{0, 1} + Complex{4, 9} work, or Vector{1, 0} + Vector{2, 2}, or BigInt{99999...} + BigInt{9999...}.
I can sort of see the argument in dynamically-typed languages when you might not know what the operands are ahead of time, but for statically-typed languages like Rust and Go (Rust gets this very, very right FWIW)? There's no defensible argument against it. Why is `+` magic and holy, but a `fn add(...)` isn't?
> isn’t it already inspecting the type of the list elements
Actually it only checks if the type of start (the initial accumulator value) is a string. If the accumulator becomes a string later on, it's happy to sum strings.
if trying_to_sum_strings(args):
- complain_bitterly()
+ separator.join(args)EDIT: OK, I think if the example is sum([some_string, some_not_string], '') then there is a possibility that some_string + some_not_string might be a valid expression computing a value while ''.join([some_string, some_not_string]) would complain that some_not_string is not a string.
At least, readability suffers.
"".join(["a", "b"]) is a lot more intuitive than sum(["a", "b"], start="")
edit: A beautiful abstraction is the one that feels as good when it's read as when it's written.
No it isn't. The join method being on the separator string is a Pythonism that looks crazy coming from any other language.
join(xs, "")
xs.join("")
or whatever else you want to come up with is more readable than building more string stuff in to sum, not that having join be a str method is the best way. sum("abc") =
sum(ord(c) for c in "abc") =
sum([97, 98, 99]) =
294
because wow, i... can't think of why you'd ever actually want that. (though it'd be perfectly natural for byte arrays)It's ok for a meme to be relevant sometimes. Don't have to do a bunch of intellectual eye rolling just because a Simpsons character is making the point.
So if “sum” is a shortcut for “fold with +”, summing a list of strings wouldn’t make sense.
Python still isn’t as regular as it could be, of course, as it does use “+” for concatenating strings.
I think this can all be justified for ergonomic reasons -- using the simple “+” for strong concatenation is convenient, but “sum” is discouraged for strings because it’s slow.
It would be interesting to see a parallel-universe Python that didn’t allow “+” on strings. I wonder if it would have caught on.
Due to this, under a general definition of "correct" without the context of Haskell such a thing is still classified as generally incorrect. Instead it is more accurate to phrase it as "this is Guido's personal definition of correct." Not that there's anything wrong with that but this is generally unusual and opinionated behavior encoded into python and I wish people will acknowledge that.
Prelude> sum ["a", "b"]
<interactive>:4:1: error:
• No instance for (Num [Char]) arising from a use of ‘sum’
• In the expression: sum ["a", "b"]
In an equation for ‘it’: it = sum ["a", "b"]
You appear to be refuting a straw man.
Could you design a sum that automatically looked at its argument type and deferred to join for strings? Sure! And that'd be a valid design decision as well. But the specific choice Python made here is motivated and defensible: it's only "incorrect" if your personal definition of "correct" is "built with the same algebraic sensibility that Haskell's designers had," but at that point, all other languages are "incorrect" because they're not Haskell!