A trick to have arbitrary infix operators in Python
tomerfiliba.com
tomerfiliba.com
You `import q`, then you can slap `q/` or `q|` in front of an expression to log the value (q/ for high precedence, q| for low precedence). These days, `f"{x=}"` is probably preferred, but I still wind up using q occasionally because of its simplicity.
I still use the @q decorator pretty often; it's really nice to be able to trace the arguments and return values of specific functions.
HTML links make it 10-100x more likely other readers will learn about the thing being mentioned (and thus help increase awareness and adoption of good tools and resources).
https://github.com/gruns/icecream
Example:
from icecream import ic
def foo(i):
return i + 333
ic(foo(123))
Prints ic| foo(123): 456The precedence is determined by the overridden operator. In this case the "|" operator has low precedence.¹ For a high precedence operator, override "%" with __mod__ and __rmod__:
4 |add| 5 %mul% 6 |add| 7 # mul happens before add
¹ https://docs.python.org/3/reference/expressions.html#operato...There’s more than that; Raku, for instance, recognizes two more classes: circumfix (pair of symbols defining operator surrounds operands) and postcircumfix operators (like circumfix, but with an additional first operand outside of a preceding the paid of symbols), and lets you define new operators in any of the five notational classes.
if_then_else_ : {A : Set} → Bool → A → A → A
if true then x else y = x
if false then x else y = y
[0] https://agda.readthedocs.io/en/v2.6.0.1/language/mixfix-oper... a ? b : c
This is a single operation `?:` that takes operands a, b, and c.ps: we're going back to APL it seems
This is on par with the "Generics in Go"^1 hack. Is there a list of this kind of funny language hack somewhere?
[1]: https://www.reddit.com/r/rust/comments/5penft/parallelizing_...
What about string + string?
I think the real rule is "it should be intuitive rather than confusing". :-)
Huh? Why?
More simply, "addition" is generally defined as a commutative operation, which this isn't. When people want a non-commutative operation, they call it "multiplication".
For a more superficial but tangible reason, if you just consider the notation, AB is the concatenation of A and B, which is the notation for a product in math. (Recall putting "A" "B" next to each other in Python literally means concatenation, and it's a product in math.)
1. juxtaposition is shorter than any alternative and hence a natural choice for the most common / only operation on the set in question
2. the operation is roughly analogous to function composition, and when you apply two composed functions it looks like f(g(x)) with f and g juxtaposed (if the functions are among those that have the longest history of being written as prefix operators, e.g. sin, cos, log, you don't even need the paren between the f and the g)
3. juxtaposition is a natural way to express an operation which is just "putting stuff together", such as string concatenation
So while it makes sense to denote string concatenation by juxtaposition in mathematics, I think it's a bit of a leap to go from there to say that A * B is the right notation for a programming language. (Actually using juxtaposition would be fine, but a general-purpose programming language is likely to want to reserve juxtaposition for something else.)
Also if you run with the idea that string concatenation = putting two strings together, well, addition is sort of like putting two numbers together. Rephrasing that in mathematical language: natural numbers can be encoded as elements of the free monoid on one generator, in which case the monoid operation turns out to be addition. I think that might be why the A + B notation is preferred over A * B when we have to choose one or the other.
Concatenation of strings is much closer to the tuple approach. And the argument about the number of elements in the type still holds if you keep the string-length fixed.
Multiplication of numbers is commutative, as is the dot product of vectors. So multiplication is sometimes commutative, sometimes not depending on what is being multiplied and how. So why cannot addition be likewise sometimes commutative and sometimes not?
Yes, that's true. But this seems to me like another argument against classifying concatenation as multiplication because if "A concat B" makes sense, then "B concat A" necessarily makes sense as well (notwithstanding that they produce different results).
Except it makes perfect sense. B concat A gives you B followed by A, as opposed to A followed by B. They produce different results, but they make perfect sense. Nobody claimed multiplicative operations cannot make sense if you swap the operands. The claim was they don't have to.
Like I said, in addition, not only do they make sense, but they give you the same result. Here they clearly don't. Therefore, it makes no sense to suggest concatenation is more similar to addition than multiplication.
To which I reiterate my response: because addition in plain English is frequently (if not always?) commutative, whereas multiplication in plain English is frequently NOT commutative (even though it sometimes can be).
But that is manifestly untrue. Ask a native English speaker what "ABC" plus "DEF" is and they will almost certainly respond "ABCDEF". Ask them what "ABC" times "DEF" is and they will almost certainly respond with something analogous to to WTF?
But regardless: I don't know why you're arguing with me back and forth like this. It's not like I set the convention. I didn't design your favorite programming language. I didn't even claim there are zero reasons to denote concatenation with addition in programming. (!) I was just trying to help you understand something I thought you genuinely had a question about: some reasons in favor of using multiplication that make sense, notwithstanding any reasons arguing for the opposite position. And like I told you in the beginning, this isn't absolute, you can quite literally find counterexamples even in math if you go Googling. But your goal was clearly something else entirely, so don't expect me to have anything else to add (whether commutatively or otherwise).
I am trying to understand if the claim you are defending:
> concatenation is a natural product, not a sum
has any merit. I'm "arguing" to see if you have any rebuttals to my objections to your reasoning before I conclude that no, it doesn't.
Going back over this thread I am puzzled by one thing: that original claim was not yours, it was from /u/naniwaduni. You opened with "string + string". Why are you now so vehemently defending "string * string"?
Oh, and just for the record:
> Make those two words mean something instead of literally giving them strings, and see what they tell you.
Well, yeah, of course. Addition applied to fruit is a different operation than addition applied to strings. So?
https://github.com/JuliaLang/julia/issues/1771
In particular:
https://github.com/JuliaLang/julia/issues/1771#issuecomment-...
Undoubtedly, it is a big surprise for new users (without exposure to free monoids :-)), that * is string concatenation.
By the way, in python juxtaposition ("abc" "def") is also valid.
user_name + ": " + attr1 + "| " + attr2 + "\n"
is more difficult to parse by eyeball for me then
concat(user_name, ": ", attr1, "| ", attr2, "\n")
This
user_name + ": " + attr1
is easier to eyeball than concat(user_name, ": ", attr1)Incidentally, the Wolfram Language has a similar notation for arbitrary functions: a~Plus~b.
E.g., in Python `foo|bar` is totally unambiguous right now, give or take some nuance as to whether it's in a string/comment/whatever, but outside that tiny bit of context it's clear what's a variable and what's an operator. As soon as I can define `oo` and `|ba` as operators though it's a lot less clear what's going on, and all parsers (including editors, not just the cpython interpreter) need to be able to understand those semantics to work reliably.
Edit: I'm having a tiny bit of trouble finding it, but my memory just came back to me, and there's a PEP floating around where Guido's rationale was just that there wasn't a solid use case so it wasn't worth implementing. Later the numpy folks introduced the @ symbol as a solid use case (matrix multiply), but nobody could come up with a sufficiently convincing argument for arbitrary operators -- we gained the @ operator and don't have the rest.
(when (0 . < . x)
(displayln "positive"))
Although in Racket you could also add your own syntax. > '%mult%' <- function(a,b) a*b
> 3 %mult% 5
[1] 15
Using infix operators as functions: > '*'(3,5)
[1] 15I liked GNU Guile for the fact that it's enabled by default, thus the following is a bit more readable in my opinion
(when { 0 < x }
(diplay "positive") (newline))A hypothetical `curly-infix` language could be used like this:
#lang curly-infix racket
(define (fact n) (if (= n 0) 1 {n * fact(n-1)}))
The reader for `curly-infix` can use a read-table to alter how
`{<infix-expression>}` is read. The result can the be passed
to the language after the `#lang curly-infix` which in the
example is just `racket`, but could be something else.Hmmm. Maybe I should make a `curly-infix` wrapping `infix` from my `infix` package.
(0 . < . x . < . 12)
read-syntax: illegal use of `.`
Typing those extra '.'s gets annoying very quickly too.Racket has some other things that are non-lispy. Support for alternate (maybe non-SEXP) syntax readers and for loops come to mind.
At least it's all simple. Sometimes terrifyingly simple, but simple.
They're useful too. Non-SEXP reader could be used to create a non-infix dialect of Racket. Loop macros like for/or can be used as a more ergonomic way than recursive functions to walk a list to find/transform one of its elements.
The double dot notation is mostly used for inequalites:
(x . < . 3) instead of (< x 3)
and for function contracts: (any . -> . boolean?) instead of (-> any boolean?)- https://github.com/borntyping/python-infix (https://pypi.org/project/infix/)
- https://pypi.org/project/infixed/
- https://pypi.org/project/betwixt/ (Diclaimer: author here)
I'd only reach for it for certain types of DSL where infix greatly eased readability (perhaps templating). I would also run it by a handful of people before committing it to verify I haven't gone completely insane.
Fwiw kotlin supports infix operators as well and they shine in dsl use cases.
a <<op>> b <<op>> cHaskell supports custom infix operators, but you end up having an unreadable soup of <* <$> <$ >>=
True to some extent. After learning haskell, I find these operators very readable actually.
Prelude> :doc <$>
and
Prelude> :t (<$>)
Depending on dialect there can be reader macros [its analog] in lisp which allows defining your own DSL that can hurt readability even more than operator overloading
That said, I doubt I will ever use it professionally. In fact, I would probably reject any changes in code review that used this kind of trick.
Why not? Operator overloading (if used well) is invaluable to expressiveness, and at least IMO related to a strong type system.
Abstract Algebra defines a whole formal nomenclature around defining operators such as add, subtract, etc., for _arbitrary structures_.
Sure, it is complex, but good _engineering_ sometimes requires complexity.
For real world examples, I would take numpy - its custom arrays are a bit jarring to newcomers at first, to be sure, but it is a damn _useful_ library.
You can implement everything numpy does with "normal" functions, but you will end up with either unmaintainable nonsense or spaghetti code, e.g. in pseudo-code, it is clear what this does:
Matrix Result = Matrix1^2+Matrix2^2
Versus:
Matrix Matrix3 = Square(Matrix1); Matrix Matrix4 = Square(Matrix2); Matrix Result = Matrix1.Add(Matrix4);
This code is intentionally incorrect - can you spot the difference immediately? It's trivially fixed, but only by running the processing stack in your head. This is a very common sort of error. People prefer Infix operators and then say that function soup is the only way to go...
Note also that, while tests might catch that your code is wrong, you have to know what the code should be doing in order to spot that. It may even be easier to verify by using e.g automated proof verification.
It also doesn't require you to modify/extend the operands' classes. As always, be responsible with such magic. Tools (and the compiler for error messages) won't recognize that |add| is supposed to be one unit. Autoformatting may extend it to | add | or even break it up into two lines...
Consider Tk-style page or GUI layout, as in http://canonical.org/~kragen/sw/dev3/alglayout.py (in OCaml I used `:` and `..` http://canonical.org/~kragen/sw/dev3/alglayout.ml):
pageheader |above| (leftsidebar |beside| vstack(*abstracts)) |above| footer
My Python version used `|` and `-` and `~`, which I think is super confusing, and I think would probably be a lot better with named infix operators like the above: vr, hr, dhr = [~String(s) for s in '|-=']
left = (('Chapters' | String('')) - hr -
(chapno | vr | [t | ~String(' . ') for t in titles]))
pages = 'Page' - hr - pnos
table = ~('*' - hr - '*' - ' ') | ' ' | dhr - (vr | left | pages | vr) - dhr
This produced this layout: * ========================================================================
- |Chapters Page|
* |----------------------------------------------------------------------|
| 0.|The unchained desert of the singing perfume. . . . . . . 1|
* | 1.|La brisa ilusa de la neblina abandonada. . . . . . . . 43|
- | 2.|The smiling murder within her young daughters. . . . . . 80|
* | 3.|Aquella hija tortuosa de su precipicio árido. . . . . . . 96|
| 4.|False villages of the foolish monks. . . . . . . . . . 114|
* | 5.|El roble sacrílego de mi sacerdote desmoronado y enlutado. . 124|
- | 6.|The brazen river of his filthy, fallen ivory. . . . . . . 166|
* | 7.|The laughing monster of his humble scream. . . . . . . . 203|
| 8.|The rough tattoo of his unchained abomination. . . . . . 219|
* | 9.|Los perfumes silenciosos de un árbol monstruoso muriéndose. . 237|
- |10.|The sweet tower of the smooth fire. . . . . . . . . . 247|
* ========================================================================
Such named infix operators would also take care of the coercions; instead of writing `('Chapters' | String(''))` I could write `'Chapters' |beside| ''` and let `beside` do the coercion.†— ⁂ —
Another application for weird infix operators is computation on a lattice (in the partial-order sense):
w |meet| x |meet| (y |join| z)
Boolean bitvectors are a lattice, so it might be forgivable to overload the bitwise operators for general lattice elements, but I would argue that the following notation is less readable than the above: w & x & (y | z)
— ⁂ —How about binary relations, as in http://canonical.org/~kragen/binary-relations?
neighbor = (x |compose| near |compose| x.T) |union| (y |compose| near |compose| y.T)
You could usefully apply this to composing SQL queries, for example. Instead of writing """SELECT clients.name, orders.id
FROM clients, orders
WHERE clients.id = orders.clientid
ORDER BY clients.name
"""
you could reasonably write clients.name |product|
(clients.id |compose| orders.clientid.T
|compose| orders.id)
or, with better namespace management, maybe just name |product|
(clients.id |compose| clientid.T |compose| orders.id)
— ⁂ —Maybe 3-D modeling, as in https://dercuano.github.io/notes/algebraic-graphics.html? Extrusion along a path is associative even if it contains rotation, right?
square = xline |extrudedby| yline
cube = square |extrudedby| zline
twistedcube = square |extrudedby| (zline |during| zrotate(2*math.PI))
— ⁂ —Maybe melodies, as in http://canonical.org/~kragen/synthgramelodia and http://canonical.org/~kragen/sw/aspmisc/gramelodia.py? The operators defined there aren't associative but they could be.
theme = bell.twice |then| bell.fifthlower |then| rest
tune = theme.twice.octavehigher |during| theme.slower
rhythm = boom |then| whack |then| boom.twice.faster |then| whack
play(rhythm.repeatedly |during| tune)
Of course the real test is not whether you can write code to say what it means straightforwardly; that's necessary but not sufficient. The real test is whether you get reasonable error messages when you write something like play(rhythm.repeatedly |during| then| tune)
____† In that particular case, if the code had a purpose other than being an experiment in ultra-minimalism, `ljust('Chapters')` would have been better: the particular goofy thing I was doing was that string boxes right-justify their strings if they get allocated more space than they need, while horizontal concatenation boxes allocate any extra space they receive to their rightmost box, so you can get left-justified strings by just stacking an empty string box to their right.
> Prefix (as well as postfix) operators are used in languages like LISP/Scheme, and have the nice property of not requiring parenthesis
This reads quite funny because Lisp is famous for its parentheses! The missing bit is parentheses aren't required but only in the binary case. Lisp supports n-ary operators hence why it needs parens.
But working with infix operators in other contexts would be a complete nightmare. Limiting what a given expression can "bind" to, what it can form a large expression with, is a way to limit the processing needed to understand a given piece of code. Being able to say one thing a hundred different ways isn't good if the read has to search for a hundred different maybe-meanings a given string might have.
object | dothis | dothat | dosomething
I suppose the piping operator is infix. I feel like some of the advantages of the notation object.f()
over f(object)
in object oriented programming are due to this.In Python, of course, one argument against implementing it is that there already is the getattr magic method (and the getattribute one), so even if it'd be possible to implement UFCS without breaking changes, it would at least complicate the way attribute access works even further. But, of course, one could always introduce a new operator/token (instead of ".").
But I also don't care for arbitrary infix operators (which I'm pretty sure f# supports but I haven't used it in a way where I run into them). I know I'm crazy but I almost wish we did away with ALL infix operators, even stuff like +/-* and just do everything as functions. You know, like Lisp :P Edit: I guess I should clarify as it sounds back and forth. For traditional stuff everything functions, with exceptions for things like piping that are purely about chaining operations together (both |> and >> in f# are examples of infix that's specific use case is different enough from say +-/* I think they warrant it).
C# got around this by introducing extension methods. You put static methods inside any static class whose namespace you've imported and label the first parameter with `this` before the type name. Then you're able to call that static method as if it was an instance method on anything that is or implements the type of the first parameter. Basically it's an explicit version of D's unified function call syntax.
I think the pipeline operator is nicer, but extension methods aren't a bad solution.
It's more like composition isn't it?
object | dothis | dothat
Is pretty much dothat(dothis(object))
If stdout is considered the return value at least.The pipe itself is just a sort of.. notation that a postfix function follows? Whereas in mathematics the function is prefixed, and the notation itself is infix and optional. (That's a knowingly rough and lazy description that any mathematician will hate...)
dothat(dothis(object))
is more like < object dothis | dothat
or dothis object | dothat
while the full composition object | dothis | dothat
is more like dothis(dothat(object(stdin_and_everything_else)))instead of: len(my_list)
to do: my_list | len
of course you can still do: my_list.__len__() but it's ugly
This the absurd notion that there is some one "best" way to do things...hard core disagree.
If this were true, there should only be one library, one way of doing anything, one codebase, and therefore only one program, nay even there should only be one programmer, and Python should be a pet project of this one programmer doing everything one way...
The fact that you can obfuscate python, means python has failed its ethos in this respect (https://pypi.org/project/python-obfuscator/).
Compatibility is good, a thriving ecosystem is better.
Critically, a thriving ecosystem is made by a hundred different ways of doing something, all doing something similar, not by making everything similar, so that the first time if fails, its game over.
in `one two three`, are one and two functions using prefix notation or is two an infix function that takes both one ant three as arguments?
It's not too bad if you have a small amount of infix functions to remember, but I don't want to second guess everything while reading code.
I think the earliest use I can remember is with the experimental FC++ library[1], circa 2001.
Well, of course boost has it, they use `<` and `>`:
[1] https://www.boost.org/doc/libs/1_78_0/libs/hof/doc/html/incl...
df[(df["col1"] == foo) | (df["col2"] == bar)] a <swap> b; def begin(\*args):
return args[-1]
begin(
func := lambda x, y: begin(
z := int(input()),
x + y + z
),
func(1, 2)
)It would be cool if they could be modified just like classes, I think - keep the original lambdas, and add something else nice like Haskell's Arrows.
key_value = Infix(lambda u,v: dict(zip(list(u),list(v))))
Let's goooo.Cool trick, but you can also just use def add(*args).
> Where is this documented
Insofar as it is, on the page linked. The underlying operator overloading mechanism that his leverages is documented in the official Python docs.
Not sure yet if it is possible to make this work for generic operators.
Or I did not understand the relevance of your comment to my pondering.